Last commit merged: 7146913c71
This commit is contained in:
LuftVerbot 2023-11-19 00:36:58 +01:00
parent e29dc62837
commit fa7b8427a2
86 changed files with 2504 additions and 2210 deletions

View file

@ -85,6 +85,7 @@ android {
signingConfig = signingConfigs.getByName("debug") signingConfig = signingConfigs.getByName("debug")
matchingFallbacks.add("release") matchingFallbacks.add("release")
isDebuggable = false isDebuggable = false
isProfileable = true
versionNameSuffix = "-benchmark" versionNameSuffix = "-benchmark"
applicationIdSuffix = ".benchmark" applicationIdSuffix = ".benchmark"
} }

View file

@ -11,6 +11,7 @@
-keep,allowoptimization class kotlin.** { public protected *; } -keep,allowoptimization class kotlin.** { public protected *; }
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; } -keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
-keep,allowoptimization class kotlinx.serialization.** { public protected *; } -keep,allowoptimization class kotlinx.serialization.** { public protected *; }
-keep,allowoptimization class kotlin.time.** { public protected *; }
-keep,allowoptimization class okhttp3.** { public protected *; } -keep,allowoptimization class okhttp3.** { public protected *; }
-keep,allowoptimization class okio.** { public protected *; } -keep,allowoptimization class okio.** { public protected *; }
-keep,allowoptimization class rx.** { public protected *; } -keep,allowoptimization class rx.** { public protected *; }

View file

@ -41,11 +41,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Tachiyomi"> android:theme="@style/Theme.Tachiyomi">
<!-- enable profiling by macrobenchmark -->
<profileable
android:shell="true"
tools:targetApi="q" />
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"
@ -173,8 +168,8 @@
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".ui.setting.track.AnilistLoginActivity" android:name=".ui.setting.track.TrackLoginActivity"
android:label="Anilist" android:label="@string/track_activity_name"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -182,69 +177,21 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:host="anilist-auth"/>
android:host="anilist-auth" <data android:host="bangumi-auth"/>
android:scheme="tachiyomi" /> <data android:host="myanimelist-auth"/>
<data android:host="shikimori-auth"/>
<data android:scheme="tachiyomi"/>
</intent-filter> </intent-filter>
</activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:label="MyAnimeList"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:host="simkl-auth"/>
android:host="myanimelist-auth" <data android:scheme="aniyomi"/>
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="shikimori-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.BangumiLoginActivity"
android:label="Bangumi"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="bangumi-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.SimklLoginActivity"
android:label="Simkl"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="simkl-auth"
android:scheme="aniyomi" />
</intent-filter> </intent-filter>
</activity> </activity>
@ -292,10 +239,12 @@
android:name=".data.updater.AppUpdateService" android:name=".data.updater.AppUpdateService"
android:exported="false" /> android:exported="false" />
<service android:name=".extension.manga.util.MangaExtensionInstallService" <service
android:name=".extension.manga.util.MangaExtensionInstallService"
android:exported="false" /> android:exported="false" />
<service android:name=".extension.anime.util.AnimeExtensionInstallService" <service
android:name=".extension.anime.util.AnimeExtensionInstallService"
android:exported="false" /> android:exported="false" />
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,6 @@ import tachiyomi.domain.entries.anime.interactor.SetAnimeFetchInterval
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate import tachiyomi.domain.entries.anime.model.AnimeUpdate
import tachiyomi.domain.entries.anime.repository.AnimeRepository import tachiyomi.domain.entries.anime.repository.AnimeRepository
import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.source.local.entries.anime.isLocal import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -79,16 +78,12 @@ class UpdateAnime(
suspend fun awaitUpdateFetchInterval( suspend fun awaitUpdateFetchInterval(
anime: Anime, anime: Anime,
episodes: List<Episode>, dateTime: ZonedDateTime = ZonedDateTime.now(),
zonedDateTime: ZonedDateTime = ZonedDateTime.now(), window: Pair<Long, Long> = setAnimeFetchInterval.getWindow(dateTime),
fetchRange: Pair<Long, Long> = setAnimeFetchInterval.getCurrent(zonedDateTime),
): Boolean { ): Boolean {
val updateAnime = setAnimeFetchInterval.update(anime, episodes, zonedDateTime, fetchRange) return setAnimeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
return if (updateAnime != null) { ?.let { animeRepository.updateAnime(it) }
animeRepository.updateAnime(updateAnime) ?: false
} else {
true
}
} }
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean { suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {

View file

@ -7,7 +7,6 @@ import tachiyomi.domain.entries.manga.interactor.SetMangaFetchInterval
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.entries.manga.model.MangaUpdate import tachiyomi.domain.entries.manga.model.MangaUpdate
import tachiyomi.domain.entries.manga.repository.MangaRepository import tachiyomi.domain.entries.manga.repository.MangaRepository
import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.source.local.entries.manga.isLocal import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -79,16 +78,12 @@ class UpdateManga(
suspend fun awaitUpdateFetchInterval( suspend fun awaitUpdateFetchInterval(
manga: Manga, manga: Manga,
chapters: List<Chapter>, dateTime: ZonedDateTime = ZonedDateTime.now(),
zonedDateTime: ZonedDateTime = ZonedDateTime.now(), window: Pair<Long, Long> = setMangaFetchInterval.getWindow(dateTime),
fetchRange: Pair<Long, Long> = setMangaFetchInterval.getCurrent(zonedDateTime),
): Boolean { ): Boolean {
val updatedManga = setMangaFetchInterval.update(manga, chapters, zonedDateTime, fetchRange) return setMangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
return if (updatedManga != null) { ?.let { mangaRepository.updateManga(it) }
mangaRepository.updateManga(updatedManga) ?: false
} else {
true
}
} }
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {

View file

@ -50,13 +50,14 @@ class SyncChaptersWithSource(
manga: Manga, manga: Manga,
source: MangaSource, source: MangaSource,
manualFetch: Boolean = false, manualFetch: Boolean = false,
zoneDateTime: ZonedDateTime = ZonedDateTime.now(), fetchWindow: Pair<Long, Long> = Pair(0, 0),
fetchRange: Pair<Long, Long> = Pair(0, 0),
): List<Chapter> { ): List<Chapter> {
if (rawSourceChapters.isEmpty() && !source.isLocal()) { if (rawSourceChapters.isEmpty() && !source.isLocal()) {
throw NoChaptersException() throw NoChaptersException()
} }
val now = ZonedDateTime.now()
val sourceChapters = rawSourceChapters val sourceChapters = rawSourceChapters
.distinctBy { it.url } .distinctBy { it.url }
.mapIndexed { i, sChapter -> .mapIndexed { i, sChapter ->
@ -138,12 +139,11 @@ class SyncChaptersWithSource(
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchRange.first) { if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
updateManga.awaitUpdateFetchInterval( updateManga.awaitUpdateFetchInterval(
manga, manga,
dbChapters, now,
zoneDateTime, fetchWindow,
fetchRange,
) )
} }
return emptyList() return emptyList()
@ -200,8 +200,7 @@ class SyncChaptersWithSource(
val chapterUpdates = toChange.map { it.toChapterUpdate() } val chapterUpdates = toChange.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates) updateChapter.awaitAll(chapterUpdates)
} }
val newChapters = chapterRepository.getChapterByMangaId(manga.id) updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
// Set this manga as updated since chapters were changed // Set this manga as updated since chapters were changed
// Note that last_update actually represents last time the chapter list changed at all // Note that last_update actually represents last time the chapter list changed at all

View file

@ -50,13 +50,14 @@ class SyncEpisodesWithSource(
anime: Anime, anime: Anime,
source: AnimeSource, source: AnimeSource,
manualFetch: Boolean = false, manualFetch: Boolean = false,
zoneDateTime: ZonedDateTime = ZonedDateTime.now(), fetchWindow: Pair<Long, Long> = Pair(0, 0),
fetchRange: Pair<Long, Long> = Pair(0, 0),
): List<Episode> { ): List<Episode> {
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) { if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
throw NoEpisodesException() throw NoEpisodesException()
} }
val now = ZonedDateTime.now()
val sourceEpisodes = rawSourceEpisodes val sourceEpisodes = rawSourceEpisodes
.distinctBy { it.url } .distinctBy { it.url }
.mapIndexed { i, sEpisode -> .mapIndexed { i, sEpisode ->
@ -138,12 +139,11 @@ class SyncEpisodesWithSource(
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchRange.first) { if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
updateAnime.awaitUpdateFetchInterval( updateAnime.awaitUpdateFetchInterval(
anime, anime,
dbEpisodes, now,
zoneDateTime, fetchWindow,
fetchRange,
) )
} }
return emptyList() return emptyList()
@ -200,8 +200,7 @@ class SyncEpisodesWithSource(
val episodeUpdates = toChange.map { it.toEpisodeUpdate() } val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
updateEpisode.awaitAll(episodeUpdates) updateEpisode.awaitAll(episodeUpdates)
} }
val newChapters = episodeRepository.getEpisodeByAnimeId(anime.id) updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
updateAnime.awaitUpdateFetchInterval(anime, newChapters, zoneDateTime, fetchRange)
// Set this anime as updated since episodes were changed // Set this anime as updated since episodes were changed
// Note that last_update actually represents last time the episode list changed at all // Note that last_update actually represents last time the episode list changed at all

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.domain.entries.anime.interactor.MAX_GRACE_PERIOD
import tachiyomi.presentation.core.components.WheelTextPicker import tachiyomi.presentation.core.components.WheelTextPicker
@Composable @Composable
@ -58,7 +57,7 @@ fun SetIntervalDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onValueChanged: (Int) -> Unit, onValueChanged: (Int) -> Unit,
) { ) {
var intervalValue by rememberSaveable { mutableIntStateOf(interval) } var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -69,7 +68,7 @@ fun SetIntervalDialog(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
val size = DpSize(width = maxWidth / 2, height = 128.dp) val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..MAX_GRACE_PERIOD).map { val items = (0..28).map {
if (it == 0) { if (it == 0) {
stringResource(R.string.label_default) stringResource(R.string.label_default)
} else { } else {
@ -79,8 +78,8 @@ fun SetIntervalDialog(
WheelTextPicker( WheelTextPicker(
size = size, size = size,
items = items, items = items,
startIndex = intervalValue, startIndex = selectedInterval,
onSelectionChanged = { intervalValue = it }, onSelectionChanged = { selectedInterval = it },
) )
} }
}, },
@ -91,7 +90,7 @@ fun SetIntervalDialog(
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
onValueChanged(intervalValue) onValueChanged(selectedInterval)
onDismissRequest() onDismissRequest()
},) { },) {
Text(text = stringResource(R.string.action_ok)) Text(text = stringResource(R.string.action_ok))

View file

@ -74,7 +74,6 @@ import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem
import eu.kanade.tachiyomi.ui.entries.anime.FetchAnimeInterval
import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -98,7 +97,7 @@ import java.util.concurrent.TimeUnit
fun AnimeScreen( fun AnimeScreen(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
fetchInterval: FetchAnimeInterval?, fetchInterval: Int?,
dateFormat: DateFormat, dateFormat: DateFormat,
isTabletUi: Boolean, isTabletUi: Boolean,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
@ -247,7 +246,7 @@ private fun AnimeScreenSmallImpl(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateFormat: DateFormat, dateFormat: DateFormat,
fetchInterval: FetchAnimeInterval?, fetchInterval: Int?,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
showNextEpisodeAirTime: Boolean, showNextEpisodeAirTime: Boolean,
@ -518,7 +517,7 @@ fun AnimeScreenLargeImpl(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateFormat: DateFormat, dateFormat: DateFormat,
fetchInterval: FetchAnimeInterval?, fetchInterval: Int?,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
showNextEpisodeAirTime: Boolean, showNextEpisodeAirTime: Boolean,

View file

@ -80,13 +80,13 @@ import eu.kanade.presentation.entries.DotSeparatorText
import eu.kanade.presentation.entries.ItemCover import eu.kanade.presentation.entries.ItemCover
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.ui.entries.anime.FetchAnimeInterval
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@ -168,7 +168,7 @@ fun AnimeActionRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
favorite: Boolean, favorite: Boolean,
trackingCount: Int, trackingCount: Int,
fetchInterval: FetchAnimeInterval?, fetchInterval: Int?,
isUserIntervalMode: Boolean, isUserIntervalMode: Boolean,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
@ -192,14 +192,8 @@ fun AnimeActionRow(
onLongClick = onEditCategory, onLongClick = onEditCategory,
) )
if (onEditIntervalClicked != null && fetchInterval != null) { if (onEditIntervalClicked != null && fetchInterval != null) {
val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays)
AnimeActionButton( AnimeActionButton(
title = title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
if (intervalPair.first == intervalPair.second) {
pluralStringResource(id = R.plurals.day, count = intervalPair.second, intervalPair.second)
} else {
pluralStringResource(id = R.plurals.range_interval_day, count = intervalPair.second, intervalPair.first, intervalPair.second)
},
icon = Icons.Default.HourglassEmpty, icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onEditIntervalClicked, onClick = onEditIntervalClicked,

View file

@ -68,7 +68,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem
import eu.kanade.tachiyomi.ui.entries.manga.FetchMangaInterval
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
import eu.kanade.tachiyomi.util.lang.toRelativeString import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
@ -91,7 +90,7 @@ import java.util.Date
fun MangaScreen( fun MangaScreen(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
fetchInterval: FetchMangaInterval?, fetchInterval: Int?,
dateFormat: DateFormat, dateFormat: DateFormat,
isTabletUi: Boolean, isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@ -230,7 +229,7 @@ private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateFormat: DateFormat, dateFormat: DateFormat,
fetchInterval: FetchMangaInterval?, fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
@ -466,7 +465,7 @@ fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateFormat: DateFormat, dateFormat: DateFormat,
fetchInterval: FetchMangaInterval?, fetchInterval: Int?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,

View file

@ -80,13 +80,13 @@ import eu.kanade.presentation.entries.DotSeparatorText
import eu.kanade.presentation.entries.ItemCover import eu.kanade.presentation.entries.ItemCover
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.entries.manga.FetchMangaInterval
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.presentation.core.components.material.TextButton import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.clickableNoIndication
import tachiyomi.presentation.core.util.secondaryItemAlpha import tachiyomi.presentation.core.util.secondaryItemAlpha
import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@ -168,7 +168,7 @@ fun MangaActionRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
favorite: Boolean, favorite: Boolean,
trackingCount: Int, trackingCount: Int,
fetchInterval: FetchMangaInterval?, fetchInterval: Int?,
isUserIntervalMode: Boolean, isUserIntervalMode: Boolean,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
@ -192,14 +192,8 @@ fun MangaActionRow(
onLongClick = onEditCategory, onLongClick = onEditCategory,
) )
if (onEditIntervalClicked != null && fetchInterval != null) { if (onEditIntervalClicked != null && fetchInterval != null) {
val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays)
MangaActionButton( MangaActionButton(
title = title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
if (intervalPair.first == intervalPair.second) {
pluralStringResource(id = R.plurals.day, count = intervalPair.second, intervalPair.second)
} else {
pluralStringResource(id = R.plurals.range_interval_day, count = intervalPair.second, intervalPair.first, intervalPair.second)
},
icon = Icons.Default.HourglassEmpty, icon = Icons.Default.HourglassEmpty,
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
onClick = onEditIntervalClicked, onClick = onEditIntervalClicked,

View file

@ -1,33 +1,18 @@
package eu.kanade.presentation.more.settings.screen package eu.kanade.presentation.more.settings.screen
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMap
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
@ -52,7 +37,6 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_U
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
import tachiyomi.presentation.core.components.WheelTextPicker
import tachiyomi.presentation.core.util.collectAsState import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -181,13 +165,10 @@ object SettingsLibraryScreen : SearchableSettings {
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval() val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState() val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
val libraryUpdateDeviceRestrictionPref = libraryPreferences.libraryUpdateDeviceRestriction()
val libraryUpdateMangaRestrictionPref = libraryPreferences.libraryUpdateItemRestriction()
val animelibUpdateCategoriesPref = libraryPreferences.animeLibraryUpdateCategories() val animelibUpdateCategoriesPref = libraryPreferences.animeLibraryUpdateCategories()
val animelibUpdateCategoriesExcludePref = val animelibUpdateCategoriesExcludePref =
libraryPreferences.animeLibraryUpdateCategoriesExclude() libraryPreferences.animeLibraryUpdateCategoriesExclude()
val libraryUpdateAnimeRestriction by libraryUpdateMangaRestrictionPref.collectAsState()
val includedAnime by animelibUpdateCategoriesPref.collectAsState() val includedAnime by animelibUpdateCategoriesPref.collectAsState()
val excludedAnime by animelibUpdateCategoriesExcludePref.collectAsState() val excludedAnime by animelibUpdateCategoriesExcludePref.collectAsState()
@ -211,27 +192,10 @@ object SettingsLibraryScreen : SearchableSettings {
}, },
) )
} }
val leadAnimeRange by libraryPreferences.leadingAnimeExpectedDays().collectAsState()
val followAnimeRange by libraryPreferences.followingAnimeExpectedDays().collectAsState()
var showFetchAnimeRangesDialog by rememberSaveable { mutableStateOf(false) }
if (showFetchAnimeRangesDialog) {
LibraryExpectedRangeDialog(
initialLead = leadAnimeRange,
initialFollow = followAnimeRange,
onDismissRequest = { showFetchAnimeRangesDialog = false },
onValueChanged = { leadValue, followValue ->
libraryPreferences.leadingAnimeExpectedDays().set(leadValue)
libraryPreferences.followingAnimeExpectedDays().set(followValue)
showFetchAnimeRangesDialog = false
},
)
}
val libraryUpdateCategoriesPref = libraryPreferences.mangaLibraryUpdateCategories() val libraryUpdateCategoriesPref = libraryPreferences.mangaLibraryUpdateCategories()
val libraryUpdateCategoriesExcludePref = val libraryUpdateCategoriesExcludePref =
libraryPreferences.mangaLibraryUpdateCategoriesExclude() libraryPreferences.mangaLibraryUpdateCategoriesExclude()
val libraryUpdateMangaRestriction by libraryUpdateMangaRestrictionPref.collectAsState()
val includedManga by libraryUpdateCategoriesPref.collectAsState() val includedManga by libraryUpdateCategoriesPref.collectAsState()
val excludedManga by libraryUpdateCategoriesExcludePref.collectAsState() val excludedManga by libraryUpdateCategoriesExcludePref.collectAsState()
@ -255,25 +219,10 @@ object SettingsLibraryScreen : SearchableSettings {
}, },
) )
} }
val leadMangaRange by libraryPreferences.leadingMangaExpectedDays().collectAsState()
val followMangaRange by libraryPreferences.followingMangaExpectedDays().collectAsState()
var showFetchMangaRangesDialog by rememberSaveable { mutableStateOf(false) }
if (showFetchMangaRangesDialog) {
LibraryExpectedRangeDialog(
initialLead = leadMangaRange,
initialFollow = followMangaRange,
onDismissRequest = { showFetchMangaRangesDialog = false },
onValueChanged = { leadValue, followValue ->
libraryPreferences.leadingMangaExpectedDays().set(leadValue)
libraryPreferences.followingMangaExpectedDays().set(followValue)
showFetchMangaRangesDialog = false
},
)
}
return Preference.PreferenceGroup( return Preference.PreferenceGroup(
title = stringResource(R.string.pref_category_library_update), title = stringResource(R.string.pref_category_library_update),
preferenceItems = listOfNotNull( preferenceItems = listOf(
Preference.PreferenceItem.ListPreference( Preference.PreferenceItem.ListPreference(
pref = libraryUpdateIntervalPref, pref = libraryUpdateIntervalPref,
title = stringResource(R.string.pref_library_update_interval), title = stringResource(R.string.pref_library_update_interval),
@ -292,7 +241,7 @@ object SettingsLibraryScreen : SearchableSettings {
}, },
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryUpdateDeviceRestrictionPref, pref = libraryPreferences.libraryUpdateDeviceRestriction(),
enabled = libraryUpdateInterval > 0, enabled = libraryUpdateInterval > 0,
title = stringResource(R.string.pref_library_update_restriction), title = stringResource(R.string.pref_library_update_restriction),
subtitle = stringResource(R.string.restrictions), subtitle = stringResource(R.string.restrictions),
@ -341,7 +290,7 @@ object SettingsLibraryScreen : SearchableSettings {
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary), subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
), ),
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryUpdateMangaRestrictionPref, pref = libraryPreferences.libraryUpdateItemRestriction(),
title = stringResource(R.string.pref_library_update_manga_restriction), title = stringResource(R.string.pref_library_update_manga_restriction),
entries = mapOf( entries = mapOf(
ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read), ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
@ -350,29 +299,6 @@ object SettingsLibraryScreen : SearchableSettings {
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period), ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
), ),
), ),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_update_release_grace_period),
subtitle = listOf(
pluralStringResource(R.plurals.pref_update_release_leading_days, leadMangaRange, leadMangaRange),
pluralStringResource(R.plurals.pref_update_release_following_days, followMangaRange, followMangaRange),
).joinToString(),
onClick = { showFetchMangaRangesDialog = true },
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
Preference.PreferenceItem.InfoPreference(
title = stringResource(R.string.pref_update_release_grace_period_info),
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_update_anime_release_grace_period),
subtitle = listOf(
pluralStringResource(R.plurals.pref_update_release_leading_days, leadAnimeRange, leadAnimeRange),
pluralStringResource(R.plurals.pref_update_release_following_days, followAnimeRange, followAnimeRange),
).joinToString(),
onClick = { showFetchAnimeRangesDialog = true },
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction },
Preference.PreferenceItem.InfoPreference(
title = stringResource(R.string.pref_update_release_grace_period_info),
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction },
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.newShowUpdatesCount(), pref = libraryPreferences.newShowUpdatesCount(),
title = stringResource(R.string.pref_library_update_show_tab_badge), title = stringResource(R.string.pref_library_update_show_tab_badge),
@ -442,79 +368,4 @@ object SettingsLibraryScreen : SearchableSettings {
), ),
) )
} }
@Composable
private fun LibraryExpectedRangeDialog(
initialLead: Int,
initialFollow: Int,
onDismissRequest: () -> Unit,
onValueChanged: (portrait: Int, landscape: Int) -> Unit,
) {
var leadValue by rememberSaveable { mutableIntStateOf(initialLead) }
var followValue by rememberSaveable { mutableIntStateOf(initialFollow) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_update_release_grace_period)) },
text = {
Column {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.weight(1f),
text = pluralStringResource(R.plurals.pref_update_release_leading_days, leadValue, leadValue),
textAlign = TextAlign.Center,
maxLines = 1,
style = MaterialTheme.typography.labelMedium,
)
Text(
modifier = Modifier.weight(1f),
text = pluralStringResource(R.plurals.pref_update_release_following_days, followValue, followValue),
textAlign = TextAlign.Center,
maxLines = 1,
style = MaterialTheme.typography.labelMedium,
)
}
}
BoxWithConstraints(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..28).map(Int::toString)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
WheelTextPicker(
size = size,
items = items,
startIndex = leadValue,
onSelectionChanged = {
leadValue = it
},
)
WheelTextPicker(
size = size,
items = items,
startIndex = followValue,
onSelectionChanged = {
followValue = it
},
)
}
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(android.R.string.cancel))
}
},
confirmButton = {
TextButton(onClick = { onValueChanged(leadValue, followValue) }) {
Text(text = stringResource(R.string.action_ok))
}
},
)
}
} }

