mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 20:27:06 +03:00
parent
e29dc62837
commit
fa7b8427a2
86 changed files with 2504 additions and 2210 deletions
|
@ -85,6 +85,7 @@ android {
|
|||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
isDebuggable = false
|
||||
isProfileable = true
|
||||
versionNameSuffix = "-benchmark"
|
||||
applicationIdSuffix = ".benchmark"
|
||||
}
|
||||
|
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
|
@ -11,6 +11,7 @@
|
|||
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||
-keep,allowoptimization class kotlinx.coroutines.** { 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 okio.** { public protected *; }
|
||||
-keep,allowoptimization class rx.** { public protected *; }
|
||||
|
|
|
@ -41,11 +41,6 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Tachiyomi">
|
||||
|
||||
<!-- enable profiling by macrobenchmark -->
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="q" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
|
@ -173,8 +168,8 @@
|
|||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||
android:label="Anilist"
|
||||
android:name=".ui.setting.track.TrackLoginActivity"
|
||||
android:label="@string/track_activity_name"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
@ -182,69 +177,21 @@
|
|||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="anilist-auth"
|
||||
android:scheme="tachiyomi" />
|
||||
<data android:host="anilist-auth"/>
|
||||
<data android:host="bangumi-auth"/>
|
||||
<data android:host="myanimelist-auth"/>
|
||||
<data android:host="shikimori-auth"/>
|
||||
|
||||
<data android:scheme="tachiyomi"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||
android:label="MyAnimeList"
|
||||
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="myanimelist-auth"
|
||||
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" />
|
||||
<data android:host="simkl-auth"/>
|
||||
<data android:scheme="aniyomi"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
@ -292,10 +239,12 @@
|
|||
android:name=".data.updater.AppUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".extension.manga.util.MangaExtensionInstallService"
|
||||
<service
|
||||
android:name=".extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".extension.anime.util.AnimeExtensionInstallService"
|
||||
<service
|
||||
android:name=".extension.anime.util.AnimeExtensionInstallService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,6 @@ import tachiyomi.domain.entries.anime.interactor.SetAnimeFetchInterval
|
|||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import tachiyomi.source.local.entries.anime.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -79,16 +78,12 @@ class UpdateAnime(
|
|||
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
anime: Anime,
|
||||
episodes: List<Episode>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
fetchRange: Pair<Long, Long> = setAnimeFetchInterval.getCurrent(zonedDateTime),
|
||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = setAnimeFetchInterval.getWindow(dateTime),
|
||||
): Boolean {
|
||||
val updateAnime = setAnimeFetchInterval.update(anime, episodes, zonedDateTime, fetchRange)
|
||||
return if (updateAnime != null) {
|
||||
animeRepository.updateAnime(updateAnime)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
return setAnimeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
|
||||
?.let { animeRepository.updateAnime(it) }
|
||||
?: false
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {
|
||||
|
|
|
@ -7,7 +7,6 @@ import tachiyomi.domain.entries.manga.interactor.SetMangaFetchInterval
|
|||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import tachiyomi.source.local.entries.manga.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -79,16 +78,12 @@ class UpdateManga(
|
|||
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
fetchRange: Pair<Long, Long> = setMangaFetchInterval.getCurrent(zonedDateTime),
|
||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = setMangaFetchInterval.getWindow(dateTime),
|
||||
): Boolean {
|
||||
val updatedManga = setMangaFetchInterval.update(manga, chapters, zonedDateTime, fetchRange)
|
||||
return if (updatedManga != null) {
|
||||
mangaRepository.updateManga(updatedManga)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
return setMangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||
?.let { mangaRepository.updateManga(it) }
|
||||
?: false
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||
|
|
|
@ -50,13 +50,14 @@ class SyncChaptersWithSource(
|
|||
manga: Manga,
|
||||
source: MangaSource,
|
||||
manualFetch: Boolean = false,
|
||||
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
||||
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
||||
): List<Chapter> {
|
||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||
throw NoChaptersException()
|
||||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
|
||||
val sourceChapters = rawSourceChapters
|
||||
.distinctBy { it.url }
|
||||
.mapIndexed { i, sChapter ->
|
||||
|
@ -138,12 +139,11 @@ class SyncChaptersWithSource(
|
|||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
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(
|
||||
manga,
|
||||
dbChapters,
|
||||
zoneDateTime,
|
||||
fetchRange,
|
||||
now,
|
||||
fetchWindow,
|
||||
)
|
||||
}
|
||||
return emptyList()
|
||||
|
@ -200,8 +200,7 @@ class SyncChaptersWithSource(
|
|||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
val newChapters = chapterRepository.getChapterByMangaId(manga.id)
|
||||
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||
|
||||
// Set this manga as updated since chapters were changed
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
|
|
|
@ -50,13 +50,14 @@ class SyncEpisodesWithSource(
|
|||
anime: Anime,
|
||||
source: AnimeSource,
|
||||
manualFetch: Boolean = false,
|
||||
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
||||
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
||||
): List<Episode> {
|
||||
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
|
||||
throw NoEpisodesException()
|
||||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
|
||||
val sourceEpisodes = rawSourceEpisodes
|
||||
.distinctBy { it.url }
|
||||
.mapIndexed { i, sEpisode ->
|
||||
|
@ -138,12 +139,11 @@ class SyncEpisodesWithSource(
|
|||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
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(
|
||||
anime,
|
||||
dbEpisodes,
|
||||
zoneDateTime,
|
||||
fetchRange,
|
||||
now,
|
||||
fetchWindow,
|
||||
)
|
||||
}
|
||||
return emptyList()
|
||||
|
@ -200,8 +200,7 @@ class SyncEpisodesWithSource(
|
|||
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
||||
updateEpisode.awaitAll(episodeUpdates)
|
||||
}
|
||||
val newChapters = episodeRepository.getEpisodeByAnimeId(anime.id)
|
||||
updateAnime.awaitUpdateFetchInterval(anime, newChapters, zoneDateTime, fetchRange)
|
||||
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
|
||||
|
||||
// Set this anime as updated since episodes were changed
|
||||
// Note that last_update actually represents last time the episode list changed at all
|
||||
|
|
|
@ -16,7 +16,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.entries.anime.interactor.MAX_GRACE_PERIOD
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
|
||||
@Composable
|
||||
|
@ -58,7 +57,7 @@ fun SetIntervalDialog(
|
|||
onDismissRequest: () -> Unit,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
) {
|
||||
var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
|
||||
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -69,7 +68,7 @@ fun SetIntervalDialog(
|
|||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||
val items = (0..MAX_GRACE_PERIOD).map {
|
||||
val items = (0..28).map {
|
||||
if (it == 0) {
|
||||
stringResource(R.string.label_default)
|
||||
} else {
|
||||
|
@ -79,8 +78,8 @@ fun SetIntervalDialog(
|
|||
WheelTextPicker(
|
||||
size = size,
|
||||
items = items,
|
||||
startIndex = intervalValue,
|
||||
onSelectionChanged = { intervalValue = it },
|
||||
startIndex = selectedInterval,
|
||||
onSelectionChanged = { selectedInterval = it },
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -91,7 +90,7 @@ fun SetIntervalDialog(
|
|||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onValueChanged(intervalValue)
|
||||
onValueChanged(selectedInterval)
|
||||
onDismissRequest()
|
||||
},) {
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
|
|
|
@ -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.entries.anime.AnimeScreenModel
|
||||
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.system.copyToClipboard
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -98,7 +97,7 @@ import java.util.concurrent.TimeUnit
|
|||
fun AnimeScreen(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fetchInterval: FetchAnimeInterval?,
|
||||
fetchInterval: Int?,
|
||||
dateFormat: DateFormat,
|
||||
isTabletUi: Boolean,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -247,7 +246,7 @@ private fun AnimeScreenSmallImpl(
|
|||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: FetchAnimeInterval?,
|
||||
fetchInterval: Int?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
showNextEpisodeAirTime: Boolean,
|
||||
|
@ -518,7 +517,7 @@ fun AnimeScreenLargeImpl(
|
|||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: FetchAnimeInterval?,
|
||||
fetchInterval: Int?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
showNextEpisodeAirTime: Boolean,
|
||||
|
|
|
@ -80,13 +80,13 @@ import eu.kanade.presentation.entries.DotSeparatorText
|
|||
import eu.kanade.presentation.entries.ItemCover
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.ui.entries.anime.FetchAnimeInterval
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.presentation.core.components.material.TextButton
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
@ -168,7 +168,7 @@ fun AnimeActionRow(
|
|||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
fetchInterval: FetchAnimeInterval?,
|
||||
fetchInterval: Int?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
|
@ -192,14 +192,8 @@ fun AnimeActionRow(
|
|||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||
val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays)
|
||||
AnimeActionButton(
|
||||
title =
|
||||
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)
|
||||
},
|
||||
title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
|
|
|
@ -68,7 +68,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
|||
import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
|
||||
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.FetchMangaInterval
|
||||
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
|
||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
|
@ -91,7 +90,7 @@ import java.util.Date
|
|||
fun MangaScreen(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fetchInterval: FetchMangaInterval?,
|
||||
fetchInterval: Int?,
|
||||
dateFormat: DateFormat,
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -230,7 +229,7 @@ private fun MangaScreenSmallImpl(
|
|||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: FetchMangaInterval?,
|
||||
fetchInterval: Int?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -466,7 +465,7 @@ fun MangaScreenLargeImpl(
|
|||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: FetchMangaInterval?,
|
||||
fetchInterval: Int?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
|
|
@ -80,13 +80,13 @@ import eu.kanade.presentation.entries.DotSeparatorText
|
|||
import eu.kanade.presentation.entries.ItemCover
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.entries.manga.FetchMangaInterval
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.TextButton
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
@ -168,7 +168,7 @@ fun MangaActionRow(
|
|||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
fetchInterval: FetchMangaInterval?,
|
||||
fetchInterval: Int?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
|
@ -192,14 +192,8 @@ fun MangaActionRow(
|
|||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||
val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays)
|
||||
MangaActionButton(
|
||||
title =
|
||||
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)
|
||||
},
|
||||
title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
|
|
|
@ -1,33 +1,18 @@
|
|||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
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.ReadOnlyComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.res.pluralStringResource
|
||||
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.core.content.ContextCompat
|
||||
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_VIEWED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -181,13 +165,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
|
||||
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
|
||||
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
|
||||
val libraryUpdateDeviceRestrictionPref = libraryPreferences.libraryUpdateDeviceRestriction()
|
||||
val libraryUpdateMangaRestrictionPref = libraryPreferences.libraryUpdateItemRestriction()
|
||||
|
||||
val animelibUpdateCategoriesPref = libraryPreferences.animeLibraryUpdateCategories()
|
||||
val animelibUpdateCategoriesExcludePref =
|
||||
libraryPreferences.animeLibraryUpdateCategoriesExclude()
|
||||
val libraryUpdateAnimeRestriction by libraryUpdateMangaRestrictionPref.collectAsState()
|
||||
|
||||
val includedAnime by animelibUpdateCategoriesPref.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 libraryUpdateCategoriesExcludePref =
|
||||
libraryPreferences.mangaLibraryUpdateCategoriesExclude()
|
||||
val libraryUpdateMangaRestriction by libraryUpdateMangaRestrictionPref.collectAsState()
|
||||
|
||||
val includedManga by libraryUpdateCategoriesPref.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(
|
||||
title = stringResource(R.string.pref_category_library_update),
|
||||
preferenceItems = listOfNotNull(
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = libraryUpdateIntervalPref,
|
||||
title = stringResource(R.string.pref_library_update_interval),
|
||||
|
@ -292,7 +241,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
},
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryUpdateDeviceRestrictionPref,
|
||||
pref = libraryPreferences.libraryUpdateDeviceRestriction(),
|
||||
enabled = libraryUpdateInterval > 0,
|
||||
title = stringResource(R.string.pref_library_update_restriction),
|
||||
subtitle = stringResource(R.string.restrictions),
|
||||
|
@ -341,7 +290,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
||||
),
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryUpdateMangaRestrictionPref,
|
||||
pref = libraryPreferences.libraryUpdateItemRestriction(),
|
||||
title = stringResource(R.string.pref_library_update_manga_restriction),
|
||||
entries = mapOf(
|
||||
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),
|
||||
),
|
||||
),
|
||||
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(
|
||||
pref = libraryPreferences.newShowUpdatesCount(),
|
||||
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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -53,6 +53,15 @@ fun ChapterNavigator(
|
|||
val layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
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
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
Row(
|
||||
|
@ -61,14 +70,6 @@ fun ChapterNavigator(
|
|||
.padding(horizontal = horizontalPadding),
|
||||
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(
|
||||
enabled = if (isRtl) enabledNext else enabledPrevious,
|
||||
onClick = if (isRtl) onNextChapter else onPreviousChapter,
|
||||
|
|
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.Row
|
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,11 +42,12 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||
pref = screenModel.preferences.fullscreen(),
|
||||
)
|
||||
|
||||
// TODO: hide if there's no cutout
|
||||
CheckboxItem(
|
||||
label = stringResource(R.string.pref_cutout_short),
|
||||
pref = screenModel.preferences.cutoutShort(),
|
||||
)
|
||||
if (screenModel.hasDisplayCutout) {
|
||||
CheckboxItem(
|
||||
label = stringResource(R.string.pref_cutout_short),
|
||||
pref = screenModel.preferences.cutoutShort(),
|
||||
)
|
||||
}
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(R.string.pref_keep_screen_on),
|
||||
|
|
|
@ -60,9 +60,9 @@ class BackupRestorer(
|
|||
private val episodeRepository: EpisodeRepository = Injekt.get()
|
||||
private val setAnimeFetchInterval: SetAnimeFetchInterval = Injekt.get()
|
||||
|
||||
private var zonedDateTime = ZonedDateTime.now()
|
||||
private var currentMangaFetchInterval = setMangaFetchInterval.getCurrent(zonedDateTime)
|
||||
private var currentAnimeFetchInterval = setAnimeFetchInterval.getCurrent(zonedDateTime)
|
||||
private var now = ZonedDateTime.now()
|
||||
private var currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
|
||||
private var currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
|
||||
|
||||
private var backupManager = BackupManager(context)
|
||||
|
||||
|
@ -140,9 +140,9 @@ class BackupRestorer(
|
|||
val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
|
||||
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
||||
|
||||
zonedDateTime = ZonedDateTime.now()
|
||||
currentMangaFetchInterval = setMangaFetchInterval.getCurrent(zonedDateTime)
|
||||
currentAnimeFetchInterval = setAnimeFetchInterval.getCurrent(zonedDateTime)
|
||||
now = ZonedDateTime.now()
|
||||
currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
|
||||
currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
|
||||
|
||||
return coroutineScope {
|
||||
// Restore individual manga
|
||||
|
@ -216,8 +216,7 @@ class BackupRestorer(
|
|||
// Fetch rest of manga information
|
||||
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
|
||||
}
|
||||
val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id)
|
||||
updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentMangaFetchInterval)
|
||||
updateManga.awaitUpdateFetchInterval(restoredManga, now, currentMangaFetchWindow)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
|
@ -291,8 +290,7 @@ class BackupRestorer(
|
|||
// Fetch rest of anime information
|
||||
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
|
||||
}
|
||||
val updatedEpisodes = episodeRepository.getEpisodeByAnimeId(restoredAnime.id)
|
||||
updateAnime.awaitUpdateFetchInterval(restoredAnime, updatedEpisodes, zonedDateTime, currentAnimeFetchInterval)
|
||||
updateAnime.awaitUpdateFetchInterval(restoredAnime, now, currentAnimeFetchWindow)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
|
||||
errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.protobuf.ProtoNumber
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
|
|
@ -30,7 +30,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
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.shouldDownloadNewEpisodes
|
||||
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 hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val fetchInterval = setAnimeFetchInterval.getCurrent(now)
|
||||
val higherLimit = fetchInterval.second
|
||||
val fetchWindow = setAnimeFetchInterval.getWindow(ZonedDateTime.now())
|
||||
|
||||
coroutineScope {
|
||||
animeToUpdate.groupBy { it.anime.source }.values
|
||||
|
@ -255,8 +252,8 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
anime,
|
||||
) {
|
||||
when {
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > higherLimit ->
|
||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||
anime.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_always_update))
|
||||
|
||||
ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.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 ->
|
||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_started))
|
||||
|
||||
anime.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_always_update))
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > fetchWindow.second ->
|
||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||
|
||||
else -> {
|
||||
try {
|
||||
val newEpisodes = updateAnime(anime, now, fetchInterval)
|
||||
val newEpisodes = updateAnime(anime, fetchWindow)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
if (newEpisodes.isNotEmpty()) {
|
||||
|
@ -328,6 +325,13 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -344,7 +348,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
* @param anime the anime to update.
|
||||
* @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)
|
||||
|
||||
// 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
|
||||
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() {
|
||||
|
|
|
@ -28,9 +28,9 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
|||
import eu.kanade.tachiyomi.data.track.EnhancedMangaTrackService
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
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.shouldDownloadNewChapters
|
||||
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 hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val fetchInterval = setMangaFetchInterval.getCurrent(now)
|
||||
val higherLimit = fetchInterval.second
|
||||
val fetchWindow = setMangaFetchInterval.getWindow(ZonedDateTime.now())
|
||||
|
||||
coroutineScope {
|
||||
mangaToUpdate.groupBy { it.manga.source }.values
|
||||
|
@ -255,8 +252,8 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
manga,
|
||||
) {
|
||||
when {
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit ->
|
||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
|
||||
|
||||
ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.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 ->
|
||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
|
||||
|
||||
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > fetchWindow.second ->
|
||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||
|
||||
else -> {
|
||||
try {
|
||||
val newChapters = updateManga(manga, now, fetchInterval)
|
||||
val newChapters = updateManga(manga, fetchWindow)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
|
@ -328,6 +325,13 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -344,7 +348,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
* @param manga the manga to update.
|
||||
* @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)
|
||||
|
||||
// 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
|
||||
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() {
|
||||
|
|
|
@ -29,7 +29,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||
import tachiyomi.core.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||
|
||||
|
@ -37,7 +37,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
|
||||
private val authClient = client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES)
|
||||
.rateLimit(permits = 85, period = 1.minutes)
|
||||
.build()
|
||||
|
||||
suspend fun addLibManga(track: MangaTrack): MangaTrack {
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.core.app.NotificationCompat
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.notify
|
||||
|
||||
class ExtensionUpdateNotifier(private val context: Context) {
|
||||
|
@ -29,4 +30,8 @@ class ExtensionUpdateNotifier(private val context: Context) {
|
|||
setAutoCancel(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
context.cancelNotification(Notifications.ID_UPDATES_TO_EXTS)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
|
||||
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
|
||||
* 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
|
||||
* unsubscribed before its completion.
|
||||
*
|
||||
|
@ -356,6 +357,10 @@ class AnimeExtensionManager(
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
|
||||
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
|
||||
* 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
|
||||
* unsubscribed before its completion.
|
||||
*
|
||||
|
@ -356,6 +357,10 @@ class MangaExtensionManager(
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,14 +157,14 @@ class AnimeExtensionsScreenModel(
|
|||
extensionManager.cancelInstallUpdateExtension(extension)
|
||||
}
|
||||
|
||||
private fun removeDownloadState(extension: AnimeExtension) {
|
||||
_currentDownloads.update { it - extension.pkgName }
|
||||
}
|
||||
|
||||
private fun addDownloadState(extension: AnimeExtension, installStep: 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) =
|
||||
this
|
||||
.onEach { installStep -> addDownloadState(extension, installStep) }
|
||||
|
|
|
@ -4,28 +4,36 @@ import eu.kanade.domain.entries.anime.model.hasCustomCover
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.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 {
|
||||
|
||||
private const val EPISODES = 0b00001
|
||||
private const val CATEGORIES = 0b00010
|
||||
private const val TRACK = 0b00100
|
||||
private const val CUSTOM_COVER = 0b01000
|
||||
private const val DELETE_DOWNLOADED = 0b10000
|
||||
|
||||
private val coverCache: AnimeCoverCache by injectLazy()
|
||||
private val getTracks: GetAnimeTracks = Injekt.get()
|
||||
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 {
|
||||
return value and EPISODES != 0
|
||||
}
|
||||
|
@ -34,10 +42,6 @@ object AnimeMigrationFlags {
|
|||
return value and CATEGORIES != 0
|
||||
}
|
||||
|
||||
fun hasTracks(value: Int): Boolean {
|
||||
return value and TRACK != 0
|
||||
}
|
||||
|
||||
fun hasCustomCover(value: Int): Boolean {
|
||||
return value and CUSTOM_COVER != 0
|
||||
}
|
||||
|
@ -46,35 +50,32 @@ object AnimeMigrationFlags {
|
|||
return value and DELETE_DOWNLOADED != 0
|
||||
}
|
||||
|
||||
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
||||
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
||||
}
|
||||
/** Returns information about applicable flags with default selections. */
|
||||
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 (runBlocking { getTracks.await(anime.id) }.isNotEmpty()) {
|
||||
titles.add(R.string.track)
|
||||
enableFlags.add(TRACK)
|
||||
}
|
||||
|
||||
if (anime.hasCustomCover(coverCache)) {
|
||||
titles.add(R.string.custom_cover)
|
||||
enableFlags.add(CUSTOM_COVER)
|
||||
flags += AnimeMigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, R.string.custom_cover)
|
||||
}
|
||||
if (downloadCache.getDownloadCount(anime) > 0) {
|
||||
titles.add(R.string.delete_downloaded)
|
||||
enableFlags.add(DELETE_DOWNLOADED)
|
||||
flags += AnimeMigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, R.string.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,15 +19,14 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
||||
|
@ -74,15 +73,8 @@ internal fun MigrateAnimeDialog(
|
|||
val scope = rememberCoroutineScope()
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
val activeFlags = remember { AnimeMigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) }
|
||||
val items = remember {
|
||||
AnimeMigrationFlags.titles(oldAnime)
|
||||
.map { context.getString(it) }
|
||||
.toList()
|
||||
}
|
||||
val selected = remember {
|
||||
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
|
||||
}
|
||||
val flags = remember { AnimeMigrationFlags.getFlags(oldAnime, screenModel.migrateFlags.get()) }
|
||||
val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() }
|
||||
|
||||
if (state.isMigrating) {
|
||||
LoadingScreen(
|
||||
|
@ -99,18 +91,16 @@ internal fun MigrateAnimeDialog(
|
|||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
items.forEachIndexed { index, title ->
|
||||
val onChange: () -> Unit = {
|
||||
selected[index] = !selected[index]
|
||||
}
|
||||
flags.forEachIndexed { index, flag ->
|
||||
val onChange = { selectedFlags[index] = !selectedFlags[index] }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onChange),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(checked = selected[index], onCheckedChange = { onChange() })
|
||||
Text(text = title)
|
||||
Checkbox(checked = selectedFlags[index], onCheckedChange = { onChange() })
|
||||
Text(text = context.getString(flag.titleId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +123,12 @@ internal fun MigrateAnimeDialog(
|
|||
TextButton(
|
||||
onClick = {
|
||||
scope.launchIO {
|
||||
screenModel.migrateAnime(oldAnime, newAnime, false)
|
||||
screenModel.migrateAnime(
|
||||
oldAnime,
|
||||
newAnime,
|
||||
false,
|
||||
AnimeMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
||||
)
|
||||
withUIContext { onPopScreen() }
|
||||
}
|
||||
},
|
||||
|
@ -143,12 +138,13 @@ internal fun MigrateAnimeDialog(
|
|||
TextButton(
|
||||
onClick = {
|
||||
scope.launchIO {
|
||||
val selectedIndices = mutableListOf<Int>()
|
||||
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) }
|
||||
val newValue =
|
||||
AnimeMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
|
||||
screenModel.migrateFlags.set(newValue)
|
||||
screenModel.migrateAnime(oldAnime, newAnime, true)
|
||||
screenModel.migrateAnime(
|
||||
oldAnime,
|
||||
newAnime,
|
||||
true,
|
||||
AnimeMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
||||
)
|
||||
|
||||
withUIContext { onPopScreen() }
|
||||
}
|
||||
},
|
||||
|
@ -184,7 +180,13 @@ internal class MigrateAnimeDialogScreenModel(
|
|||
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 prevSource = sourceManager.get(oldAnime.source)
|
||||
|
||||
|
@ -200,6 +202,7 @@ internal class MigrateAnimeDialogScreenModel(
|
|||
newAnime = newAnime,
|
||||
sourceEpisodes = episodes,
|
||||
replace = replace,
|
||||
flags = flags,
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
|
||||
|
@ -215,12 +218,10 @@ internal class MigrateAnimeDialogScreenModel(
|
|||
newAnime: Anime,
|
||||
sourceEpisodes: List<SEpisode>,
|
||||
replace: Boolean,
|
||||
flags: Int,
|
||||
) {
|
||||
val flags = migrateFlags.get()
|
||||
|
||||
val migrateEpisodes = AnimeMigrationFlags.hasEpisodes(flags)
|
||||
val migrateCategories = AnimeMigrationFlags.hasCategories(flags)
|
||||
val migrateTracks = AnimeMigrationFlags.hasTracks(flags)
|
||||
val migrateCustomCover = AnimeMigrationFlags.hasCustomCover(flags)
|
||||
val deleteDownloaded = AnimeMigrationFlags.hasDeleteDownloaded(flags)
|
||||
|
||||
|
@ -271,21 +272,20 @@ internal class MigrateAnimeDialogScreenModel(
|
|||
}
|
||||
|
||||
// Update track
|
||||
if (migrateTracks) {
|
||||
val tracks = getTracks.await(oldAnime.id).mapNotNull { track ->
|
||||
val updatedTrack = track.copy(animeId = newAnime.id)
|
||||
getTracks.await(oldAnime.id).mapNotNull { track ->
|
||||
val updatedTrack = track.copy(animeId = newAnime.id)
|
||||
|
||||
val service = enhancedServices
|
||||
.firstOrNull { it.isTrackFrom(updatedTrack, oldAnime, oldSource) }
|
||||
val service = enhancedServices
|
||||
.firstOrNull { it.isTrackFrom(updatedTrack, oldAnime, oldSource) }
|
||||
|
||||
if (service != null) {
|
||||
service.migrateTrack(updatedTrack, newAnime, newSource)
|
||||
} else {
|
||||
updatedTrack
|
||||
}
|
||||
if (service != null) {
|
||||
service.migrateTrack(updatedTrack, newAnime, newSource)
|
||||
} else {
|
||||
updatedTrack
|
||||
}
|
||||
insertTrack.awaitAll(tracks)
|
||||
}
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { insertTrack.awaitAll(it) }
|
||||
|
||||
// Delete downloaded
|
||||
if (deleteDownloaded) {
|
||||
|
|
|
@ -158,14 +158,14 @@ class MangaExtensionsScreenModel(
|
|||
extensionManager.cancelInstallUpdateExtension(extension)
|
||||
}
|
||||
|
||||
private fun removeDownloadState(extension: MangaExtension) {
|
||||
_currentDownloads.update { it - extension.pkgName }
|
||||
}
|
||||
|
||||
private fun addDownloadState(extension: MangaExtension, installStep: 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) =
|
||||
this
|
||||
.onEach { installStep -> addDownloadState(extension, installStep) }
|
||||
|
|
|
@ -4,28 +4,36 @@ import eu.kanade.domain.entries.manga.model.hasCustomCover
|
|||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.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 {
|
||||
|
||||
private const val CHAPTERS = 0b00001
|
||||
private const val CATEGORIES = 0b00010
|
||||
private const val TRACK = 0b00100
|
||||
private const val CUSTOM_COVER = 0b01000
|
||||
private const val DELETE_DOWNLOADED = 0b10000
|
||||
|
||||
private val coverCache: MangaCoverCache by injectLazy()
|
||||
private val getTracks: GetMangaTracks = Injekt.get()
|
||||
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 {
|
||||
return value and CHAPTERS != 0
|
||||
}
|
||||
|
@ -34,10 +42,6 @@ object MangaMigrationFlags {
|
|||
return value and CATEGORIES != 0
|
||||
}
|
||||
|
||||
fun hasTracks(value: Int): Boolean {
|
||||
return value and TRACK != 0
|
||||
}
|
||||
|
||||
fun hasCustomCover(value: Int): Boolean {
|
||||
return value and CUSTOM_COVER != 0
|
||||
}
|
||||
|
@ -46,34 +50,32 @@ object MangaMigrationFlags {
|
|||
return value and DELETE_DOWNLOADED != 0
|
||||
}
|
||||
|
||||
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
||||
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
||||
}
|
||||
/** Returns information about applicable flags with default selections. */
|
||||
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 (runBlocking { getTracks.await(manga.id) }.isNotEmpty()) {
|
||||
titles.add(R.string.track)
|
||||
enableFlags.add(TRACK)
|
||||
}
|
||||
if (manga.hasCustomCover(coverCache)) {
|
||||
titles.add(R.string.custom_cover)
|
||||
enableFlags.add(CUSTOM_COVER)
|
||||
flags += MangaMigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, R.string.custom_cover)
|
||||
}
|
||||
if (downloadCache.getDownloadCount(manga) > 0) {
|
||||
titles.add(R.string.delete_downloaded)
|
||||
enableFlags.add(DELETE_DOWNLOADED)
|
||||
flags += MangaMigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, R.string.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,15 +19,14 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
||||
|
@ -74,15 +73,8 @@ internal fun MigrateMangaDialog(
|
|||
val scope = rememberCoroutineScope()
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
val activeFlags = remember { MangaMigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) }
|
||||
val items = remember {
|
||||
MangaMigrationFlags.titles(oldManga)
|
||||
.map { context.getString(it) }
|
||||
.toList()
|
||||
}
|
||||
val selected = remember {
|
||||
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
|
||||
}
|
||||
val flags = remember { MangaMigrationFlags.getFlags(oldManga, screenModel.migrateFlags.get()) }
|
||||
val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() }
|
||||
|
||||
if (state.isMigrating) {
|
||||
LoadingScreen(
|
||||
|
@ -99,18 +91,16 @@ internal fun MigrateMangaDialog(
|
|||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
items.forEachIndexed { index, title ->
|
||||
val onChange: () -> Unit = {
|
||||
selected[index] = !selected[index]
|
||||
}
|
||||
flags.forEachIndexed { index, flag ->
|
||||
val onChange = { selectedFlags[index] = !selectedFlags[index] }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onChange),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Checkbox(checked = selected[index], onCheckedChange = { onChange() })
|
||||
Text(text = title)
|
||||
Checkbox(checked = selectedFlags[index], onCheckedChange = { onChange() })
|
||||
Text(text = context.getString(flag.titleId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +123,12 @@ internal fun MigrateMangaDialog(
|
|||
TextButton(
|
||||
onClick = {
|
||||
scope.launchIO {
|
||||
screenModel.migrateManga(oldManga, newManga, false)
|
||||
screenModel.migrateManga(
|
||||
oldManga,
|
||||
newManga,
|
||||
false,
|
||||
MangaMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
||||
)
|
||||
withUIContext { onPopScreen() }
|
||||
}
|
||||
},
|
||||
|
@ -143,12 +138,13 @@ internal fun MigrateMangaDialog(
|
|||
TextButton(
|
||||
onClick = {
|
||||
scope.launchIO {
|
||||
val selectedIndices = mutableListOf<Int>()
|
||||
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) }
|
||||
val newValue =
|
||||
MangaMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
|
||||
screenModel.migrateFlags.set(newValue)
|
||||
screenModel.migrateManga(oldManga, newManga, true)
|
||||
screenModel.migrateManga(
|
||||
oldManga,
|
||||
newManga,
|
||||
true,
|
||||
MangaMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
||||
)
|
||||
|
||||
withUIContext { onPopScreen() }
|
||||
}
|
||||
},
|
||||
|
@ -184,7 +180,13 @@ internal class MigrateMangaDialogScreenModel(
|
|||
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 prevSource = sourceManager.get(oldManga.source)
|
||||
|
||||
|
@ -200,6 +202,7 @@ internal class MigrateMangaDialogScreenModel(
|
|||
newManga = newManga,
|
||||
sourceChapters = chapters,
|
||||
replace = replace,
|
||||
flags = flags,
|
||||
)
|
||||
} catch (_: Throwable) {
|
||||
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
|
||||
|
@ -215,12 +218,10 @@ internal class MigrateMangaDialogScreenModel(
|
|||
newManga: Manga,
|
||||
sourceChapters: List<SChapter>,
|
||||
replace: Boolean,
|
||||
flags: Int,
|
||||
) {
|
||||
val flags = migrateFlags.get()
|
||||
|
||||
val migrateChapters = MangaMigrationFlags.hasChapters(flags)
|
||||
val migrateCategories = MangaMigrationFlags.hasCategories(flags)
|
||||
val migrateTracks = MangaMigrationFlags.hasTracks(flags)
|
||||
val migrateCustomCover = MangaMigrationFlags.hasCustomCover(flags)
|
||||
val deleteDownloaded = MangaMigrationFlags.hasDeleteDownloaded(flags)
|
||||
|
||||
|
@ -271,21 +272,20 @@ internal class MigrateMangaDialogScreenModel(
|
|||
}
|
||||
|
||||
// Update track
|
||||
if (migrateTracks) {
|
||||
val tracks = getTracks.await(oldManga.id).mapNotNull { track ->
|
||||
val updatedTrack = track.copy(mangaId = newManga.id)
|
||||
getTracks.await(oldManga.id).mapNotNull { track ->
|
||||
val updatedTrack = track.copy(mangaId = newManga.id)
|
||||
|
||||
val service = enhancedServices
|
||||
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
|
||||
val service = enhancedServices
|
||||
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
|
||||
|
||||
if (service != null) {
|
||||
service.migrateTrack(updatedTrack, newManga, newSource)
|
||||
} else {
|
||||
updatedTrack
|
||||
}
|
||||
if (service != null) {
|
||||
service.migrateTrack(updatedTrack, newManga, newSource)
|
||||
} else {
|
||||
updatedTrack
|
||||
}
|
||||
insertTrack.awaitAll(tracks)
|
||||
}
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { insertTrack.awaitAll(it) }
|
||||
|
||||
// Delete downloaded
|
||||
if (deleteDownloaded) {
|
||||
|
|
|
@ -89,13 +89,6 @@ class AnimeScreen(
|
|||
|
||||
val successState = state as AnimeScreenModel.State.Success
|
||||
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) {
|
||||
if (isAnimeHttpSource) {
|
||||
|
@ -113,7 +106,7 @@ class AnimeScreen(
|
|||
state = successState,
|
||||
snackbarHostState = screenModel.snackbarHostState,
|
||||
dateFormat = screenModel.dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
fetchInterval = successState.anime.fetchInterval,
|
||||
isTabletUi = isTabletUi(),
|
||||
episodeSwipeStartAction = screenModel.episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = screenModel.episodeSwipeEndAction,
|
||||
|
@ -244,7 +237,7 @@ class AnimeScreen(
|
|||
}
|
||||
is AnimeScreenModel.Dialog.SetAnimeFetchInterval -> {
|
||||
SetIntervalDialog(
|
||||
interval = if (dialog.anime.fetchInterval < 0) -dialog.anime.fetchInterval else 0,
|
||||
interval = dialog.anime.fetchInterval,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onValueChanged = { screenModel.setFetchInterval(dialog.anime, it) },
|
||||
)
|
||||
|
|
|
@ -135,8 +135,6 @@ class AnimeScreenModel(
|
|||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().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 selectedEpisodeIds: HashSet<Long> = HashSet()
|
||||
|
@ -377,20 +375,14 @@ class AnimeScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun setFetchInterval(anime: Anime, newInterval: 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
|
||||
}
|
||||
fun setFetchInterval(anime: Anime, interval: Int) {
|
||||
coroutineScope.launchIO {
|
||||
updateAnime.awaitUpdateFetchInterval(
|
||||
anime.copy(fetchInterval = interval),
|
||||
successState?.episodes?.map { it.episode }.orEmpty(),
|
||||
// Custom intervals are negative
|
||||
anime.copy(fetchInterval = -interval),
|
||||
)
|
||||
val newAnime = animeRepository.getAnimeById(animeId)
|
||||
updateSuccessState { it.copy(anime = newAnime) }
|
||||
val updatedAnime = animeRepository.getAnimeById(anime.id)
|
||||
updateSuccessState { it.copy(anime = updatedAnime) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1105,10 +1097,3 @@ data class EpisodeItem(
|
|||
) {
|
||||
val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class FetchAnimeInterval(
|
||||
val interval: Int,
|
||||
val leadDays: Int,
|
||||
val followDays: Int,
|
||||
)
|
||||
|
|
|
@ -84,13 +84,6 @@ class MangaScreen(
|
|||
|
||||
val successState = state as MangaScreenModel.State.Success
|
||||
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) {
|
||||
if (isHttpSource) {
|
||||
|
@ -108,7 +101,7 @@ class MangaScreen(
|
|||
state = successState,
|
||||
snackbarHostState = screenModel.snackbarHostState,
|
||||
dateFormat = screenModel.dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
fetchInterval = successState.manga.fetchInterval,
|
||||
isTabletUi = isTabletUi(),
|
||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||
|
@ -226,7 +219,7 @@ class MangaScreen(
|
|||
}
|
||||
is MangaScreenModel.Dialog.SetMangaFetchInterval -> {
|
||||
SetIntervalDialog(
|
||||
interval = if (dialog.manga.fetchInterval < 0) -dialog.manga.fetchInterval else 0,
|
||||
interval = dialog.manga.fetchInterval,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
|
||||
)
|
||||
|
|
|
@ -131,8 +131,6 @@ class MangaScreenModel(
|
|||
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
||||
|
||||
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 selectedChapterIds: HashSet<Long> = HashSet()
|
||||
|
@ -374,20 +372,14 @@ class MangaScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun setFetchInterval(manga: Manga, newInterval: 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
|
||||
}
|
||||
fun setFetchInterval(manga: Manga, interval: Int) {
|
||||
coroutineScope.launchIO {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga.copy(fetchInterval = interval),
|
||||
successState?.chapters?.map { it.chapter }.orEmpty(),
|
||||
// Custom intervals are negative
|
||||
manga.copy(fetchInterval = -interval),
|
||||
)
|
||||
val newManga = mangaRepository.getMangaById(mangaId)
|
||||
updateSuccessState { it.copy(manga = newManga) }
|
||||
val updatedManga = mangaRepository.getMangaById(manga.id)
|
||||
updateSuccessState { it.copy(manga = updatedManga) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1073,10 +1065,3 @@ data class ChapterItem(
|
|||
) {
|
||||
val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class FetchMangaInterval(
|
||||
val interval: Int,
|
||||
val leadDays: Int,
|
||||
val followDays: Int,
|
||||
)
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.view.animation.AnimationUtils
|
|||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.AlertDialog
|
||||
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 dev.chrisbanes.insetter.applyInsetter
|
||||
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.OrientationModeSelectDialog
|
||||
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.tachiyomi.R
|
||||
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.toShareIntent
|
||||
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.setTooltip
|
||||
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
|
@ -93,12 +94,12 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.preference.toggle
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.util.lang.withUIContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import tachiyomi.presentation.widget.util.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
@ -124,7 +125,7 @@ class ReaderActivity : BaseActivity() {
|
|||
val viewModel by viewModels<ReaderViewModel>()
|
||||
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.
|
||||
|
@ -391,11 +392,12 @@ class ReaderActivity : BaseActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
binding.dialogRoot.setComposeContent {
|
||||
binding.readerMenuBottom.setComposeContent {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val settingsScreenModel = remember {
|
||||
ReaderSettingsScreenModel(
|
||||
readerState = viewModel.state,
|
||||
hasDisplayCutout = hasCutout,
|
||||
onChangeReadingMode = viewModel::setMangaReadingMode,
|
||||
onChangeOrientation = viewModel::setMangaOrientationType,
|
||||
)
|
||||
|
@ -426,6 +428,28 @@ class ReaderActivity : BaseActivity() {
|
|||
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 -> {
|
||||
ReaderPageActionsDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -438,36 +462,61 @@ class ReaderActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// Init listeners on bottom menu
|
||||
binding.readerNav.setComposeContent {
|
||||
binding.readerMenuBottom.setComposeContent {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
if (state.viewer == null) return@setComposeContent
|
||||
val isRtl = state.viewer is R2LPagerViewer
|
||||
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
|
||||
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
|
||||
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
|
||||
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
|
||||
|
||||
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 {
|
||||
elevation = resources.getDimension(R.dimen.m3_sys_elevation_level2)
|
||||
alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light
|
||||
}
|
||||
binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity)
|
||||
|
||||
val toolbarColor = ColorUtils.setAlphaComponent(
|
||||
toolbarBackground.resolvedTintColor,
|
||||
toolbarBackground.alpha,
|
||||
|
@ -481,112 +530,6 @@ class ReaderActivity : BaseActivity() {
|
|||
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
|
||||
* [animate] the views.
|
||||
|
@ -653,13 +596,8 @@ class ReaderActivity : BaseActivity() {
|
|||
*/
|
||||
private fun setManga(manga: Manga) {
|
||||
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)
|
||||
|
||||
updateCropBordersShortcut()
|
||||
if (window.sharedElementEnterTransition is MaterialContainerTransform) {
|
||||
// Wait until transition is complete to avoid crash on API 26
|
||||
window.sharedElementEnterTransition.doOnEnd {
|
||||
|
@ -894,7 +832,6 @@ class ReaderActivity : BaseActivity() {
|
|||
if (newOrientation.flag != requestedOrientation) {
|
||||
requestedOrientation = newOrientation.flag
|
||||
}
|
||||
updateOrientationShortcut(viewModel.getMangaOrientationType(resolveDefault = false))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -61,7 +61,7 @@ class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet)
|
|||
override fun onDraw(canvas: Canvas) {
|
||||
if (navigation == null) return
|
||||
|
||||
navigation?.regions?.forEach { region ->
|
||||
navigation?.getRegions()?.forEach { region ->
|
||||
val rect = region.rectF
|
||||
|
||||
// Scale rect from 1f,1f to screen width and height
|
||||
|
|
|
@ -60,6 +60,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.preference.toggle
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.launchNonCancellable
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
|
@ -129,6 +130,15 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
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.
|
||||
*/
|
||||
|
@ -213,7 +223,10 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.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
|
||||
}
|
||||
chapterId = currentChapter.chapter.id!!
|
||||
|
@ -507,6 +520,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
it.copy(currentPage = pageIndex + 1)
|
||||
}
|
||||
readerChapter.requestedPage = pageIndex
|
||||
chapterPageIndex = pageIndex
|
||||
|
||||
if (!incognitoMode && page.status != Page.State.ERROR) {
|
||||
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]
|
||||
*/
|
||||
|
@ -700,6 +723,14 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
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) {
|
||||
mutableState.update { it.copy(dialog = Dialog.PageActions(page)) }
|
||||
}
|
||||
|
@ -910,6 +941,8 @@ class ReaderViewModel @JvmOverloads constructor(
|
|||
sealed interface Dialog {
|
||||
data object Loading : Dialog
|
||||
data object Settings : Dialog
|
||||
data object ReadingModeSelect : Dialog
|
||||
data object OrientationModeSelect : Dialog
|
||||
data class PageActions(val page: ReaderPage) : Dialog
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import uy.kohesive.injekt.api.get
|
|||
|
||||
class ReaderSettingsScreenModel(
|
||||
readerState: StateFlow<ReaderViewModel.State>,
|
||||
val hasDisplayCutout: Boolean,
|
||||
val onChangeReadingMode: (ReadingModeType) -> Unit,
|
||||
val onChangeOrientation: (OrientationType) -> Unit,
|
||||
val preferences: ReaderPreferences = Injekt.get(),
|
||||
|
|
|
@ -32,15 +32,19 @@ abstract class ViewerNavigation {
|
|||
|
||||
private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f)
|
||||
|
||||
abstract var regions: List<Region>
|
||||
|
||||
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 {
|
||||
val x = pos.x
|
||||
val y = pos.y
|
||||
val region = regions.map { it.invert(invertMode) }
|
||||
.find { it.rectF.contains(x, y) }
|
||||
val region = getRegions().find { it.rectF.contains(x, y) }
|
||||
return when {
|
||||
region != null -> region.type
|
||||
constantMenuRegion.contains(x, y) -> NavigationRegion.MENU
|
||||
|
|
|
@ -14,5 +14,5 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||
*/
|
||||
class DisabledNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = emptyList()
|
||||
override var regionList: List<Region> = emptyList()
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||
*/
|
||||
class EdgeNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = listOf(
|
||||
override var regionList: List<Region> = listOf(
|
||||
Region(
|
||||
rectF = RectF(0f, 0f, 0.33f, 1f),
|
||||
type = NavigationRegion.NEXT,
|
||||
|
|
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||
*/
|
||||
class KindlishNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = listOf(
|
||||
override var regionList: List<Region> = listOf(
|
||||
Region(
|
||||
rectF = RectF(0.33f, 0.33f, 1f, 1f),
|
||||
type = NavigationRegion.NEXT,
|
||||
|
|
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||
*/
|
||||
open class LNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = listOf(
|
||||
override var regionList: List<Region> = listOf(
|
||||
Region(
|
||||
rectF = RectF(0f, 0.33f, 0.33f, 0.66f),
|
||||
type = NavigationRegion.PREV,
|
||||
|
|
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
|||
*/
|
||||
class RightAndLeftNavigation : ViewerNavigation() {
|
||||
|
||||
override var regions: List<Region> = listOf(
|
||||
override var regionList: List<Region> = listOf(
|
||||
Region(
|
||||
rectF = RectF(0f, 0f, 0.33f, 1f),
|
||||
type = NavigationRegion.LEFT,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.util.chapter
|
|||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import tachiyomi.source.local.entries.manga.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
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> {
|
||||
if (manga.isLocal()) return this
|
||||
|
||||
val downloadCache: MangaDownloadCache = Injekt.get()
|
||||
|
||||
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) }
|
||||
|
|
|
@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.util.episode
|
|||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import tachiyomi.source.local.entries.anime.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
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> {
|
||||
if (anime.isLocal()) return this
|
||||
|
||||
val downloadCache: AnimeDownloadCache = Injekt.get()
|
||||
|
||||
return filter { downloadCache.isEpisodeDownloaded(it.name, it.scanlator, anime.title, anime.source, false) }
|
||||
|
|
|
@ -7,20 +7,13 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.content.PermissionChecker
|
||||
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 com.hippo.unifile.UniFile
|
||||
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.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
get() = getSystemService()!!
|
||||
|
||||
|
|
|
@ -2,11 +2,8 @@
|
|||
|
||||
package eu.kanade.tachiyomi.util.view
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.Gravity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
|
@ -14,11 +11,7 @@ import android.view.View
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
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.TooltipCompat
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -27,11 +20,8 @@ import androidx.compose.runtime.CompositionContext
|
|||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
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.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
|
||||
inline fun ComponentActivity.setComposeContent(
|
||||
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.
|
||||
*
|
||||
|
@ -110,57 +82,6 @@ inline fun View.popupMenu(
|
|||
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 {
|
||||
if (this == null) {
|
||||
return false
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,5 +1,4 @@
|
|||
<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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -58,83 +57,11 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize" />
|
||||
|
||||
<LinearLayout
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/reader_menu_bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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>
|
||||
android:layout_gravity="bottom" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
|
|
@ -8,10 +8,17 @@ import java.io.IOException
|
|||
import java.util.ArrayDeque
|
||||
import java.util.concurrent.Semaphore
|
||||
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.
|
||||
*
|
||||
* This uses `java.time` APIs and is the legacy method, kept
|
||||
* for compatibility reasons with existing extensions.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
||||
|
@ -19,27 +26,43 @@ import java.util.concurrent.TimeUnit
|
|||
*
|
||||
* @since extension-lib 1.3
|
||||
*
|
||||
* @param permits {Int} Number of requests allowed within a period of units.
|
||||
* @param period {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
* @param permits [Int] Number of requests allowed within a period of units.
|
||||
* @param period [Long] The limiting duration. Defaults to 1.
|
||||
* @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(
|
||||
permits: Int,
|
||||
period: Long = 1,
|
||||
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. */
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
internal class RateLimitInterceptor(
|
||||
private val host: String?,
|
||||
private val permits: Int,
|
||||
period: Long,
|
||||
unit: TimeUnit,
|
||||
period: Duration,
|
||||
) : Interceptor {
|
||||
|
||||
private val requestQueue = ArrayDeque<Long>(permits)
|
||||
private val rateLimitMillis = unit.toMillis(period)
|
||||
private val rateLimitMillis = period.inWholeMilliseconds
|
||||
private val fairLock = Semaphore(1, true)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
package eu.kanade.tachiyomi.network.interceptor
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
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.
|
||||
*
|
||||
* This uses Java Time APIs and is the legacy method, kept
|
||||
* for compatibility reasons with existing extensions.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* @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 {Long} The limiting duration. Defaults to 1.
|
||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
||||
* @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 [Long] The limiting duration. Defaults to 1.
|
||||
* @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(
|
||||
httpUrl: HttpUrl,
|
||||
permits: Int,
|
||||
period: Long = 1,
|
||||
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))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package tachiyomi.data
|
||||
|
||||
import app.cash.sqldelight.ColumnAdapter
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||
import java.util.Date
|
||||
|
||||
object DateColumnAdapter : ColumnAdapter<Date, Long> {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.library.anime.LibraryAnime
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.library.manga.LibraryManga
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy;
|
||||
import kotlin.collections.List;
|
||||
import kotlin.Boolean;
|
||||
import kotlin.String;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy;
|
||||
import kotlin.collections.List;
|
||||
import kotlin.Boolean;
|
||||
import kotlin.String;
|
||||
|
|
|
@ -2,34 +2,34 @@ package tachiyomi.domain.entries.anime.interactor
|
|||
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
||||
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 java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
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(
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
|
||||
) {
|
||||
|
||||
fun update(
|
||||
suspend fun toAnimeUpdateOrNull(
|
||||
anime: Anime,
|
||||
episodes: List<Episode>,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
dateTime: ZonedDateTime,
|
||||
window: Pair<Long, Long>,
|
||||
): AnimeUpdate? {
|
||||
val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||
getCurrent(ZonedDateTime.now())
|
||||
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||
getWindow(ZonedDateTime.now())
|
||||
} else {
|
||||
fetchRange
|
||||
window
|
||||
}
|
||||
val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(episodes, zonedDateTime)
|
||||
val nextUpdate = calculateNextUpdate(anime, interval, zonedDateTime, currentInterval)
|
||||
val episodes = getEpisodeByAnimeId.await(anime.id)
|
||||
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) {
|
||||
null
|
||||
|
@ -38,20 +38,11 @@ class SetAnimeFetchInterval(
|
|||
}
|
||||
}
|
||||
|
||||
fun getCurrent(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||
var followRange = 0
|
||||
var leadRange = 0
|
||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
|
||||
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)
|
||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||
}
|
||||
|
||||
internal fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
|
||||
|
@ -91,35 +82,41 @@ class SetAnimeFetchInterval(
|
|||
// Default to 7 days
|
||||
else -> 7
|
||||
}
|
||||
// Min 1, max 28 days
|
||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
|
||||
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
anime: Anime,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
dateTime: ZonedDateTime,
|
||||
window: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (
|
||||
anime.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
||||
anime.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
||||
anime.fetchInterval == 0
|
||||
) {
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), dateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
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 {
|
||||
anime.nextUpdate
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||
if (delta >= maxValue) return maxValue
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
||||
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 java.io.Serializable
|
||||
import kotlin.math.pow
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package tachiyomi.domain.entries.anime.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||
|
||||
data class AnimeUpdate(
|
||||
val id: Long,
|
||||
|
|
|
@ -2,34 +2,34 @@ package tachiyomi.domain.entries.manga.interactor
|
|||
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
||||
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 java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
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(
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
private val getChapterByMangaId: GetChapterByMangaId,
|
||||
) {
|
||||
|
||||
fun update(
|
||||
suspend fun toMangaUpdateOrNull(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
dateTime: ZonedDateTime,
|
||||
window: Pair<Long, Long>,
|
||||
): MangaUpdate? {
|
||||
val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||
getCurrent(ZonedDateTime.now())
|
||||
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||
getWindow(ZonedDateTime.now())
|
||||
} else {
|
||||
fetchRange
|
||||
window
|
||||
}
|
||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentInterval)
|
||||
val chapters = getChapterByMangaId.await(manga.id)
|
||||
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) {
|
||||
null
|
||||
|
@ -38,20 +38,11 @@ class SetMangaFetchInterval(
|
|||
}
|
||||
}
|
||||
|
||||
fun getCurrent(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||
var followRange = 0
|
||||
var leadRange = 0
|
||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
|
||||
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)
|
||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||
}
|
||||
|
||||
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||
|
@ -91,35 +82,41 @@ class SetMangaFetchInterval(
|
|||
// Default to 7 days
|
||||
else -> 7
|
||||
}
|
||||
// Min 1, max 28 days
|
||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
|
||||
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
dateTime: ZonedDateTime,
|
||||
window: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (
|
||||
manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
||||
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
||||
manga.fetchInterval == 0
|
||||
) {
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
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 {
|
||||
manga.nextUpdate
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||
if (delta >= maxValue) return maxValue
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
||||
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 java.io.Serializable
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package tachiyomi.domain.entries.manga.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||
|
||||
data class MangaUpdate(
|
||||
val id: Long,
|
||||
|
|
|
@ -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 autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
||||
|
|
|
@ -11,6 +11,7 @@ import java.time.ZonedDateTime
|
|||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class SetAnimeFetchIntervalTest {
|
||||
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private var episode = Episode.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
|
@ -19,14 +20,8 @@ class SetAnimeFetchIntervalTest {
|
|||
|
||||
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
|
||||
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>()
|
||||
(1..1).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
|
@ -63,9 +58,8 @@ class SetAnimeFetchIntervalTest {
|
|||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
// Default 1 if interval less than 1
|
||||
@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>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(15L * it)
|
||||
|
@ -98,9 +92,8 @@ class SetAnimeFetchIntervalTest {
|
|||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 2
|
||||
}
|
||||
|
||||
// If interval is decimal, floor to closest integer
|
||||
@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>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
|
@ -121,9 +114,8 @@ class SetAnimeFetchIntervalTest {
|
|||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Use fetch time if upload time not available
|
||||
@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>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
|
@ -132,4 +124,9 @@ class SetAnimeFetchIntervalTest {
|
|||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import java.time.ZonedDateTime
|
|||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class SetMangaFetchIntervalTest {
|
||||
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private var chapter = Chapter.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
|
@ -19,14 +20,8 @@ class SetMangaFetchIntervalTest {
|
|||
|
||||
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
|
||||
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>()
|
||||
(1..1).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
|
@ -63,9 +58,8 @@ class SetMangaFetchIntervalTest {
|
|||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
// Default 1 if interval less than 1
|
||||
@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>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(15L * it)
|
||||
|
@ -98,9 +92,8 @@ class SetMangaFetchIntervalTest {
|
|||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||
}
|
||||
|
||||
// If interval is decimal, floor to closest integer
|
||||
@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>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
|
@ -121,9 +114,8 @@ class SetMangaFetchIntervalTest {
|
|||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Use fetch time if upload time not available
|
||||
@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>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
|
@ -132,4 +124,9 @@ class SetMangaFetchIntervalTest {
|
|||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
||||
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-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
||||
|
|
|
@ -19,7 +19,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
|
|||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", 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" }
|
||||
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"
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
<!-- Models -->
|
||||
<string name="name">Name</string>
|
||||
<string name="categories">Categories</string>
|
||||
<string name="manga">Manga</string>
|
||||
<string name="chapters">Chapters</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_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_summary">Check for new cover and details when updating library</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_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="track_activity_name">Tracking login</string>
|
||||
|
||||
<!-- Browse section -->
|
||||
<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="other">%d days</item>
|
||||
</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 -->
|
||||
<plurals name="missing_items">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package eu.kanade.tachiyomi.animesource.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||
import java.io.Serializable
|
||||
|
||||
interface SAnime : Serializable {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package eu.kanade.tachiyomi.animesource.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||
|
||||
class SAnimeImpl : SAnime {
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* @since extensions-lib 1.4
|
|
@ -1,5 +1,6 @@
|
|||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||
import java.io.Serializable
|
||||
|
||||
interface SManga : Serializable {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||
|
||||
class SMangaImpl : SManga {
|
||||
|
||||
override lateinit var url: String
|
||||
|
|
Loading…
Reference in a new issue