View file

@ -0,0 +1,76 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable
fun BottomReaderBar(
readingMode: ReadingModeType,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
onClickOrientationMode: () -> Unit,
cropEnabled: Boolean,
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
) {
// Match with toolbar background color set in ReaderActivity
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
Row(
modifier = Modifier
.fillMaxWidth()
.background(backgroundColor)
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(onClick = onClickReadingMode) {
Icon(
painter = painterResource(readingMode.iconRes),
contentDescription = stringResource(R.string.viewer),
)
}
IconButton(onClick = onClickCropBorder) {
Icon(
painter = painterResource(if (cropEnabled) R.drawable.ic_crop_24dp else R.drawable.ic_crop_off_24dp),
contentDescription = stringResource(R.string.pref_crop_borders),
)
}
IconButton(onClick = onClickOrientationMode) {
Icon(
painter = painterResource(orientationMode.iconRes),
contentDescription = stringResource(R.string.pref_rotation_type),
)
}
IconButton(onClick = onClickSettings) {
Icon(
imageVector = Icons.Outlined.Settings,
contentDescription = stringResource(R.string.action_settings),
)
}
}
}

View file

@ -53,6 +53,15 @@ fun ChapterNavigator(
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
// Match with toolbar background color set in ReaderActivity
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val buttonColor = IconButtonDefaults.filledIconButtonColors(
containerColor = backgroundColor,
disabledContainerColor = backgroundColor,
)
// We explicitly handle direction based on the reader viewer rather than the system direction // We explicitly handle direction based on the reader viewer rather than the system direction
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
Row( Row(
@ -61,14 +70,6 @@ fun ChapterNavigator(
.padding(horizontal = horizontalPadding), .padding(horizontal = horizontalPadding),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
// Match with toolbar background color set in ReaderActivity
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val buttonColor = IconButtonDefaults.filledIconButtonColors(
containerColor = backgroundColor,
disabledContainerColor = backgroundColor,
)
FilledIconButton( FilledIconButton(
enabled = if (isRtl) enabledNext else enabledPrevious, enabled = if (isRtl) enabledNext else enabledPrevious,
onClick = if (isRtl) onNextChapter else onPreviousChapter, onClick = if (isRtl) onNextChapter else onPreviousChapter,

View file

@ -0,0 +1,56 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
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.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.entries.manga.model.orientationType
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.material.padding
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
@Composable
fun OrientationModeSelectDialog(
onDismissRequest: () -> Unit,
screenModel: ReaderSettingsScreenModel,
onChange: (Int) -> Unit,
) {
val manga by screenModel.mangaFlow.collectAsState()
val orientationType = remember(manga) { OrientationType.fromPreference(manga?.orientationType?.toInt()) }
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
SettingsChipRow(R.string.rotation_type) {
orientationTypeOptions.map { (stringRes, it) ->
FilterChip(
selected = it == orientationType,
onClick = {
screenModel.onChangeOrientation(it)
onChange(stringRes)
},
label = { Text(stringResource(stringRes)) },
)
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.reader package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row

View file

@ -0,0 +1,56 @@
package eu.kanade.presentation.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
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.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.domain.entries.manga.model.readingModeType
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.material.padding
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
@Composable
fun ReadingModeSelectDialog(
onDismissRequest: () -> Unit,
screenModel: ReaderSettingsScreenModel,
onChange: (Int) -> Unit,
) {
val manga by screenModel.mangaFlow.collectAsState()
val readingMode = remember(manga) { ReadingModeType.fromPreference(manga?.readingModeType?.toInt()) }
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
SettingsChipRow(R.string.pref_category_reading_mode) {
readingModeOptions.map { (stringRes, it) ->
FilterChip(
selected = it == readingMode,
onClick = {
screenModel.onChangeReadingMode(it)
onChange(stringRes)
},
label = { Text(stringResource(stringRes)) },
)
}
}
}
}
}

View file

@ -42,11 +42,12 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
pref = screenModel.preferences.fullscreen(), pref = screenModel.preferences.fullscreen(),
) )
// TODO: hide if there's no cutout if (screenModel.hasDisplayCutout) {
CheckboxItem( CheckboxItem(
label = stringResource(R.string.pref_cutout_short), label = stringResource(R.string.pref_cutout_short),
pref = screenModel.preferences.cutoutShort(), pref = screenModel.preferences.cutoutShort(),
) )
}
CheckboxItem( CheckboxItem(
label = stringResource(R.string.pref_keep_screen_on), label = stringResource(R.string.pref_keep_screen_on),

View file

@ -60,9 +60,9 @@ class BackupRestorer(
private val episodeRepository: EpisodeRepository = Injekt.get() private val episodeRepository: EpisodeRepository = Injekt.get()
private val setAnimeFetchInterval: SetAnimeFetchInterval = Injekt.get() private val setAnimeFetchInterval: SetAnimeFetchInterval = Injekt.get()
private var zonedDateTime = ZonedDateTime.now() private var now = ZonedDateTime.now()
private var currentMangaFetchInterval = setMangaFetchInterval.getCurrent(zonedDateTime) private var currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
private var currentAnimeFetchInterval = setAnimeFetchInterval.getCurrent(zonedDateTime) private var currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
private var backupManager = BackupManager(context) private var backupManager = BackupManager(context)
@ -140,9 +140,9 @@ class BackupRestorer(
val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name } animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
zonedDateTime = ZonedDateTime.now() now = ZonedDateTime.now()
currentMangaFetchInterval = setMangaFetchInterval.getCurrent(zonedDateTime) currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
currentAnimeFetchInterval = setAnimeFetchInterval.getCurrent(zonedDateTime) currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
return coroutineScope { return coroutineScope {
// Restore individual manga // Restore individual manga
@ -216,8 +216,7 @@ class BackupRestorer(
// Fetch rest of manga information // Fetch rest of manga information
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories) restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
} }
val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id) updateManga.awaitUpdateFetchInterval(restoredManga, now, currentMangaFetchWindow)
updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentMangaFetchInterval)
} catch (e: Exception) { } catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString() val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
@ -291,8 +290,7 @@ class BackupRestorer(
// Fetch rest of anime information // Fetch rest of anime information
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories) restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
} }
val updatedEpisodes = episodeRepository.getEpisodeByAnimeId(restoredAnime.id) updateAnime.awaitUpdateFetchInterval(restoredAnime, now, currentAnimeFetchWindow)
updateAnime.awaitUpdateFetchInterval(restoredAnime, updatedEpisodes, zonedDateTime, currentAnimeFetchInterval)
} catch (e: Exception) { } catch (e: Exception) {
val sourceName = sourceMapping[anime.source] ?: anime.source.toString() val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}") errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}")

View file

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.backup.models package eu.kanade.tachiyomi.data.backup.models
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime

View file

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.backup.models package eu.kanade.tachiyomi.data.backup.models
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber

View file

@ -30,7 +30,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewEpisodes import eu.kanade.tachiyomi.util.shouldDownloadNewEpisodes
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
@ -230,10 +230,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>() val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
val hasDownloads = AtomicBoolean(false) val hasDownloads = AtomicBoolean(false)
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get() val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
val fetchWindow = setAnimeFetchInterval.getWindow(ZonedDateTime.now())
val now = ZonedDateTime.now()
val fetchInterval = setAnimeFetchInterval.getCurrent(now)
val higherLimit = fetchInterval.second
coroutineScope { coroutineScope {
animeToUpdate.groupBy { it.anime.source }.values animeToUpdate.groupBy { it.anime.source }.values
@ -255,8 +252,8 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
anime, anime,
) { ) {
when { when {
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > higherLimit -> anime.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_in_release_period)) skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_always_update))
ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED -> ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED ->
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed)) skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed))
@ -267,12 +264,12 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
ENTRY_NON_VIEWED in restrictions && libraryAnime.totalEpisodes > 0L && !libraryAnime.hasStarted -> ENTRY_NON_VIEWED in restrictions && libraryAnime.totalEpisodes > 0L && !libraryAnime.hasStarted ->
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_started)) skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_started))
anime.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > fetchWindow.second ->
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_always_update)) skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_in_release_period))
else -> { else -> {
try { try {
val newEpisodes = updateAnime(anime, now, fetchInterval) val newEpisodes = updateAnime(anime, fetchWindow)
.sortedByDescending { it.sourceOrder } .sortedByDescending { it.sourceOrder }
if (newEpisodes.isNotEmpty()) { if (newEpisodes.isNotEmpty()) {
@ -328,6 +325,13 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
) )
} }
if (skippedUpdates.isNotEmpty()) { if (skippedUpdates.isNotEmpty()) {
// TODO: surface skipped reasons to user
logcat {
skippedUpdates
.groupBy { it.second }
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
.joinToString()
}
notifier.showUpdateSkippedNotification(skippedUpdates.size) notifier.showUpdateSkippedNotification(skippedUpdates.size)
} }
} }
@ -344,7 +348,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
* @param anime the anime to update. * @param anime the anime to update.
* @return a pair of the inserted and removed episodes. * @return a pair of the inserted and removed episodes.
*/ */
private suspend fun updateAnime(anime: Anime, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Episode> { private suspend fun updateAnime(anime: Anime, fetchWindow: Pair<Long, Long>): List<Episode> {
val source = sourceManager.getOrStub(anime.source) val source = sourceManager.getOrStub(anime.source)
// Update anime metadata if needed // Update anime metadata if needed
@ -359,7 +363,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
// to get latest data so it doesn't get overwritten later on // to get latest data so it doesn't get overwritten later on
val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList() val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList()
return syncEpisodesWithSource.await(episodes, dbAnime, source, false, zoneDateTime, fetchRange) return syncEpisodesWithSource.await(episodes, dbAnime, source, false, fetchWindow)
} }
private suspend fun updateCovers() { private suspend fun updateCovers() {

View file

@ -28,9 +28,9 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.track.EnhancedMangaTrackService import eu.kanade.tachiyomi.data.track.EnhancedMangaTrackService
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.model.UpdateStrategy
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
@ -230,10 +230,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>() val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
val hasDownloads = AtomicBoolean(false) val hasDownloads = AtomicBoolean(false)
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get() val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
val fetchWindow = setMangaFetchInterval.getWindow(ZonedDateTime.now())
val now = ZonedDateTime.now()
val fetchInterval = setMangaFetchInterval.getCurrent(now)
val higherLimit = fetchInterval.second
coroutineScope { coroutineScope {
mangaToUpdate.groupBy { it.manga.source }.values mangaToUpdate.groupBy { it.manga.source }.values
@ -255,8 +252,8 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
manga, manga,
) { ) {
when { when {
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit -> manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period)) skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED -> ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed)) skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
@ -267,12 +264,12 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
ENTRY_NON_VIEWED in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted -> ENTRY_NON_VIEWED in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started)) skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE -> ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > fetchWindow.second ->
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update)) skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
else -> { else -> {
try { try {
val newChapters = updateManga(manga, now, fetchInterval) val newChapters = updateManga(manga, fetchWindow)
.sortedByDescending { it.sourceOrder } .sortedByDescending { it.sourceOrder }
if (newChapters.isNotEmpty()) { if (newChapters.isNotEmpty()) {
@ -328,6 +325,13 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
) )
} }
if (skippedUpdates.isNotEmpty()) { if (skippedUpdates.isNotEmpty()) {
// TODO: surface skipped reasons to user
logcat {
skippedUpdates
.groupBy { it.second }
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
.joinToString()
}
notifier.showUpdateSkippedNotification(skippedUpdates.size) notifier.showUpdateSkippedNotification(skippedUpdates.size)
} }
} }
@ -344,7 +348,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
* @param manga the manga to update. * @param manga the manga to update.
* @return a pair of the inserted and removed chapters. * @return a pair of the inserted and removed chapters.
*/ */
private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> { private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> {
val source = sourceManager.getOrStub(manga.source) val source = sourceManager.getOrStub(manga.source)
// Update manga metadata if needed // Update manga metadata if needed
@ -359,7 +363,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
// to get latest data so it doesn't get overwritten later on // to get latest data so it doesn't get overwritten later on
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList() val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange) return syncChaptersWithSource.await(chapters, dbManga, source, false, fetchWindow)
} }
private suspend fun updateCovers() { private suspend fun updateCovers() {

View file

@ -29,7 +29,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Calendar import java.util.Calendar
import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.minutes
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
@ -37,7 +37,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val authClient = client.newBuilder() private val authClient = client.newBuilder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
.rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES) .rateLimit(permits = 85, period = 1.minutes)
.build() .build()
suspend fun addLibManga(track: MangaTrack): MangaTrack { suspend fun addLibManga(track: MangaTrack): MangaTrack {

View file

@ -5,6 +5,7 @@ import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.notify import eu.kanade.tachiyomi.util.system.notify
class ExtensionUpdateNotifier(private val context: Context) { class ExtensionUpdateNotifier(private val context: Context) {
@ -29,4 +30,8 @@ class ExtensionUpdateNotifier(private val context: Context) {
setAutoCancel(true) setAutoCancel(true)
} }
} }
fun dismiss() {
context.cancelNotification(Notifications.ID_UPDATES_TO_EXTS)
}
} }

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
@ -195,7 +196,7 @@ class AnimeExtensionManager(
} }
/** /**
* Returns an observable of the installation process for the given anime extension. It will complete * Returns a flow of the installation process for the given anime extension. It will complete
* once the anime extension is installed or throws an error. The process will be canceled if * once the anime extension is installed or throws an error. The process will be canceled if
* unsubscribed before its completion. * unsubscribed before its completion.
* *
@ -206,7 +207,7 @@ class AnimeExtensionManager(
} }
/** /**
* Returns an observable of the installation process for the given anime extension. It will complete * Returns a flow of the installation process for the given anime extension. It will complete
* once the anime extension is updated or throws an error. The process will be canceled if * once the anime extension is updated or throws an error. The process will be canceled if
* unsubscribed before its completion. * unsubscribed before its completion.
* *
@ -356,6 +357,10 @@ class AnimeExtensionManager(
} }
private fun updatePendingUpdatesCount() { private fun updatePendingUpdatesCount() {
preferences.animeExtensionUpdatesCount().set(_installedAnimeExtensionsFlow.value.count { it.hasUpdate }) val pendingUpdateCount = _installedAnimeExtensionsFlow.value.count { it.hasUpdate }
preferences.animeExtensionUpdatesCount().set(pendingUpdateCount)
if (pendingUpdateCount == 0) {
ExtensionUpdateNotifier(context).dismiss()
}
} }
} }

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.InstallStep import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
@ -195,7 +196,7 @@ class MangaExtensionManager(
} }
/** /**
* Returns an observable of the installation process for the given extension. It will complete * Returns a flow of the installation process for the given extension. It will complete
* once the extension is installed or throws an error. The process will be canceled if * once the extension is installed or throws an error. The process will be canceled if
* unsubscribed before its completion. * unsubscribed before its completion.
* *
@ -206,7 +207,7 @@ class MangaExtensionManager(
} }
/** /**
* Returns an observable of the installation process for the given extension. It will complete * Returns a flow of the installation process for the given extension. It will complete
* once the extension is updated or throws an error. The process will be canceled if * once the extension is updated or throws an error. The process will be canceled if
* unsubscribed before its completion. * unsubscribed before its completion.
* *
@ -356,6 +357,10 @@ class MangaExtensionManager(
} }
private fun updatePendingUpdatesCount() { private fun updatePendingUpdatesCount() {
preferences.mangaExtensionUpdatesCount().set(_installedExtensionsFlow.value.count { it.hasUpdate }) val pendingUpdateCount = _installedExtensionsFlow.value.count { it.hasUpdate }
preferences.mangaExtensionUpdatesCount().set(pendingUpdateCount)
if (pendingUpdateCount == 0) {
ExtensionUpdateNotifier(context).dismiss()
}
} }
} }

View file

@ -157,14 +157,14 @@ class AnimeExtensionsScreenModel(
extensionManager.cancelInstallUpdateExtension(extension) extensionManager.cancelInstallUpdateExtension(extension)
} }
private fun removeDownloadState(extension: AnimeExtension) {
_currentDownloads.update { it - extension.pkgName }
}
private fun addDownloadState(extension: AnimeExtension, installStep: InstallStep) { private fun addDownloadState(extension: AnimeExtension, installStep: InstallStep) {
_currentDownloads.update { it + Pair(extension.pkgName, installStep) } _currentDownloads.update { it + Pair(extension.pkgName, installStep) }
} }
private fun removeDownloadState(extension: AnimeExtension) {
_currentDownloads.update { it - extension.pkgName }
}
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: AnimeExtension) = private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: AnimeExtension) =
this this
.onEach { installStep -> addDownloadState(extension, installStep) } .onEach { installStep -> addDownloadState(extension, installStep) }

View file

@ -4,28 +4,36 @@ import eu.kanade.domain.entries.anime.model.hasCustomCover
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
data class AnimeMigrationFlag(
val flag: Int,
val isDefaultSelected: Boolean,
val titleId: Int,
) {
companion object {
fun create(flag: Int, defaultSelectionMap: Int, titleId: Int): AnimeMigrationFlag {
return AnimeMigrationFlag(
flag = flag,
isDefaultSelected = defaultSelectionMap and flag != 0,
titleId = titleId,
)
}
}
}
object AnimeMigrationFlags { object AnimeMigrationFlags {
private const val EPISODES = 0b00001 private const val EPISODES = 0b00001
private const val CATEGORIES = 0b00010 private const val CATEGORIES = 0b00010
private const val TRACK = 0b00100
private const val CUSTOM_COVER = 0b01000 private const val CUSTOM_COVER = 0b01000
private const val DELETE_DOWNLOADED = 0b10000 private const val DELETE_DOWNLOADED = 0b10000
private val coverCache: AnimeCoverCache by injectLazy() private val coverCache: AnimeCoverCache by injectLazy()
private val getTracks: GetAnimeTracks = Injekt.get()
private val downloadCache: AnimeDownloadCache by injectLazy() private val downloadCache: AnimeDownloadCache by injectLazy()
val flags get() = arrayOf(EPISODES, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED)
private var enableFlags = emptyList<Int>().toMutableList()
fun hasEpisodes(value: Int): Boolean { fun hasEpisodes(value: Int): Boolean {
return value and EPISODES != 0 return value and EPISODES != 0
} }
@ -34,10 +42,6 @@ object AnimeMigrationFlags {
return value and CATEGORIES != 0 return value and CATEGORIES != 0
} }
fun hasTracks(value: Int): Boolean {
return value and TRACK != 0
}
fun hasCustomCover(value: Int): Boolean { fun hasCustomCover(value: Int): Boolean {
return value and CUSTOM_COVER != 0 return value and CUSTOM_COVER != 0
} }
@ -46,35 +50,32 @@ object AnimeMigrationFlags {
return value and DELETE_DOWNLOADED != 0 return value and DELETE_DOWNLOADED != 0
} }
fun getEnabledFlagsPositions(value: Int): List<Int> { /** Returns information about applicable flags with default selections. */
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null } fun getFlags(anime: Anime?, defaultSelectedBitMap: Int): List<AnimeMigrationFlag> {
} val flags = mutableListOf<AnimeMigrationFlag>()
flags += AnimeMigrationFlag.create(EPISODES, defaultSelectedBitMap, R.string.chapters)
flags += AnimeMigrationFlag.create(CATEGORIES, defaultSelectedBitMap, R.string.categories)
fun getFlagsFromPositions(positions: Array<Int>): Int {
val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] }
enableFlags.clear()
return fold
}
fun titles(anime: Anime?): Array<Int> {
enableFlags.add(EPISODES)
enableFlags.add(CATEGORIES)
val titles = arrayOf(R.string.episodes, R.string.anime_categories).toMutableList()
if (anime != null) { if (anime != null) {
if (runBlocking { getTracks.await(anime.id) }.isNotEmpty()) {
titles.add(R.string.track)
enableFlags.add(TRACK)
}
if (anime.hasCustomCover(coverCache)) { if (anime.hasCustomCover(coverCache)) {
titles.add(R.string.custom_cover) flags += AnimeMigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, R.string.custom_cover)
enableFlags.add(CUSTOM_COVER)
} }
if (downloadCache.getDownloadCount(anime) > 0) { if (downloadCache.getDownloadCount(anime) > 0) {
titles.add(R.string.delete_downloaded) flags += AnimeMigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, R.string.delete_downloaded)
enableFlags.add(DELETE_DOWNLOADED)
} }
} }
return titles.toTypedArray() return flags
}
/** Returns a bit map of selected flags. */
fun getSelectedFlagsBitMap(
selectedFlags: List<Boolean>,
flags: List<AnimeMigrationFlag>,
): Int {
return selectedFlags
.zip(flags)
.filter { (isSelected, _) -> isSelected }
.map { (_, flag) -> flag.flag }
.reduceOrNull { acc, mask -> acc or mask } ?: 0
} }
} }

View file

@ -19,15 +19,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.domain.entries.anime.interactor.UpdateAnime import eu.kanade.domain.entries.anime.interactor.UpdateAnime
import eu.kanade.domain.entries.anime.model.hasCustomCover import eu.kanade.domain.entries.anime.model.hasCustomCover
@ -74,15 +73,8 @@ internal fun MigrateAnimeDialog(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
val activeFlags = remember { AnimeMigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) } val flags = remember { AnimeMigrationFlags.getFlags(oldAnime, screenModel.migrateFlags.get()) }
val items = remember { val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() }
AnimeMigrationFlags.titles(oldAnime)
.map { context.getString(it) }
.toList()
}
val selected = remember {
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
}
if (state.isMigrating) { if (state.isMigrating) {
LoadingScreen( LoadingScreen(
@ -99,18 +91,16 @@ internal fun MigrateAnimeDialog(
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
) { ) {
items.forEachIndexed { index, title -> flags.forEachIndexed { index, flag ->
val onChange: () -> Unit = { val onChange = { selectedFlags[index] = !selectedFlags[index] }
selected[index] = !selected[index]
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onChange), .clickable(onClick = onChange),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Checkbox(checked = selected[index], onCheckedChange = { onChange() }) Checkbox(checked = selectedFlags[index], onCheckedChange = { onChange() })
Text(text = title) Text(text = context.getString(flag.titleId))
} }
} }
} }
@ -133,7 +123,12 @@ internal fun MigrateAnimeDialog(
TextButton( TextButton(
onClick = { onClick = {
scope.launchIO { scope.launchIO {
screenModel.migrateAnime(oldAnime, newAnime, false) screenModel.migrateAnime(
oldAnime,
newAnime,
false,
AnimeMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
)
withUIContext { onPopScreen() } withUIContext { onPopScreen() }
} }
}, },
@ -143,12 +138,13 @@ internal fun MigrateAnimeDialog(
TextButton( TextButton(
onClick = { onClick = {
scope.launchIO { scope.launchIO {
val selectedIndices = mutableListOf<Int>() screenModel.migrateAnime(
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) } oldAnime,
val newValue = newAnime,
AnimeMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray()) true,
screenModel.migrateFlags.set(newValue) AnimeMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
screenModel.migrateAnime(oldAnime, newAnime, true) )
withUIContext { onPopScreen() } withUIContext { onPopScreen() }
} }
}, },
@ -184,7 +180,13 @@ internal class MigrateAnimeDialogScreenModel(
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedAnimeTrackService>() Injekt.get<TrackManager>().services.filterIsInstance<EnhancedAnimeTrackService>()
} }
suspend fun migrateAnime(oldAnime: Anime, newAnime: Anime, replace: Boolean) { suspend fun migrateAnime(
oldAnime: Anime,
newAnime: Anime,
replace: Boolean,
flags: Int,
) {
migrateFlags.set(flags)
val source = sourceManager.get(newAnime.source) ?: return val source = sourceManager.get(newAnime.source) ?: return
val prevSource = sourceManager.get(oldAnime.source) val prevSource = sourceManager.get(oldAnime.source)
@ -200,6 +202,7 @@ internal class MigrateAnimeDialogScreenModel(
newAnime = newAnime, newAnime = newAnime,
sourceEpisodes = episodes, sourceEpisodes = episodes,
replace = replace, replace = replace,
flags = flags,
) )
} catch (_: Throwable) { } catch (_: Throwable) {
// Explicitly stop if an error occurred; the dialog normally gets popped at the end // Explicitly stop if an error occurred; the dialog normally gets popped at the end
@ -215,12 +218,10 @@ internal class MigrateAnimeDialogScreenModel(
newAnime: Anime, newAnime: Anime,
sourceEpisodes: List<SEpisode>, sourceEpisodes: List<SEpisode>,
replace: Boolean, replace: Boolean,
flags: Int,
) { ) {
val flags = migrateFlags.get()
val migrateEpisodes = AnimeMigrationFlags.hasEpisodes(flags) val migrateEpisodes = AnimeMigrationFlags.hasEpisodes(flags)
val migrateCategories = AnimeMigrationFlags.hasCategories(flags) val migrateCategories = AnimeMigrationFlags.hasCategories(flags)
val migrateTracks = AnimeMigrationFlags.hasTracks(flags)
val migrateCustomCover = AnimeMigrationFlags.hasCustomCover(flags) val migrateCustomCover = AnimeMigrationFlags.hasCustomCover(flags)
val deleteDownloaded = AnimeMigrationFlags.hasDeleteDownloaded(flags) val deleteDownloaded = AnimeMigrationFlags.hasDeleteDownloaded(flags)
@ -271,21 +272,20 @@ internal class MigrateAnimeDialogScreenModel(
} }
// Update track // Update track
if (migrateTracks) { getTracks.await(oldAnime.id).mapNotNull { track ->
val tracks = getTracks.await(oldAnime.id).mapNotNull { track -> val updatedTrack = track.copy(animeId = newAnime.id)
val updatedTrack = track.copy(animeId = newAnime.id)
val service = enhancedServices val service = enhancedServices
.firstOrNull { it.isTrackFrom(updatedTrack, oldAnime, oldSource) } .firstOrNull { it.isTrackFrom(updatedTrack, oldAnime, oldSource) }
if (service != null) { if (service != null) {
service.migrateTrack(updatedTrack, newAnime, newSource) service.migrateTrack(updatedTrack, newAnime, newSource)
} else { } else {
updatedTrack updatedTrack
}
} }
insertTrack.awaitAll(tracks)
} }
.takeIf { it.isNotEmpty() }
?.let { insertTrack.awaitAll(it) }
// Delete downloaded // Delete downloaded
if (deleteDownloaded) { if (deleteDownloaded) {

View file

@ -158,14 +158,14 @@ class MangaExtensionsScreenModel(
extensionManager.cancelInstallUpdateExtension(extension) extensionManager.cancelInstallUpdateExtension(extension)
} }
private fun removeDownloadState(extension: MangaExtension) {
_currentDownloads.update { it - extension.pkgName }
}
private fun addDownloadState(extension: MangaExtension, installStep: InstallStep) { private fun addDownloadState(extension: MangaExtension, installStep: InstallStep) {
_currentDownloads.update { it + Pair(extension.pkgName, installStep) } _currentDownloads.update { it + Pair(extension.pkgName, installStep) }
} }
private fun removeDownloadState(extension: MangaExtension) {
_currentDownloads.update { it - extension.pkgName }
}
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: MangaExtension) = private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: MangaExtension) =
this this
.onEach { installStep -> addDownloadState(extension, installStep) } .onEach { installStep -> addDownloadState(extension, installStep) }

View file

@ -4,28 +4,36 @@ import eu.kanade.domain.entries.manga.model.hasCustomCover
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.MangaCoverCache import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
data class MangaMigrationFlag(
val flag: Int,
val isDefaultSelected: Boolean,
val titleId: Int,
) {
companion object {
fun create(flag: Int, defaultSelectionMap: Int, titleId: Int): MangaMigrationFlag {
return MangaMigrationFlag(
flag = flag,
isDefaultSelected = defaultSelectionMap and flag != 0,
titleId = titleId,
)
}
}
}
object MangaMigrationFlags { object MangaMigrationFlags {
private const val CHAPTERS = 0b00001 private const val CHAPTERS = 0b00001
private const val CATEGORIES = 0b00010 private const val CATEGORIES = 0b00010
private const val TRACK = 0b00100
private const val CUSTOM_COVER = 0b01000 private const val CUSTOM_COVER = 0b01000
private const val DELETE_DOWNLOADED = 0b10000 private const val DELETE_DOWNLOADED = 0b10000
private val coverCache: MangaCoverCache by injectLazy() private val coverCache: MangaCoverCache by injectLazy()
private val getTracks: GetMangaTracks = Injekt.get()
private val downloadCache: MangaDownloadCache by injectLazy() private val downloadCache: MangaDownloadCache by injectLazy()
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED)
private var enableFlags = emptyList<Int>().toMutableList()
fun hasChapters(value: Int): Boolean { fun hasChapters(value: Int): Boolean {
return value and CHAPTERS != 0 return value and CHAPTERS != 0
} }
@ -34,10 +42,6 @@ object MangaMigrationFlags {
return value and CATEGORIES != 0 return value and CATEGORIES != 0
} }
fun hasTracks(value: Int): Boolean {
return value and TRACK != 0
}
fun hasCustomCover(value: Int): Boolean { fun hasCustomCover(value: Int): Boolean {
return value and CUSTOM_COVER != 0 return value and CUSTOM_COVER != 0
} }
@ -46,34 +50,32 @@ object MangaMigrationFlags {
return value and DELETE_DOWNLOADED != 0 return value and DELETE_DOWNLOADED != 0
} }
fun getEnabledFlagsPositions(value: Int): List<Int> { /** Returns information about applicable flags with default selections. */
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null } fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List<MangaMigrationFlag> {
} val flags = mutableListOf<MangaMigrationFlag>()
flags += MangaMigrationFlag.create(CHAPTERS, defaultSelectedBitMap, R.string.chapters)
flags += MangaMigrationFlag.create(CATEGORIES, defaultSelectedBitMap, R.string.categories)
fun getFlagsFromPositions(positions: Array<Int>): Int {
val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] }
enableFlags.clear()
return fold
}
fun titles(manga: Manga?): Array<Int> {
enableFlags.add(CHAPTERS)
enableFlags.add(CATEGORIES)
val titles = arrayOf(R.string.chapters, R.string.manga_categories).toMutableList()
if (manga != null) { if (manga != null) {
if (runBlocking { getTracks.await(manga.id) }.isNotEmpty()) {
titles.add(R.string.track)
enableFlags.add(TRACK)
}
if (manga.hasCustomCover(coverCache)) { if (manga.hasCustomCover(coverCache)) {
titles.add(R.string.custom_cover) flags += MangaMigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, R.string.custom_cover)
enableFlags.add(CUSTOM_COVER)
} }
if (downloadCache.getDownloadCount(manga) > 0) { if (downloadCache.getDownloadCount(manga) > 0) {
titles.add(R.string.delete_downloaded) flags += MangaMigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, R.string.delete_downloaded)
enableFlags.add(DELETE_DOWNLOADED)
} }
} }
return titles.toTypedArray() return flags
}
/** Returns a bit map of selected flags. */
fun getSelectedFlagsBitMap(
selectedFlags: List<Boolean>,
flags: List<MangaMigrationFlag>,
): Int {
return selectedFlags
.zip(flags)
.filter { (isSelected, _) -> isSelected }
.map { (_, flag) -> flag.flag }
.reduceOrNull { acc, mask -> acc or mask } ?: 0
} }
} }

View file

@ -19,15 +19,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.domain.entries.manga.interactor.UpdateManga import eu.kanade.domain.entries.manga.interactor.UpdateManga
import eu.kanade.domain.entries.manga.model.hasCustomCover import eu.kanade.domain.entries.manga.model.hasCustomCover
@ -74,15 +73,8 @@ internal fun MigrateMangaDialog(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
val activeFlags = remember { MangaMigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) } val flags = remember { MangaMigrationFlags.getFlags(oldManga, screenModel.migrateFlags.get()) }
val items = remember { val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() }
MangaMigrationFlags.titles(oldManga)
.map { context.getString(it) }
.toList()
}
val selected = remember {
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
}
if (state.isMigrating) { if (state.isMigrating) {
LoadingScreen( LoadingScreen(
@ -99,18 +91,16 @@ internal fun MigrateMangaDialog(
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
) { ) {
items.forEachIndexed { index, title -> flags.forEachIndexed { index, flag ->
val onChange: () -> Unit = { val onChange = { selectedFlags[index] = !selectedFlags[index] }
selected[index] = !selected[index]
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onChange), .clickable(onClick = onChange),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Checkbox(checked = selected[index], onCheckedChange = { onChange() }) Checkbox(checked = selectedFlags[index], onCheckedChange = { onChange() })
Text(text = title) Text(text = context.getString(flag.titleId))
} }
} }
} }
@ -133,7 +123,12 @@ internal fun MigrateMangaDialog(
TextButton( TextButton(
onClick = { onClick = {
scope.launchIO { scope.launchIO {
screenModel.migrateManga(oldManga, newManga, false) screenModel.migrateManga(
oldManga,
newManga,
false,
MangaMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
)
withUIContext { onPopScreen() } withUIContext { onPopScreen() }
} }
}, },
@ -143,12 +138,13 @@ internal fun MigrateMangaDialog(
TextButton( TextButton(
onClick = { onClick = {
scope.launchIO { scope.launchIO {
val selectedIndices = mutableListOf<Int>() screenModel.migrateManga(
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) } oldManga,
val newValue = newManga,
MangaMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray()) true,
screenModel.migrateFlags.set(newValue) MangaMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
screenModel.migrateManga(oldManga, newManga, true) )
withUIContext { onPopScreen() } withUIContext { onPopScreen() }
} }
}, },
@ -184,7 +180,13 @@ internal class MigrateMangaDialogScreenModel(
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedMangaTrackService>() Injekt.get<TrackManager>().services.filterIsInstance<EnhancedMangaTrackService>()
} }
suspend fun migrateManga(oldManga: Manga, newManga: Manga, replace: Boolean) { suspend fun migrateManga(
oldManga: Manga,
newManga: Manga,
replace: Boolean,
flags: Int,
) {
migrateFlags.set(flags)
val source = sourceManager.get(newManga.source) ?: return val source = sourceManager.get(newManga.source) ?: return
val prevSource = sourceManager.get(oldManga.source) val prevSource = sourceManager.get(oldManga.source)
@ -200,6 +202,7 @@ internal class MigrateMangaDialogScreenModel(
newManga = newManga, newManga = newManga,
sourceChapters = chapters, sourceChapters = chapters,
replace = replace, replace = replace,
flags = flags,
) )
} catch (_: Throwable) { } catch (_: Throwable) {
// Explicitly stop if an error occurred; the dialog normally gets popped at the end // Explicitly stop if an error occurred; the dialog normally gets popped at the end
@ -215,12 +218,10 @@ internal class MigrateMangaDialogScreenModel(
newManga: Manga, newManga: Manga,
sourceChapters: List<SChapter>, sourceChapters: List<SChapter>,
replace: Boolean, replace: Boolean,
flags: Int,
) { ) {
val flags = migrateFlags.get()
val migrateChapters = MangaMigrationFlags.hasChapters(flags) val migrateChapters = MangaMigrationFlags.hasChapters(flags)
val migrateCategories = MangaMigrationFlags.hasCategories(flags) val migrateCategories = MangaMigrationFlags.hasCategories(flags)
val migrateTracks = MangaMigrationFlags.hasTracks(flags)
val migrateCustomCover = MangaMigrationFlags.hasCustomCover(flags) val migrateCustomCover = MangaMigrationFlags.hasCustomCover(flags)
val deleteDownloaded = MangaMigrationFlags.hasDeleteDownloaded(flags) val deleteDownloaded = MangaMigrationFlags.hasDeleteDownloaded(flags)
@ -271,21 +272,20 @@ internal class MigrateMangaDialogScreenModel(
} }
// Update track // Update track
if (migrateTracks) { getTracks.await(oldManga.id).mapNotNull { track ->
val tracks = getTracks.await(oldManga.id).mapNotNull { track -> val updatedTrack = track.copy(mangaId = newManga.id)
val updatedTrack = track.copy(mangaId = newManga.id)
val service = enhancedServices val service = enhancedServices
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) } .firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
if (service != null) { if (service != null) {
service.migrateTrack(updatedTrack, newManga, newSource) service.migrateTrack(updatedTrack, newManga, newSource)
} else { } else {
updatedTrack updatedTrack
}
} }
insertTrack.awaitAll(tracks)
} }
.takeIf { it.isNotEmpty() }
?.let { insertTrack.awaitAll(it) }
// Delete downloaded // Delete downloaded
if (deleteDownloaded) { if (deleteDownloaded) {

View file

@ -89,13 +89,6 @@ class AnimeScreen(
val successState = state as AnimeScreenModel.State.Success val successState = state as AnimeScreenModel.State.Success
val isAnimeHttpSource = remember { successState.source is AnimeHttpSource } val isAnimeHttpSource = remember { successState.source is AnimeHttpSource }
val fetchInterval = remember(successState.anime.fetchInterval) {
FetchAnimeInterval(
interval = successState.anime.fetchInterval,
leadDays = screenModel.leadDay,
followDays = screenModel.followDay,
)
}
LaunchedEffect(successState.anime, screenModel.source) { LaunchedEffect(successState.anime, screenModel.source) {
if (isAnimeHttpSource) { if (isAnimeHttpSource) {
@ -113,7 +106,7 @@ class AnimeScreen(
state = successState, state = successState,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
dateFormat = screenModel.dateFormat, dateFormat = screenModel.dateFormat,
fetchInterval = fetchInterval, fetchInterval = successState.anime.fetchInterval,
isTabletUi = isTabletUi(), isTabletUi = isTabletUi(),
episodeSwipeStartAction = screenModel.episodeSwipeStartAction, episodeSwipeStartAction = screenModel.episodeSwipeStartAction,
episodeSwipeEndAction = screenModel.episodeSwipeEndAction, episodeSwipeEndAction = screenModel.episodeSwipeEndAction,
@ -244,7 +237,7 @@ class AnimeScreen(
} }
is AnimeScreenModel.Dialog.SetAnimeFetchInterval -> { is AnimeScreenModel.Dialog.SetAnimeFetchInterval -> {
SetIntervalDialog( SetIntervalDialog(
interval = if (dialog.anime.fetchInterval < 0) -dialog.anime.fetchInterval else 0, interval = dialog.anime.fetchInterval,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onValueChanged = { screenModel.setFetchInterval(dialog.anime, it) }, onValueChanged = { screenModel.setFetchInterval(dialog.anime, it) },
) )

View file

@ -135,8 +135,6 @@ class AnimeScreenModel(
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get() val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
val leadDay = libraryPreferences.leadingAnimeExpectedDays().get()
val followDay = libraryPreferences.followingAnimeExpectedDays().get()
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
private val selectedEpisodeIds: HashSet<Long> = HashSet() private val selectedEpisodeIds: HashSet<Long> = HashSet()
@ -377,20 +375,14 @@ class AnimeScreenModel(
} }
} }
fun setFetchInterval(anime: Anime, newInterval: Int) { fun setFetchInterval(anime: Anime, interval: Int) {
val interval = when (newInterval) {
// reset interval 0 default to trigger recalculation
// only reset if interval is custom, which is negative
0 -> if (anime.fetchInterval < 0) 0 else anime.fetchInterval
else -> -newInterval
}
coroutineScope.launchIO { coroutineScope.launchIO {
updateAnime.awaitUpdateFetchInterval( updateAnime.awaitUpdateFetchInterval(
anime.copy(fetchInterval = interval), // Custom intervals are negative
successState?.episodes?.map { it.episode }.orEmpty(), anime.copy(fetchInterval = -interval),
) )
val newAnime = animeRepository.getAnimeById(animeId) val updatedAnime = animeRepository.getAnimeById(anime.id)
updateSuccessState { it.copy(anime = newAnime) } updateSuccessState { it.copy(anime = updatedAnime) }
} }
} }
@ -1105,10 +1097,3 @@ data class EpisodeItem(
) { ) {
val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED
} }
@Immutable
data class FetchAnimeInterval(
val interval: Int,
val leadDays: Int,
val followDays: Int,
)

View file

@ -84,13 +84,6 @@ class MangaScreen(
val successState = state as MangaScreenModel.State.Success val successState = state as MangaScreenModel.State.Success
val isHttpSource = remember { successState.source is HttpSource } val isHttpSource = remember { successState.source is HttpSource }
val fetchInterval = remember(successState.manga.fetchInterval) {
FetchMangaInterval(
interval = successState.manga.fetchInterval,
leadDays = screenModel.leadDay,
followDays = screenModel.followDay,
)
}
LaunchedEffect(successState.manga, screenModel.source) { LaunchedEffect(successState.manga, screenModel.source) {
if (isHttpSource) { if (isHttpSource) {
@ -108,7 +101,7 @@ class MangaScreen(
state = successState, state = successState,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
dateFormat = screenModel.dateFormat, dateFormat = screenModel.dateFormat,
fetchInterval = fetchInterval, fetchInterval = successState.manga.fetchInterval,
isTabletUi = isTabletUi(), isTabletUi = isTabletUi(),
chapterSwipeStartAction = screenModel.chapterSwipeStartAction, chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
chapterSwipeEndAction = screenModel.chapterSwipeEndAction, chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
@ -226,7 +219,7 @@ class MangaScreen(
} }
is MangaScreenModel.Dialog.SetMangaFetchInterval -> { is MangaScreenModel.Dialog.SetMangaFetchInterval -> {
SetIntervalDialog( SetIntervalDialog(
interval = if (dialog.manga.fetchInterval < 0) -dialog.manga.fetchInterval else 0, interval = dialog.manga.fetchInterval,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) }, onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
) )

View file

@ -131,8 +131,6 @@ class MangaScreenModel(
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope) val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get() val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
val leadDay = libraryPreferences.leadingMangaExpectedDays().get()
val followDay = libraryPreferences.followingMangaExpectedDays().get()
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
private val selectedChapterIds: HashSet<Long> = HashSet() private val selectedChapterIds: HashSet<Long> = HashSet()
@ -374,20 +372,14 @@ class MangaScreenModel(
} }
} }
fun setFetchInterval(manga: Manga, newInterval: Int) { fun setFetchInterval(manga: Manga, interval: Int) {
val interval = when (newInterval) {
// reset interval 0 default to trigger recalculation
// only reset if interval is custom, which is negative
0 -> if (manga.fetchInterval < 0) 0 else manga.fetchInterval
else -> -newInterval
}
coroutineScope.launchIO { coroutineScope.launchIO {
updateManga.awaitUpdateFetchInterval( updateManga.awaitUpdateFetchInterval(
manga.copy(fetchInterval = interval), // Custom intervals are negative
successState?.chapters?.map { it.chapter }.orEmpty(), manga.copy(fetchInterval = -interval),
) )
val newManga = mangaRepository.getMangaById(mangaId) val updatedManga = mangaRepository.getMangaById(manga.id)
updateSuccessState { it.copy(manga = newManga) } updateSuccessState { it.copy(manga = updatedManga) }
} }
} }
@ -1073,10 +1065,3 @@ data class ChapterItem(
) { ) {
val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED
} }
@Immutable
data class FetchMangaInterval(
val interval: Int,
val leadDays: Int,
val followDays: Int,
)

View file

@ -25,6 +25,7 @@ import android.view.animation.AnimationUtils
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -48,9 +49,12 @@ import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.transition.platform.MaterialContainerTransform import com.google.android.material.transition.platform.MaterialContainerTransform
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.entries.manga.model.orientationType import eu.kanade.presentation.reader.BottomReaderBar
import eu.kanade.presentation.reader.ChapterNavigator import eu.kanade.presentation.reader.ChapterNavigator
import eu.kanade.presentation.reader.OrientationModeSelectDialog
import eu.kanade.presentation.reader.PageIndicatorText import eu.kanade.presentation.reader.PageIndicatorText
import eu.kanade.presentation.reader.ReaderPageActionsDialog
import eu.kanade.presentation.reader.ReadingModeSelectDialog
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.Constants import eu.kanade.tachiyomi.core.Constants
@ -78,10 +82,7 @@ import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.copy
import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setComposeContent
import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
@ -93,12 +94,12 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.preference.toggle
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.widget.util.stringResource import tachiyomi.presentation.widget.util.stringResource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -124,7 +125,7 @@ class ReaderActivity : BaseActivity() {
val viewModel by viewModels<ReaderViewModel>() val viewModel by viewModels<ReaderViewModel>()
private var assistUrl: String? = null private var assistUrl: String? = null
val hasCutout by lazy { hasDisplayCutout() } private val hasCutout by lazy { hasDisplayCutout() }
/** /**
* Configuration at reader level, like background color or forced orientation. * Configuration at reader level, like background color or forced orientation.
@ -391,11 +392,12 @@ class ReaderActivity : BaseActivity() {
) )
} }
binding.dialogRoot.setComposeContent { binding.readerMenuBottom.setComposeContent {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val settingsScreenModel = remember { val settingsScreenModel = remember {
ReaderSettingsScreenModel( ReaderSettingsScreenModel(
readerState = viewModel.state, readerState = viewModel.state,
hasDisplayCutout = hasCutout,
onChangeReadingMode = viewModel::setMangaReadingMode, onChangeReadingMode = viewModel::setMangaReadingMode,
onChangeOrientation = viewModel::setMangaOrientationType, onChangeOrientation = viewModel::setMangaOrientationType,
) )
@ -426,6 +428,28 @@ class ReaderActivity : BaseActivity() {
screenModel = settingsScreenModel, screenModel = settingsScreenModel,
) )
} }
is ReaderViewModel.Dialog.ReadingModeSelect -> {
ReadingModeSelectDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
onChange = { stringRes ->
menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) {
menuToggleToast = toast(stringRes)
}
},
)
}
is ReaderViewModel.Dialog.OrientationModeSelect -> {
OrientationModeSelectDialog(
onDismissRequest = onDismissRequest,
screenModel = settingsScreenModel,
onChange = { stringRes ->
menuToggleToast?.cancel()
menuToggleToast = toast(stringRes)
},
)
}
is ReaderViewModel.Dialog.PageActions -> { is ReaderViewModel.Dialog.PageActions -> {
ReaderPageActionsDialog( ReaderPageActionsDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
@ -438,36 +462,61 @@ class ReaderActivity : BaseActivity() {
} }
} }
// Init listeners on bottom menu binding.readerMenuBottom.setComposeContent {
binding.readerNav.setComposeContent {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
if (state.viewer == null) return@setComposeContent if (state.viewer == null) return@setComposeContent
val isRtl = state.viewer is R2LPagerViewer val isRtl = state.viewer is R2LPagerViewer
ChapterNavigator( val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
isRtl = isRtl, val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
onNextChapter = ::loadNextChapter, val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
enabledNext = state.viewerChapters?.nextChapter != null, val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
onPreviousChapter = ::loadPreviousChapter,
enabledPrevious = state.viewerChapters?.prevChapter != null,
currentPage = state.currentPage,
totalPages = state.totalPages,
onSliderValueChange = {
isScrollingThroughPages = true
moveToPageIndex(it)
},
)
}
initBottomShortcuts() Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ChapterNavigator(
isRtl = isRtl,
onNextChapter = ::loadNextChapter,
enabledNext = state.viewerChapters?.nextChapter != null,
onPreviousChapter = ::loadPreviousChapter,
enabledPrevious = state.viewerChapters?.prevChapter != null,
currentPage = state.currentPage,
totalPages = state.totalPages,
onSliderValueChange = {
isScrollingThroughPages = true
moveToPageIndex(it)
},
)
BottomReaderBar(
readingMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false)),
onClickReadingMode = viewModel::openReadingModeSelectDialog,
orientationMode = OrientationType.fromPreference(viewModel.getMangaOrientationType(resolveDefault = false)),
onClickOrientationMode = viewModel::openOrientationModeSelectDialog,
cropEnabled = cropEnabled,
onClickCropBorder = {
val enabled = viewModel.toggleCropBorders()
menuToggleToast?.cancel()
menuToggleToast = toast(
if (enabled) {
R.string.on
} else {
R.string.off
},
)
},
onClickSettings = viewModel::openSettingsDialog,
)
}
}
val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply { val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply {
elevation = resources.getDimension(R.dimen.m3_sys_elevation_level2) elevation = resources.getDimension(R.dimen.m3_sys_elevation_level2)
alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light
} }
binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity)
val toolbarColor = ColorUtils.setAlphaComponent( val toolbarColor = ColorUtils.setAlphaComponent(
toolbarBackground.resolvedTintColor, toolbarBackground.resolvedTintColor,
toolbarBackground.alpha, toolbarBackground.alpha,
@ -481,112 +530,6 @@ class ReaderActivity : BaseActivity() {
setMenuVisibility(viewModel.state.value.menuVisible) setMenuVisibility(viewModel.state.value.menuVisible)
} }
private fun initBottomShortcuts() {
// Reading mode
with(binding.actionReadingMode) {
setTooltip(R.string.viewer)
setOnClickListener {
popupMenu(
items = ReadingModeType.entries.map { it.flagValue to it.stringRes },
selectedItemId = viewModel.getMangaReadingMode(resolveDefault = false),
) {
val newReadingMode = ReadingModeType.fromPreference(itemId)
viewModel.setMangaReadingMode(newReadingMode)
menuToggleToast?.cancel()
if (!readerPreferences.showReadingMode().get()) {
menuToggleToast = toast(newReadingMode.stringRes)
}
updateCropBordersShortcut()
}
}
}
// Crop borders
with(binding.actionCropBorders) {
setTooltip(R.string.pref_crop_borders)
setOnClickListener {
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val enabled = if (isPagerType) {
readerPreferences.cropBorders().toggle()
} else {
readerPreferences.cropBordersWebtoon().toggle()
}
menuToggleToast?.cancel()
menuToggleToast = toast(
if (enabled) {
R.string.on
} else {
R.string.off
},
)
}
}
updateCropBordersShortcut()
listOf(readerPreferences.cropBorders(), readerPreferences.cropBordersWebtoon())
.forEach { pref ->
pref.changes()
.onEach { updateCropBordersShortcut() }
.launchIn(lifecycleScope)
}
// Rotation
with(binding.actionRotation) {
setTooltip(R.string.rotation_type)
setOnClickListener {
popupMenu(
items = OrientationType.entries.map { it.flagValue to it.stringRes },
selectedItemId = viewModel.manga?.orientationType?.toInt()
?: readerPreferences.defaultOrientationType().get(),
) {
val newOrientation = OrientationType.fromPreference(itemId)
viewModel.setMangaOrientationType(newOrientation)
menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes)
}
}
}
// Settings sheet
with(binding.actionSettings) {
setTooltip(R.string.action_settings)
setOnClickListener {
viewModel.openSettingsDialog()
}
}
}
private fun updateOrientationShortcut(preference: Int) {
val orientation = OrientationType.fromPreference(preference)
binding.actionRotation.setImageResource(orientation.iconRes)
}
private fun updateCropBordersShortcut() {
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
val enabled = if (isPagerType) {
readerPreferences.cropBorders().get()
} else {
readerPreferences.cropBordersWebtoon().get()
}
binding.actionCropBorders.setImageResource(
if (enabled) {
R.drawable.ic_crop_24dp
} else {
R.drawable.ic_crop_off_24dp
},
)
}
/** /**
* Sets the visibility of the menu according to [visible] and with an optional parameter to * Sets the visibility of the menu according to [visible] and with an optional parameter to
* [animate] the views. * [animate] the views.
@ -653,13 +596,8 @@ class ReaderActivity : BaseActivity() {
*/ */
private fun setManga(manga: Manga) { private fun setManga(manga: Manga) {
val prevViewer = viewModel.state.value.viewer val prevViewer = viewModel.state.value.viewer
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this) val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this)
updateCropBordersShortcut()
if (window.sharedElementEnterTransition is MaterialContainerTransform) { if (window.sharedElementEnterTransition is MaterialContainerTransform) {
// Wait until transition is complete to avoid crash on API 26 // Wait until transition is complete to avoid crash on API 26
window.sharedElementEnterTransition.doOnEnd { window.sharedElementEnterTransition.doOnEnd {
@ -894,7 +832,6 @@ class ReaderActivity : BaseActivity() {
if (newOrientation.flag != requestedOrientation) { if (newOrientation.flag != requestedOrientation) {
requestedOrientation = newOrientation.flag requestedOrientation = newOrientation.flag
} }
updateOrientationShortcut(viewModel.getMangaOrientationType(resolveDefault = false))
} }
/** /**

View file

@ -61,7 +61,7 @@ class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet)
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (navigation == null) return if (navigation == null) return
navigation?.regions?.forEach { region -> navigation?.getRegions()?.forEach { region ->
val rect = region.rectF val rect = region.rectF
// Scale rect from 1f,1f to screen width and height // Scale rect from 1f,1f to screen width and height

View file

@ -60,6 +60,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.preference.toggle
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
@ -129,6 +130,15 @@ class ReaderViewModel @JvmOverloads constructor(
field = value field = value
} }
/**
* The visible page index of the currently loaded chapter. Used to restore from process kill.
*/
private var chapterPageIndex = savedState.get<Int>("page_index") ?: -1
set(value) {
savedState["page_index"] = value
field = value
}
/** /**
* The chapter loader for the loaded manga. It'll be null until [manga] is set. * The chapter loader for the loaded manga. It'll be null until [manga] is set.
*/ */
@ -213,7 +223,10 @@ class ReaderViewModel @JvmOverloads constructor(
.distinctUntilChanged() .distinctUntilChanged()
.filterNotNull() .filterNotNull()
.onEach { currentChapter -> .onEach { currentChapter ->
if (!currentChapter.chapter.read) { if (chapterPageIndex >= 0) {
// Restore from SavedState
currentChapter.requestedPage = chapterPageIndex
} else if (!currentChapter.chapter.read) {
currentChapter.requestedPage = currentChapter.chapter.last_page_read currentChapter.requestedPage = currentChapter.chapter.last_page_read
} }
chapterId = currentChapter.chapter.id!! chapterId = currentChapter.chapter.id!!
@ -507,6 +520,7 @@ class ReaderViewModel @JvmOverloads constructor(
it.copy(currentPage = pageIndex + 1) it.copy(currentPage = pageIndex + 1)
} }
readerChapter.requestedPage = pageIndex readerChapter.requestedPage = pageIndex
chapterPageIndex = pageIndex
if (!incognitoMode && page.status != Page.State.ERROR) { if (!incognitoMode && page.status != Page.State.ERROR) {
readerChapter.chapter.last_page_read = pageIndex readerChapter.chapter.last_page_read = pageIndex
@ -678,6 +692,15 @@ class ReaderViewModel @JvmOverloads constructor(
} }
} }
fun toggleCropBorders(): Boolean {
val isPagerType = ReadingModeType.isPagerType(getMangaReadingMode())
return if (isPagerType) {
readerPreferences.cropBorders().toggle()
} else {
readerPreferences.cropBordersWebtoon().toggle()
}
}
/** /**
* Generate a filename for the given [manga] and [page] * Generate a filename for the given [manga] and [page]
*/ */
@ -700,6 +723,14 @@ class ReaderViewModel @JvmOverloads constructor(
mutableState.update { it.copy(dialog = Dialog.Loading) } mutableState.update { it.copy(dialog = Dialog.Loading) }
} }
fun openReadingModeSelectDialog() {
mutableState.update { it.copy(dialog = Dialog.ReadingModeSelect) }
}
fun openOrientationModeSelectDialog() {
mutableState.update { it.copy(dialog = Dialog.OrientationModeSelect) }
}
fun openPageDialog(page: ReaderPage) { fun openPageDialog(page: ReaderPage) {
mutableState.update { it.copy(dialog = Dialog.PageActions(page)) } mutableState.update { it.copy(dialog = Dialog.PageActions(page)) }
} }
@ -910,6 +941,8 @@ class ReaderViewModel @JvmOverloads constructor(
sealed interface Dialog { sealed interface Dialog {
data object Loading : Dialog data object Loading : Dialog
data object Settings : Dialog data object Settings : Dialog
data object ReadingModeSelect : Dialog
data object OrientationModeSelect : Dialog
data class PageActions(val page: ReaderPage) : Dialog data class PageActions(val page: ReaderPage) : Dialog
} }

View file

@ -13,6 +13,7 @@ import uy.kohesive.injekt.api.get
class ReaderSettingsScreenModel( class ReaderSettingsScreenModel(
readerState: StateFlow<ReaderViewModel.State>, readerState: StateFlow<ReaderViewModel.State>,
val hasDisplayCutout: Boolean,
val onChangeReadingMode: (ReadingModeType) -> Unit, val onChangeReadingMode: (ReadingModeType) -> Unit,
val onChangeOrientation: (OrientationType) -> Unit, val onChangeOrientation: (OrientationType) -> Unit,
val preferences: ReaderPreferences = Injekt.get(), val preferences: ReaderPreferences = Injekt.get(),

View file

@ -32,15 +32,19 @@ abstract class ViewerNavigation {
private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f) private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f)
abstract var regions: List<Region>
var invertMode: ReaderPreferences.TappingInvertMode = ReaderPreferences.TappingInvertMode.NONE var invertMode: ReaderPreferences.TappingInvertMode = ReaderPreferences.TappingInvertMode.NONE
protected abstract var regionList: List<Region>
/** Returns regions with applied inversion. */
fun getRegions(): List<Region> {
return regionList.map { it.invert(invertMode) }
}
fun getAction(pos: PointF): NavigationRegion { fun getAction(pos: PointF): NavigationRegion {
val x = pos.x val x = pos.x
val y = pos.y val y = pos.y
val region = regions.map { it.invert(invertMode) } val region = getRegions().find { it.rectF.contains(x, y) }
.find { it.rectF.contains(x, y) }
return when { return when {
region != null -> region.type region != null -> region.type
constantMenuRegion.contains(x, y) -> NavigationRegion.MENU constantMenuRegion.contains(x, y) -> NavigationRegion.MENU

View file

@ -14,5 +14,5 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/ */
class DisabledNavigation : ViewerNavigation() { class DisabledNavigation : ViewerNavigation() {
override var regions: List<Region> = emptyList() override var regionList: List<Region> = emptyList()
} }

View file

@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/ */
class EdgeNavigation : ViewerNavigation() { class EdgeNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf( override var regionList: List<Region> = listOf(
Region( Region(
rectF = RectF(0f, 0f, 0.33f, 1f), rectF = RectF(0f, 0f, 0.33f, 1f),
type = NavigationRegion.NEXT, type = NavigationRegion.NEXT,

View file

@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/ */
class KindlishNavigation : ViewerNavigation() { class KindlishNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf( override var regionList: List<Region> = listOf(
Region( Region(
rectF = RectF(0.33f, 0.33f, 1f, 1f), rectF = RectF(0.33f, 0.33f, 1f, 1f),
type = NavigationRegion.NEXT, type = NavigationRegion.NEXT,

View file

@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/ */
open class LNavigation : ViewerNavigation() { open class LNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf( override var regionList: List<Region> = listOf(
Region( Region(
rectF = RectF(0f, 0.33f, 0.33f, 0.66f), rectF = RectF(0f, 0.33f, 0.33f, 0.66f),
type = NavigationRegion.PREV, type = NavigationRegion.PREV,

View file

@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
*/ */
class RightAndLeftNavigation : ViewerNavigation() { class RightAndLeftNavigation : ViewerNavigation() {
override var regions: List<Region> = listOf( override var regionList: List<Region> = listOf(
Region( Region(
rectF = RectF(0f, 0f, 0.33f, 1f), rectF = RectF(0f, 0f, 0.33f, 1f),
type = NavigationRegion.LEFT, type = NavigationRegion.LEFT,

View file

@ -1,22 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class AnilistLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
val matchResult = regex.find(data?.fragment.toString())
if (matchResult?.groups?.get(1) != null) {
lifecycleScope.launchIO {
trackManager.aniList.login(matchResult.groups[1]!!.value)
returnToSettings()
}
} else {
trackManager.aniList.logout()
returnToSettings()
}
}
}

View file

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class BangumiLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.bangumi.login(code)
returnToSettings()
}
} else {
trackManager.bangumi.logout()
returnToSettings()
}
}
}

View file

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class MyAnimeListLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.myAnimeList.login(code)
returnToSettings()
}
} else {
trackManager.myAnimeList.logout()
returnToSettings()
}
}
}

View file

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class ShikimoriLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.shikimori.login(code)
returnToSettings()
}
} else {
trackManager.shikimori.logout()
returnToSettings()
}
}
}

View file

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class SimklLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.simkl.login(code)
returnToSettings()
}
} else {
trackManager.simkl.logout()
returnToSettings()
}
}
}

View file

@ -0,0 +1,84 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.net.Uri
import androidx.lifecycle.lifecycleScope
import tachiyomi.core.util.lang.launchIO
class TrackLoginActivity : BaseOAuthLoginActivity() {
override fun handleResult(data: Uri?) {
when (data?.host) {
"anilist-auth" -> handleAnilist(data)
"bangumi-auth" -> handleBangumi(data)
"myanimelist-auth" -> handleMyAnimeList(data)
"shikimori-auth" -> handleShikimori(data)
"simkl-auth" -> handleSimkl(data)
}
}
private fun handleAnilist(data: Uri) {
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
val matchResult = regex.find(data.fragment.toString())
if (matchResult?.groups?.get(1) != null) {
lifecycleScope.launchIO {
trackManager.aniList.login(matchResult.groups[1]!!.value)
returnToSettings()
}
} else {
trackManager.aniList.logout()
returnToSettings()
}
}
private fun handleBangumi(data: Uri) {
val code = data.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.bangumi.login(code)
returnToSettings()
}
} else {
trackManager.bangumi.logout()
returnToSettings()
}
}
private fun handleMyAnimeList(data: Uri) {
val code = data.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.myAnimeList.login(code)
returnToSettings()
}
} else {
trackManager.myAnimeList.logout()
returnToSettings()
}
}
private fun handleShikimori(data: Uri) {
val code = data.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.shikimori.login(code)
returnToSettings()
}
} else {
trackManager.shikimori.logout()
returnToSettings()
}
}
private fun handleSimkl(data: Uri?) {
val code = data?.getQueryParameter("code")
if (code != null) {
lifecycleScope.launchIO {
trackManager.simkl.login(code)
returnToSettings()
}
} else {
trackManager.simkl.logout()
returnToSettings()
}
}
}

View file

@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.util.chapter
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
/** /**
* Returns a copy of the list with not downloaded chapters removed * Returns a copy of the list with not downloaded chapters removed.
*/ */
fun List<Chapter>.filterDownloadedChapters(manga: Manga): List<Chapter> { fun List<Chapter>.filterDownloadedChapters(manga: Manga): List<Chapter> {
if (manga.isLocal()) return this
val downloadCache: MangaDownloadCache = Injekt.get() val downloadCache: MangaDownloadCache = Injekt.get()
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) } return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) }

View file

@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.util.episode
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
/** /**
* Returns a copy of the list with not downloaded chapters removed * Returns a copy of the list with not downloaded chapters removed.
*/ */
fun List<Episode>.filterDownloadedEpisodes(anime: Anime): List<Episode> { fun List<Episode>.filterDownloadedEpisodes(anime: Anime): List<Episode> {
if (anime.isLocal()) return this
val downloadCache: AnimeDownloadCache = Injekt.get() val downloadCache: AnimeDownloadCache = Injekt.get()
return filter { downloadCache.isEpisodeDownloaded(it.name, it.scanlator, anime.title, anime.source, false) } return filter { downloadCache.isEpisodeDownloaded(it.name, it.scanlator, anime.title, anime.source, false) }

View file

@ -7,20 +7,13 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager import android.os.PowerManager
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.content.PermissionChecker import androidx.core.content.PermissionChecker
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.green
import androidx.core.graphics.red
import androidx.core.net.toUri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
@ -35,7 +28,6 @@ import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import kotlin.math.roundToInt
/** /**
* Copies a string to clipboard * Copies a string to clipboard
@ -69,25 +61,6 @@ fun Context.copyToClipboard(label: String, content: String) {
*/ */
fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED
/**
* Returns the color for the given attribute.
*
* @param resource the attribute.
* @param alphaFactor the alpha number [0,1].
*/
@ColorInt fun Context.getResourceColor(@AttrRes resource: Int, alphaFactor: Float = 1f): Int {
val typedArray = obtainStyledAttributes(intArrayOf(resource))
val color = typedArray.getColor(0, 0)
typedArray.recycle()
if (alphaFactor < 1f) {
val alpha = (color.alpha * alphaFactor).roundToInt()
return Color.argb(alpha, color.red, color.green, color.blue)
}
return color
}
val Context.powerManager: PowerManager val Context.powerManager: PowerManager
get() = getSystemService()!! get() = getSystemService()!!

View file

@ -2,11 +2,8 @@
package eu.kanade.tachiyomi.util.view package eu.kanade.tachiyomi.util.view
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.Gravity import android.view.Gravity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -14,11 +11,7 @@ import android.view.View
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.TooltipCompat
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -27,11 +20,8 @@ import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.forEach
import com.google.android.material.shape.MaterialShapeDrawable
import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
inline fun ComponentActivity.setComposeContent( inline fun ComponentActivity.setComposeContent(
parent: CompositionContext? = null, parent: CompositionContext? = null,
@ -65,24 +55,6 @@ fun ComposeView.setComposeContent(
} }
} }
/**
* Adds a tooltip shown on long press.
*
* @param stringRes String resource for tooltip.
*/
inline fun View.setTooltip(@StringRes stringRes: Int) {
setTooltip(context.getString(stringRes))
}
/**
* Adds a tooltip shown on long press.
*
* @param text Text for tooltip.
*/
inline fun View.setTooltip(text: String) {
TooltipCompat.setTooltipText(this, text)
}
/** /**
* Shows a popup menu on top of this view. * Shows a popup menu on top of this view.
* *
@ -110,57 +82,6 @@ inline fun View.popupMenu(
return popup return popup
} }
/**
* Shows a popup menu on top of this view.
*
* @param items menu item names to inflate the menu with. List of itemId to stringRes pairs.
* @param selectedItemId optionally show a checkmark beside an item with this itemId.
* @param onMenuItemClick function to execute when a menu item is clicked.
*/
@SuppressLint("RestrictedApi")
inline fun View.popupMenu(
items: List<Pair<Int, Int>>,
selectedItemId: Int? = null,
noinline onMenuItemClick: MenuItem.() -> Unit,
): PopupMenu {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
items.forEach { (id, stringRes) ->
popup.menu.add(0, id, 0, stringRes)
}
if (selectedItemId != null) {
(popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
val emptyIcon = AppCompatResources.getDrawable(context, R.drawable.ic_blank_24dp)
popup.menu.forEach { item ->
item.icon = when (item.itemId) {
selectedItemId -> AppCompatResources.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply {
setTint(context.getResourceColor(android.R.attr.textColorPrimary))
}
else -> emptyIcon
}
}
}
popup.setOnMenuItemClickListener {
it.onMenuItemClick()
true
}
popup.show()
return popup
}
/**
* Returns a deep copy of the provided [Drawable]
*/
inline fun <reified T : Drawable> T.copy(context: Context): T? {
return (constantState?.newDrawable()?.mutate() as? T).apply {
if (this is MaterialShapeDrawable) {
initializeElevationOverlay(context)
}
}
}
fun View?.isVisibleOnScreen(): Boolean { fun View?.isVisibleOnScreen(): Boolean {
if (this == null) { if (this == null) {
return false return false

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98 0,-0.34 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.09,-0.16 -0.26,-0.25 -0.44,-0.25 -0.06,0 -0.12,0.01 -0.17,0.03l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.06,-0.02 -0.12,-0.03 -0.18,-0.03 -0.17,0 -0.34,0.09 -0.43,0.25l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98 0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.09,0.16 0.26,0.25 0.44,0.25 0.06,0 0.12,-0.01 0.17,-0.03l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.06,0.02 0.12,0.03 0.18,0.03 0.17,0 0.34,-0.09 0.43,-0.25l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM17.45,11.27c0.04,0.31 0.05,0.52 0.05,0.73 0,0.21 -0.02,0.43 -0.05,0.73l-0.14,1.13 0.89,0.7 1.08,0.84 -0.7,1.21 -1.27,-0.51 -1.04,-0.42 -0.9,0.68c-0.43,0.32 -0.84,0.56 -1.25,0.73l-1.06,0.43 -0.16,1.13 -0.2,1.35h-1.4l-0.19,-1.35 -0.16,-1.13 -1.06,-0.43c-0.43,-0.18 -0.83,-0.41 -1.23,-0.71l-0.91,-0.7 -1.06,0.43 -1.27,0.51 -0.7,-1.21 1.08,-0.84 0.89,-0.7 -0.14,-1.13c-0.03,-0.31 -0.05,-0.54 -0.05,-0.74s0.02,-0.43 0.05,-0.73l0.14,-1.13 -0.89,-0.7 -1.08,-0.84 0.7,-1.21 1.27,0.51 1.04,0.42 0.9,-0.68c0.43,-0.32 0.84,-0.56 1.25,-0.73l1.06,-0.43 0.16,-1.13 0.2,-1.35h1.39l0.19,1.35 0.16,1.13 1.06,0.43c0.43,0.18 0.83,0.41 1.23,0.71l0.91,0.7 1.06,-0.43 1.27,-0.51 0.7,1.21 -1.07,0.85 -0.89,0.7 0.14,1.13zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector>

View file

@ -1,5 +1,4 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -58,83 +57,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize" /> android:minHeight="?attr/actionBarSize" />
<LinearLayout <androidx.compose.ui.platform.ComposeView
android:id="@+id/reader_menu_bottom" android:id="@+id/reader_menu_bottom"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom" android:layout_gravity="bottom" />
android:orientation="vertical">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/reader_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layoutDirection="ltr" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbar_bottom"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom"
android:clickable="true"
tools:ignore="KeyboardInaccessibleWidget">
<ImageButton
android:id="@+id/action_reading_mode"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/viewer"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/action_crop_borders"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_reader_default_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_crop_borders"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pref_crop_borders"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/action_rotation"
app:layout_constraintStart_toEndOf="@+id/action_reading_mode"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_crop_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_rotation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/pref_rotation_type"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toStartOf="@id/action_settings"
app:layout_constraintStart_toEndOf="@+id/action_crop_borders"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_screen_rotation_24dp"
app:tint="?attr/colorOnSurface" />
<ImageButton
android:id="@+id/action_settings"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_settings"
android:padding="@dimen/screen_edge_margin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/action_rotation"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings_24dp"
app:tint="?attr/colorOnSurface" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</FrameLayout> </FrameLayout>

View file

@ -8,10 +8,17 @@ import java.io.IOException
import java.util.ArrayDeque import java.util.ArrayDeque
import java.util.concurrent.Semaphore import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toDuration
import kotlin.time.toDurationUnit
/** /**
* An OkHttp interceptor that handles rate limiting. * An OkHttp interceptor that handles rate limiting.
* *
* This uses `java.time` APIs and is the legacy method, kept
* for compatibility reasons with existing extensions.
*
* Examples: * Examples:
* *
* permits = 5, period = 1, unit = seconds => 5 requests per second * permits = 5, period = 1, unit = seconds => 5 requests per second
@ -19,27 +26,43 @@ import java.util.concurrent.TimeUnit
* *
* @since extension-lib 1.3 * @since extension-lib 1.3
* *
* @param permits {Int} Number of requests allowed within a period of units. * @param permits [Int] Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1. * @param period [Long] The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds. * @param unit [TimeUnit] The unit of time for the period. Defaults to seconds.
*/ */
@Deprecated("Use the version with kotlin.time APIs instead.")
fun OkHttpClient.Builder.rateLimit( fun OkHttpClient.Builder.rateLimit(
permits: Int, permits: Int,
period: Long = 1, period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS, unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(null, permits, period, unit)) ) = addInterceptor(RateLimitInterceptor(null, permits, period.toDuration(unit.toDurationUnit())))
/**
* An OkHttp interceptor that handles rate limiting.
*
* Examples:
*
* permits = 5, period = 1.seconds => 5 requests per second
* permits = 10, period = 2.minutes => 10 requests per 2 minutes
*
* @since extension-lib 1.5
*
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimit(permits: Int, period: Duration = 1.seconds) =
addInterceptor(RateLimitInterceptor(null, permits, period))
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */ /** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal class RateLimitInterceptor( internal class RateLimitInterceptor(
private val host: String?, private val host: String?,
private val permits: Int, private val permits: Int,
period: Long, period: Duration,
unit: TimeUnit,
) : Interceptor { ) : Interceptor {
private val requestQueue = ArrayDeque<Long>(permits) private val requestQueue = ArrayDeque<Long>(permits)
private val rateLimitMillis = unit.toMillis(period) private val rateLimitMillis = period.inWholeMilliseconds
private val fairLock = Semaphore(1, true) private val fairLock = Semaphore(1, true)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {

View file

@ -1,12 +1,20 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toDuration
import kotlin.time.toDurationUnit
/** /**
* An OkHttp interceptor that handles given url host's rate limiting. * An OkHttp interceptor that handles given url host's rate limiting.
* *
* This uses Java Time APIs and is the legacy method, kept
* for compatibility reasons with existing extensions.
*
* Examples: * Examples:
* *
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com * httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
@ -14,14 +22,55 @@ import java.util.concurrent.TimeUnit
* *
* @since extension-lib 1.3 * @since extension-lib 1.3
* *
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host() * @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits {Int} Number of requests allowed within a period of units. * @param permits [Int] Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1. * @param period [Long] The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds. * @param unit [TimeUnit] The unit of time for the period. Defaults to seconds.
*/ */
@Deprecated("Use the version with kotlin.time APIs instead.")
fun OkHttpClient.Builder.rateLimitHost( fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl, httpUrl: HttpUrl,
permits: Int, permits: Int,
period: Long = 1, period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS, unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period, unit)) ) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
/**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* Examples:
*
* httpUrl = "https://api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1.seconds => 5 requests per second to api.manga.com
* httpUrl = "https://imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.5
*
* @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Duration = 1.seconds,
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period))
/**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* Examples:
*
* url = "https://api.manga.com", permits = 5, period = 1.seconds => 5 requests per second to api.manga.com
* url = "https://imagecdn.manga.com", permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.5
*
* @param url [String] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
url: String,
permits: Int,
period: Duration = 1.seconds,
) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))

View file

@ -1,7 +1,7 @@
package tachiyomi.data package tachiyomi.data
import app.cash.sqldelight.ColumnAdapter import app.cash.sqldelight.ColumnAdapter
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import java.util.Date import java.util.Date
object DateColumnAdapter : ColumnAdapter<Date, Long> { object DateColumnAdapter : ColumnAdapter<Date, Long> {

View file

@ -1,6 +1,6 @@
package tachiyomi.data.entries.anime package tachiyomi.data.entries.anime
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.library.anime.LibraryAnime import tachiyomi.domain.library.anime.LibraryAnime

View file

@ -1,6 +1,6 @@
package tachiyomi.data.entries.manga package tachiyomi.data.entries.manga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.library.manga.LibraryManga import tachiyomi.domain.library.manga.LibraryManga

View file

@ -1,4 +1,4 @@
import eu.kanade.tachiyomi.source.model.UpdateStrategy; import eu.kanade.tachiyomi.model.UpdateStrategy;
import kotlin.collections.List; import kotlin.collections.List;
import kotlin.Boolean; import kotlin.Boolean;
import kotlin.String; import kotlin.String;

View file

@ -1,4 +1,4 @@
import eu.kanade.tachiyomi.source.model.UpdateStrategy; import eu.kanade.tachiyomi.model.UpdateStrategy;
import kotlin.collections.List; import kotlin.collections.List;
import kotlin.Boolean; import kotlin.Boolean;
import kotlin.String; import kotlin.String;

View file

@ -2,34 +2,34 @@ package tachiyomi.domain.entries.anime.interactor
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate import tachiyomi.domain.entries.anime.model.AnimeUpdate
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
import tachiyomi.domain.items.episode.model.Episode import tachiyomi.domain.items.episode.model.Episode
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.Instant import java.time.Instant
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
const val MAX_GRACE_PERIOD = 28 const val MAX_FETCH_INTERVAL = 28
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
class SetAnimeFetchInterval( class SetAnimeFetchInterval(
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
) { ) {
fun update( suspend fun toAnimeUpdateOrNull(
anime: Anime, anime: Anime,
episodes: List<Episode>, dateTime: ZonedDateTime,
zonedDateTime: ZonedDateTime, window: Pair<Long, Long>,
fetchRange: Pair<Long, Long>,
): AnimeUpdate? { ): AnimeUpdate? {
val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) { val currentWindow = if (window.first == 0L && window.second == 0L) {
getCurrent(ZonedDateTime.now()) getWindow(ZonedDateTime.now())
} else { } else {
fetchRange window
} }
val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(episodes, zonedDateTime) val episodes = getEpisodeByAnimeId.await(anime.id)
val nextUpdate = calculateNextUpdate(anime, interval, zonedDateTime, currentInterval) val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(episodes, dateTime)
val nextUpdate = calculateNextUpdate(anime, interval, dateTime, currentWindow)
return if (anime.nextUpdate == nextUpdate && anime.fetchInterval == interval) { return if (anime.nextUpdate == nextUpdate && anime.fetchInterval == interval) {
null null
@ -38,20 +38,11 @@ class SetAnimeFetchInterval(
} }
} }
fun getCurrent(timeToCal: ZonedDateTime): Pair<Long, Long> { fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
// lead range and the following range depend on if updateOnlyExpectedPeriod set. val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
var followRange = 0 val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
var leadRange = 0 val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) { return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
followRange = libraryPreferences.followingAnimeExpectedDays().get()
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
}
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
// revert math of (next_update + follow < now) become (next_update < now - follow)
// so (now - follow) become lower limit
val lowerRange = startToday.minusDays(followRange.toLong())
val higherRange = startToday.plusDays(leadRange.toLong())
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
} }
internal fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int { internal fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
@ -91,35 +82,41 @@ class SetAnimeFetchInterval(
// Default to 7 days // Default to 7 days
else -> 7 else -> 7
} }
// Min 1, max 28 days
return interval.coerceIn(1, MAX_GRACE_PERIOD) return interval.coerceIn(1, MAX_FETCH_INTERVAL)
} }
private fun calculateNextUpdate( private fun calculateNextUpdate(
anime: Anime, anime: Anime,
interval: Int, interval: Int,
zonedDateTime: ZonedDateTime, dateTime: ZonedDateTime,
fetchRange: Pair<Long, Long>, window: Pair<Long, Long>,
): Long { ): Long {
return if ( return if (
anime.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) || anime.nextUpdate !in window.first.rangeTo(window.second + 1) ||
anime.fetchInterval == 0 anime.fetchInterval == 0
) { ) {
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay() val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), dateTime.zone)
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt() .toLocalDate()
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28)) .atStartOfDay()
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000 val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
val cycle = timeSinceLatest.floorDiv(
interval.absoluteValue.takeIf { interval < 0 }
?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
)
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
} else { } else {
anime.nextUpdate anime.nextUpdate
} }
} }
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int { private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
if (delta >= maxValue) return maxValue if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
val cycle = timeSinceLatest.floorDiv(delta) + 1
// double delta again if missed more than 9 check in new delta // double delta again if missed more than 9 check in new delta
val cycle = timeSinceLatest.floorDiv(delta) + 1
return if (cycle > doubleWhenOver) { return if (cycle > doubleWhenOver) {
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue) doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
} else { } else {
delta delta
} }

View file

@ -1,6 +1,6 @@
package tachiyomi.domain.entries.anime.model package tachiyomi.domain.entries.anime.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import java.io.Serializable import java.io.Serializable
import kotlin.math.pow import kotlin.math.pow

View file

@ -1,6 +1,6 @@
package tachiyomi.domain.entries.anime.model package tachiyomi.domain.entries.anime.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
data class AnimeUpdate( data class AnimeUpdate(
val id: Long, val id: Long,

View file

@ -2,34 +2,34 @@ package tachiyomi.domain.entries.manga.interactor
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.entries.manga.model.MangaUpdate import tachiyomi.domain.entries.manga.model.MangaUpdate
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.library.service.LibraryPreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.time.Instant import java.time.Instant
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
const val MAX_GRACE_PERIOD = 28 const val MAX_FETCH_INTERVAL = 28
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
class SetMangaFetchInterval( class SetMangaFetchInterval(
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val getChapterByMangaId: GetChapterByMangaId,
) { ) {
fun update( suspend fun toMangaUpdateOrNull(
manga: Manga, manga: Manga,
chapters: List<Chapter>, dateTime: ZonedDateTime,
zonedDateTime: ZonedDateTime, window: Pair<Long, Long>,
fetchRange: Pair<Long, Long>,
): MangaUpdate? { ): MangaUpdate? {
val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) { val currentWindow = if (window.first == 0L && window.second == 0L) {
getCurrent(ZonedDateTime.now()) getWindow(ZonedDateTime.now())
} else { } else {
fetchRange window
} }
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime) val chapters = getChapterByMangaId.await(manga.id)
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentInterval) val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime)
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) { return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
null null
@ -38,20 +38,11 @@ class SetMangaFetchInterval(
} }
} }
fun getCurrent(timeToCal: ZonedDateTime): Pair<Long, Long> { fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
// lead range and the following range depend on if updateOnlyExpectedPeriod set. val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
var followRange = 0 val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
var leadRange = 0 val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) { return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
followRange = libraryPreferences.followingAnimeExpectedDays().get()
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
}
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
// revert math of (next_update + follow < now) become (next_update < now - follow)
// so (now - follow) become lower limit
val lowerRange = startToday.minusDays(followRange.toLong())
val higherRange = startToday.plusDays(leadRange.toLong())
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
} }
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int { internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
@ -91,35 +82,41 @@ class SetMangaFetchInterval(
// Default to 7 days // Default to 7 days
else -> 7 else -> 7
} }
// Min 1, max 28 days
return interval.coerceIn(1, MAX_GRACE_PERIOD) return interval.coerceIn(1, MAX_FETCH_INTERVAL)
} }
private fun calculateNextUpdate( private fun calculateNextUpdate(
manga: Manga, manga: Manga,
interval: Int, interval: Int,
zonedDateTime: ZonedDateTime, dateTime: ZonedDateTime,
fetchRange: Pair<Long, Long>, window: Pair<Long, Long>,
): Long { ): Long {
return if ( return if (
manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) || manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
manga.fetchInterval == 0 manga.fetchInterval == 0
) { ) {
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay() val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt() .toLocalDate()
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28)) .atStartOfDay()
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000 val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
val cycle = timeSinceLatest.floorDiv(
interval.absoluteValue.takeIf { interval < 0 }
?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
)
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
} else { } else {
manga.nextUpdate manga.nextUpdate
} }
} }
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int { private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
if (delta >= maxValue) return maxValue if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
val cycle = timeSinceLatest.floorDiv(delta) + 1
// double delta again if missed more than 9 check in new delta // double delta again if missed more than 9 check in new delta
val cycle = timeSinceLatest.floorDiv(delta) + 1
return if (cycle > doubleWhenOver) { return if (cycle > doubleWhenOver) {
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue) doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
} else { } else {
delta delta
} }

View file

@ -1,6 +1,6 @@
package tachiyomi.domain.entries.manga.model package tachiyomi.domain.entries.manga.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import tachiyomi.core.preference.TriState import tachiyomi.core.preference.TriState
import java.io.Serializable import java.io.Serializable

View file

@ -1,6 +1,6 @@
package tachiyomi.domain.entries.manga.model package tachiyomi.domain.entries.manga.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
data class MangaUpdate( data class MangaUpdate(
val id: Long, val id: Long,

View file

@ -62,12 +62,6 @@ class LibraryPreferences(
), ),
) )
fun leadingAnimeExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1)
fun followingAnimeExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1)
fun leadingMangaExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1)
fun followingMangaExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1)
fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false) fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false)
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false) fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)

View file

@ -11,6 +11,7 @@ import java.time.ZonedDateTime
@Execution(ExecutionMode.CONCURRENT) @Execution(ExecutionMode.CONCURRENT)
class SetAnimeFetchIntervalTest { class SetAnimeFetchIntervalTest {
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z") private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
private var episode = Episode.create().copy( private var episode = Episode.create().copy(
dateFetch = testTime.toEpochSecond() * 1000, dateFetch = testTime.toEpochSecond() * 1000,
@ -19,14 +20,8 @@ class SetAnimeFetchIntervalTest {
private val setAnimeFetchInterval = SetAnimeFetchInterval(mockk()) private val setAnimeFetchInterval = SetAnimeFetchInterval(mockk())
private fun episodeAddTime(episode: Episode, duration: Duration): Episode {
val newTime = testTime.plus(duration).toEpochSecond() * 1000
return episode.copy(dateFetch = newTime, dateUpload = newTime)
}
// default 7 when less than 3 distinct day
@Test @Test
fun `calculateInterval returns 7 when 1 episodes in 1 day`() { fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
val episodes = mutableListOf<Episode>() val episodes = mutableListOf<Episode>()
(1..1).forEach { (1..1).forEach {
val duration = Duration.ofHours(10) val duration = Duration.ofHours(10)
@ -63,9 +58,8 @@ class SetAnimeFetchIntervalTest {
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7 setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
} }
// Default 1 if interval less than 1
@Test @Test
fun `calculateInterval returns 1 when 5 episodes in 75 hours, 3 days`() { fun `calculateInterval returns default of 1 day when interval less than 1`() {
val episodes = mutableListOf<Episode>() val episodes = mutableListOf<Episode>()
(1..5).forEach { (1..5).forEach {
val duration = Duration.ofHours(15L * it) val duration = Duration.ofHours(15L * it)
@ -98,9 +92,8 @@ class SetAnimeFetchIntervalTest {
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 2 setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 2
} }
// If interval is decimal, floor to closest integer
@Test @Test
fun `calculateInterval returns 1 when 5 episodes in 125 hours, 5 days`() { fun `calculateInterval returns floored value when interval is decimal`() {
val episodes = mutableListOf<Episode>() val episodes = mutableListOf<Episode>()
(1..5).forEach { (1..5).forEach {
val duration = Duration.ofHours(25L * it) val duration = Duration.ofHours(25L * it)
@ -121,9 +114,8 @@ class SetAnimeFetchIntervalTest {
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1 setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
} }
// Use fetch time if upload time not available
@Test @Test
fun `calculateInterval returns 1 when 5 episodes in 125 hours, 5 days of dateFetch`() { fun `calculateInterval returns interval based on fetch time if upload time not available`() {
val episodes = mutableListOf<Episode>() val episodes = mutableListOf<Episode>()
(1..5).forEach { (1..5).forEach {
val duration = Duration.ofHours(25L * it) val duration = Duration.ofHours(25L * it)
@ -132,4 +124,9 @@ class SetAnimeFetchIntervalTest {
} }
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1 setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
} }
private fun episodeAddTime(episode: Episode, duration: Duration): Episode {
val newTime = testTime.plus(duration).toEpochSecond() * 1000
return episode.copy(dateFetch = newTime, dateUpload = newTime)
}
} }

View file

@ -11,6 +11,7 @@ import java.time.ZonedDateTime
@Execution(ExecutionMode.CONCURRENT) @Execution(ExecutionMode.CONCURRENT)
class SetMangaFetchIntervalTest { class SetMangaFetchIntervalTest {
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z") private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
private var chapter = Chapter.create().copy( private var chapter = Chapter.create().copy(
dateFetch = testTime.toEpochSecond() * 1000, dateFetch = testTime.toEpochSecond() * 1000,
@ -19,14 +20,8 @@ class SetMangaFetchIntervalTest {
private val setMangaFetchInterval = SetMangaFetchInterval(mockk()) private val setMangaFetchInterval = SetMangaFetchInterval(mockk())
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
val newTime = testTime.plus(duration).toEpochSecond() * 1000
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
}
// default 7 when less than 3 distinct day
@Test @Test
fun `calculateInterval returns 7 when 1 chapters in 1 day`() { fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
val chapters = mutableListOf<Chapter>() val chapters = mutableListOf<Chapter>()
(1..1).forEach { (1..1).forEach {
val duration = Duration.ofHours(10) val duration = Duration.ofHours(10)
@ -63,9 +58,8 @@ class SetMangaFetchIntervalTest {
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7 setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
} }
// Default 1 if interval less than 1
@Test @Test
fun `calculateInterval returns 1 when 5 chapters in 75 hours, 3 days`() { fun `calculateInterval returns default of 1 day when interval less than 1`() {
val chapters = mutableListOf<Chapter>() val chapters = mutableListOf<Chapter>()
(1..5).forEach { (1..5).forEach {
val duration = Duration.ofHours(15L * it) val duration = Duration.ofHours(15L * it)
@ -98,9 +92,8 @@ class SetMangaFetchIntervalTest {
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 2 setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
} }
// If interval is decimal, floor to closest integer
@Test @Test
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days`() { fun `calculateInterval returns floored value when interval is decimal`() {
val chapters = mutableListOf<Chapter>() val chapters = mutableListOf<Chapter>()
(1..5).forEach { (1..5).forEach {
val duration = Duration.ofHours(25L * it) val duration = Duration.ofHours(25L * it)
@ -121,9 +114,8 @@ class SetMangaFetchIntervalTest {
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
} }
// Use fetch time if upload time not available
@Test @Test
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days of dateFetch`() { fun `calculateInterval returns interval based on fetch time if upload time not available`() {
val chapters = mutableListOf<Chapter>() val chapters = mutableListOf<Chapter>()
(1..5).forEach { (1..5).forEach {
val duration = Duration.ofHours(25L * it) val duration = Duration.ofHours(25L * it)
@ -132,4 +124,9 @@ class SetMangaFetchIntervalTest {
} }
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1 setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
} }
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
val newTime = testTime.plus(duration).toEpochSecond() * 1000
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
}
} }

View file

@ -23,7 +23,7 @@ lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.r
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" } lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
work-runtime = "androidx.work:work-runtime-ktx:2.8.1" work-runtime = "androidx.work:work-runtime-ktx:2.8.1"
guava = "com.google.guava:guava:32.0.1-android" guava = "com.google.guava:guava:32.1.2-android"
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" } paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" } paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }

View file

@ -19,7 +19,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" } okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" } okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
okio = "com.squareup.okio:okio:3.4.0" okio = "com.squareup.okio:okio:3.5.0"
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2" conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"

View file

@ -8,6 +8,7 @@
<!-- Models --> <!-- Models -->
<string name="name">Name</string> <string name="name">Name</string>
<string name="categories">Categories</string>
<string name="manga">Manga</string> <string name="manga">Manga</string>
<string name="chapters">Chapters</string> <string name="chapters">Chapters</string>
<string name="track">Tracking</string> <string name="track">Tracking</string>
@ -260,16 +261,6 @@
<string name="pref_update_only_started">That haven\'t been started</string> <string name="pref_update_only_started">That haven\'t been started</string>
<string name="pref_update_only_in_release_period">Outside expected release period</string> <string name="pref_update_only_in_release_period">Outside expected release period</string>
<string name="pref_update_release_grace_period">Expected manga release grace period</string>
<plurals name="pref_update_release_leading_days">
<item quantity="one">%d day before</item>
<item quantity="other">%d days before</item>
</plurals>
<plurals name="pref_update_release_following_days">
<item quantity="one">%d day after</item>
<item quantity="other">%d days after</item>
</plurals>
<string name="pref_update_release_grace_period_info">A low grace period is recommended to minimize stress on sources. The more checks for an entry that are missed, the longer the interval in between checks will be with a maximum of 28 days.</string>
<string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string> <string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string> <string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
<string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string> <string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
@ -466,6 +457,7 @@
<string name="enhanced_services_not_installed">Available but source not installed: %s</string> <string name="enhanced_services_not_installed">Available but source not installed: %s</string>
<string name="enhanced_tracking_info">Services that provide enhanced features for specific sources. Entries are automatically tracked when added to your library.</string> <string name="enhanced_tracking_info">Services that provide enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
<string name="action_track">Track</string> <string name="action_track">Track</string>
<string name="track_activity_name">Tracking login</string>
<!-- Browse section --> <!-- Browse section -->
<string name="pref_hide_in_library_items">Hide entries already in library</string> <string name="pref_hide_in_library_items">Hide entries already in library</string>
@ -624,10 +616,6 @@
<item quantity="one">1 day</item> <item quantity="one">1 day</item>
<item quantity="other">%d days</item> <item quantity="other">%d days</item>
</plurals> </plurals>
<plurals name="range_interval_day">
<item quantity="one">%1$d - %2$d day</item>
<item quantity="other">%1$d - %2$d days</item>
</plurals>
<!-- Item info --> <!-- Item info -->
<plurals name="missing_items"> <plurals name="missing_items">

View file

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animesource.model package eu.kanade.tachiyomi.animesource.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
import java.io.Serializable import java.io.Serializable
interface SAnime : Serializable { interface SAnime : Serializable {

View file

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.animesource.model package eu.kanade.tachiyomi.animesource.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.model.UpdateStrategy
class SAnimeImpl : SAnime { class SAnimeImpl : SAnime {

View file

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.model
/** /**
* Define the update strategy for a single [SManga]. * Define the update strategy for a single SManga or SAnime.
* The strategy used will only take effect on the library update. * The strategy used will only take effect on the library update.
* *
* @since extensions-lib 1.4 * @since extensions-lib 1.4

View file

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import eu.kanade.tachiyomi.model.UpdateStrategy
import java.io.Serializable import java.io.Serializable
interface SManga : Serializable { interface SManga : Serializable {

View file

@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import eu.kanade.tachiyomi.model.UpdateStrategy
class SMangaImpl : SManga { class SMangaImpl : SManga {
override lateinit var url: String override lateinit var url: String