mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-24 21:58:34 +03:00
parent
e29dc62837
commit
fa7b8427a2
86 changed files with 2504 additions and 2210 deletions
|
@ -85,6 +85,7 @@ android {
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
matchingFallbacks.add("release")
|
matchingFallbacks.add("release")
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
|
isProfileable = true
|
||||||
versionNameSuffix = "-benchmark"
|
versionNameSuffix = "-benchmark"
|
||||||
applicationIdSuffix = ".benchmark"
|
applicationIdSuffix = ".benchmark"
|
||||||
}
|
}
|
||||||
|
|
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 kotlin.** { public protected *; }
|
||||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||||
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
||||||
|
-keep,allowoptimization class kotlin.time.** { public protected *; }
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
-keep,allowoptimization class okio.** { public protected *; }
|
-keep,allowoptimization class okio.** { public protected *; }
|
||||||
-keep,allowoptimization class rx.** { public protected *; }
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
|
|
|
@ -41,11 +41,6 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Tachiyomi">
|
android:theme="@style/Theme.Tachiyomi">
|
||||||
|
|
||||||
<!-- enable profiling by macrobenchmark -->
|
|
||||||
<profileable
|
|
||||||
android:shell="true"
|
|
||||||
tools:targetApi="q" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
@ -173,8 +168,8 @@
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.TrackLoginActivity"
|
||||||
android:label="Anilist"
|
android:label="@string/track_activity_name"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
@ -182,69 +177,21 @@
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:host="anilist-auth"/>
|
||||||
android:host="anilist-auth"
|
<data android:host="bangumi-auth"/>
|
||||||
android:scheme="tachiyomi" />
|
<data android:host="myanimelist-auth"/>
|
||||||
|
<data android:host="shikimori-auth"/>
|
||||||
|
|
||||||
|
<data android:scheme="tachiyomi"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
|
||||||
android:label="MyAnimeList"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:host="simkl-auth"/>
|
||||||
android:host="myanimelist-auth"
|
<data android:scheme="aniyomi"/>
|
||||||
android:scheme="tachiyomi" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
|
||||||
android:label="Shikimori"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="shikimori-auth"
|
|
||||||
android:scheme="tachiyomi" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
|
||||||
android:label="Bangumi"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="bangumi-auth"
|
|
||||||
android:scheme="tachiyomi" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.SimklLoginActivity"
|
|
||||||
android:label="Simkl"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="simkl-auth"
|
|
||||||
android:scheme="aniyomi" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
@ -292,10 +239,12 @@
|
||||||
android:name=".data.updater.AppUpdateService"
|
android:name=".data.updater.AppUpdateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".extension.manga.util.MangaExtensionInstallService"
|
<service
|
||||||
|
android:name=".extension.manga.util.MangaExtensionInstallService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service android:name=".extension.anime.util.AnimeExtensionInstallService"
|
<service
|
||||||
|
android:name=".extension.anime.util.AnimeExtensionInstallService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,7 +7,6 @@ import tachiyomi.domain.entries.anime.interactor.SetAnimeFetchInterval
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||||
import tachiyomi.domain.items.episode.model.Episode
|
|
||||||
import tachiyomi.source.local.entries.anime.isLocal
|
import tachiyomi.source.local.entries.anime.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -79,16 +78,12 @@ class UpdateAnime(
|
||||||
|
|
||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
episodes: List<Episode>,
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
window: Pair<Long, Long> = setAnimeFetchInterval.getWindow(dateTime),
|
||||||
fetchRange: Pair<Long, Long> = setAnimeFetchInterval.getCurrent(zonedDateTime),
|
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val updateAnime = setAnimeFetchInterval.update(anime, episodes, zonedDateTime, fetchRange)
|
return setAnimeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
|
||||||
return if (updateAnime != null) {
|
?.let { animeRepository.updateAnime(it) }
|
||||||
animeRepository.updateAnime(updateAnime)
|
?: false
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {
|
suspend fun awaitUpdateLastUpdate(animeId: Long): Boolean {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import tachiyomi.domain.entries.manga.interactor.SetMangaFetchInterval
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
|
||||||
import tachiyomi.source.local.entries.manga.isLocal
|
import tachiyomi.source.local.entries.manga.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -79,16 +78,12 @@ class UpdateManga(
|
||||||
|
|
||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
window: Pair<Long, Long> = setMangaFetchInterval.getWindow(dateTime),
|
||||||
fetchRange: Pair<Long, Long> = setMangaFetchInterval.getCurrent(zonedDateTime),
|
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val updatedManga = setMangaFetchInterval.update(manga, chapters, zonedDateTime, fetchRange)
|
return setMangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||||
return if (updatedManga != null) {
|
?.let { mangaRepository.updateManga(it) }
|
||||||
mangaRepository.updateManga(updatedManga)
|
?: false
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||||
|
|
|
@ -50,13 +50,14 @@ class SyncChaptersWithSource(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: MangaSource,
|
source: MangaSource,
|
||||||
manualFetch: Boolean = false,
|
manualFetch: Boolean = false,
|
||||||
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
||||||
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
|
||||||
): List<Chapter> {
|
): List<Chapter> {
|
||||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||||
throw NoChaptersException()
|
throw NoChaptersException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val now = ZonedDateTime.now()
|
||||||
|
|
||||||
val sourceChapters = rawSourceChapters
|
val sourceChapters = rawSourceChapters
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }
|
||||||
.mapIndexed { i, sChapter ->
|
.mapIndexed { i, sChapter ->
|
||||||
|
@ -138,12 +139,11 @@ class SyncChaptersWithSource(
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchRange.first) {
|
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||||
updateManga.awaitUpdateFetchInterval(
|
updateManga.awaitUpdateFetchInterval(
|
||||||
manga,
|
manga,
|
||||||
dbChapters,
|
now,
|
||||||
zoneDateTime,
|
fetchWindow,
|
||||||
fetchRange,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
|
@ -200,8 +200,7 @@ class SyncChaptersWithSource(
|
||||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
}
|
}
|
||||||
val newChapters = chapterRepository.getChapterByMangaId(manga.id)
|
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||||
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
|
|
||||||
|
|
||||||
// Set this manga as updated since chapters were changed
|
// Set this manga as updated since chapters were changed
|
||||||
// Note that last_update actually represents last time the chapter list changed at all
|
// Note that last_update actually represents last time the chapter list changed at all
|
||||||
|
|
|
@ -50,13 +50,14 @@ class SyncEpisodesWithSource(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
source: AnimeSource,
|
source: AnimeSource,
|
||||||
manualFetch: Boolean = false,
|
manualFetch: Boolean = false,
|
||||||
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
||||||
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
|
||||||
): List<Episode> {
|
): List<Episode> {
|
||||||
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
|
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
|
||||||
throw NoEpisodesException()
|
throw NoEpisodesException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val now = ZonedDateTime.now()
|
||||||
|
|
||||||
val sourceEpisodes = rawSourceEpisodes
|
val sourceEpisodes = rawSourceEpisodes
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }
|
||||||
.mapIndexed { i, sEpisode ->
|
.mapIndexed { i, sEpisode ->
|
||||||
|
@ -138,12 +139,11 @@ class SyncEpisodesWithSource(
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||||
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchRange.first) {
|
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
|
||||||
updateAnime.awaitUpdateFetchInterval(
|
updateAnime.awaitUpdateFetchInterval(
|
||||||
anime,
|
anime,
|
||||||
dbEpisodes,
|
now,
|
||||||
zoneDateTime,
|
fetchWindow,
|
||||||
fetchRange,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
|
@ -200,8 +200,7 @@ class SyncEpisodesWithSource(
|
||||||
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
||||||
updateEpisode.awaitAll(episodeUpdates)
|
updateEpisode.awaitAll(episodeUpdates)
|
||||||
}
|
}
|
||||||
val newChapters = episodeRepository.getEpisodeByAnimeId(anime.id)
|
updateAnime.awaitUpdateFetchInterval(anime, now, fetchWindow)
|
||||||
updateAnime.awaitUpdateFetchInterval(anime, newChapters, zoneDateTime, fetchRange)
|
|
||||||
|
|
||||||
// Set this anime as updated since episodes were changed
|
// Set this anime as updated since episodes were changed
|
||||||
// Note that last_update actually represents last time the episode list changed at all
|
// Note that last_update actually represents last time the episode list changed at all
|
||||||
|
|
|
@ -16,7 +16,6 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.domain.entries.anime.interactor.MAX_GRACE_PERIOD
|
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -58,7 +57,7 @@ fun SetIntervalDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onValueChanged: (Int) -> Unit,
|
onValueChanged: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
|
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -69,7 +68,7 @@ fun SetIntervalDialog(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||||
val items = (0..MAX_GRACE_PERIOD).map {
|
val items = (0..28).map {
|
||||||
if (it == 0) {
|
if (it == 0) {
|
||||||
stringResource(R.string.label_default)
|
stringResource(R.string.label_default)
|
||||||
} else {
|
} else {
|
||||||
|
@ -79,8 +78,8 @@ fun SetIntervalDialog(
|
||||||
WheelTextPicker(
|
WheelTextPicker(
|
||||||
size = size,
|
size = size,
|
||||||
items = items,
|
items = items,
|
||||||
startIndex = intervalValue,
|
startIndex = selectedInterval,
|
||||||
onSelectionChanged = { intervalValue = it },
|
onSelectionChanged = { selectedInterval = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -91,7 +90,7 @@ fun SetIntervalDialog(
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onValueChanged(intervalValue)
|
onValueChanged(selectedInterval)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
},) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
|
|
|
@ -74,7 +74,6 @@ import eu.kanade.tachiyomi.source.anime.getNameForAnimeInfo
|
||||||
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
|
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
|
||||||
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel
|
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem
|
import eu.kanade.tachiyomi.ui.entries.anime.EpisodeItem
|
||||||
import eu.kanade.tachiyomi.ui.entries.anime.FetchAnimeInterval
|
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -98,7 +97,7 @@ import java.util.concurrent.TimeUnit
|
||||||
fun AnimeScreen(
|
fun AnimeScreen(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: FetchAnimeInterval?,
|
fetchInterval: Int?,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
@ -247,7 +246,7 @@ private fun AnimeScreenSmallImpl(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: FetchAnimeInterval?,
|
fetchInterval: Int?,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
showNextEpisodeAirTime: Boolean,
|
showNextEpisodeAirTime: Boolean,
|
||||||
|
@ -518,7 +517,7 @@ fun AnimeScreenLargeImpl(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: FetchAnimeInterval?,
|
fetchInterval: Int?,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
showNextEpisodeAirTime: Boolean,
|
showNextEpisodeAirTime: Boolean,
|
||||||
|
|
|
@ -80,13 +80,13 @@ import eu.kanade.presentation.entries.DotSeparatorText
|
||||||
import eu.kanade.presentation.entries.ItemCover
|
import eu.kanade.presentation.entries.ItemCover
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.ui.entries.anime.FetchAnimeInterval
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
import tachiyomi.presentation.core.components.material.TextButton
|
import tachiyomi.presentation.core.components.material.TextButton
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||||
|
@ -168,7 +168,7 @@ fun AnimeActionRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
fetchInterval: FetchAnimeInterval?,
|
fetchInterval: Int?,
|
||||||
isUserIntervalMode: Boolean,
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
|
@ -192,14 +192,8 @@ fun AnimeActionRow(
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||||
val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays)
|
|
||||||
AnimeActionButton(
|
AnimeActionButton(
|
||||||
title =
|
title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
|
||||||
if (intervalPair.first == intervalPair.second) {
|
|
||||||
pluralStringResource(id = R.plurals.day, count = intervalPair.second, intervalPair.second)
|
|
||||||
} else {
|
|
||||||
pluralStringResource(id = R.plurals.range_interval_day, count = intervalPair.second, intervalPair.first, intervalPair.second)
|
|
||||||
},
|
|
||||||
icon = Icons.Default.HourglassEmpty,
|
icon = Icons.Default.HourglassEmpty,
|
||||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
onClick = onEditIntervalClicked,
|
onClick = onEditIntervalClicked,
|
||||||
|
|
|
@ -68,7 +68,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.manga.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
|
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
|
||||||
import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem
|
import eu.kanade.tachiyomi.ui.entries.manga.ChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.entries.manga.FetchMangaInterval
|
|
||||||
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
|
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreenModel
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
@ -91,7 +90,7 @@ import java.util.Date
|
||||||
fun MangaScreen(
|
fun MangaScreen(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: FetchMangaInterval?,
|
fetchInterval: Int?,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -230,7 +229,7 @@ private fun MangaScreenSmallImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: FetchMangaInterval?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
@ -466,7 +465,7 @@ fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: FetchMangaInterval?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
|
|
@ -80,13 +80,13 @@ import eu.kanade.presentation.entries.DotSeparatorText
|
||||||
import eu.kanade.presentation.entries.ItemCover
|
import eu.kanade.presentation.entries.ItemCover
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.entries.manga.FetchMangaInterval
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.presentation.core.components.material.TextButton
|
import tachiyomi.presentation.core.components.material.TextButton
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||||
|
@ -168,7 +168,7 @@ fun MangaActionRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
fetchInterval: FetchMangaInterval?,
|
fetchInterval: Int?,
|
||||||
isUserIntervalMode: Boolean,
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
|
@ -192,14 +192,8 @@ fun MangaActionRow(
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||||
val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays)
|
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title =
|
title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
|
||||||
if (intervalPair.first == intervalPair.second) {
|
|
||||||
pluralStringResource(id = R.plurals.day, count = intervalPair.second, intervalPair.second)
|
|
||||||
} else {
|
|
||||||
pluralStringResource(id = R.plurals.range_interval_day, count = intervalPair.second, intervalPair.first, intervalPair.second)
|
|
||||||
},
|
|
||||||
icon = Icons.Default.HourglassEmpty,
|
icon = Icons.Default.HourglassEmpty,
|
||||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
onClick = onEditIntervalClicked,
|
onClick = onEditIntervalClicked,
|
||||||
|
|
|
@ -1,33 +1,18 @@
|
||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.util.fastMap
|
import androidx.compose.ui.util.fastMap
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
@ -52,7 +37,6 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_U
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -181,13 +165,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
|
|
||||||
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
|
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
|
||||||
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
|
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
|
||||||
val libraryUpdateDeviceRestrictionPref = libraryPreferences.libraryUpdateDeviceRestriction()
|
|
||||||
val libraryUpdateMangaRestrictionPref = libraryPreferences.libraryUpdateItemRestriction()
|
|
||||||
|
|
||||||
val animelibUpdateCategoriesPref = libraryPreferences.animeLibraryUpdateCategories()
|
val animelibUpdateCategoriesPref = libraryPreferences.animeLibraryUpdateCategories()
|
||||||
val animelibUpdateCategoriesExcludePref =
|
val animelibUpdateCategoriesExcludePref =
|
||||||
libraryPreferences.animeLibraryUpdateCategoriesExclude()
|
libraryPreferences.animeLibraryUpdateCategoriesExclude()
|
||||||
val libraryUpdateAnimeRestriction by libraryUpdateMangaRestrictionPref.collectAsState()
|
|
||||||
|
|
||||||
val includedAnime by animelibUpdateCategoriesPref.collectAsState()
|
val includedAnime by animelibUpdateCategoriesPref.collectAsState()
|
||||||
val excludedAnime by animelibUpdateCategoriesExcludePref.collectAsState()
|
val excludedAnime by animelibUpdateCategoriesExcludePref.collectAsState()
|
||||||
|
@ -211,27 +192,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val leadAnimeRange by libraryPreferences.leadingAnimeExpectedDays().collectAsState()
|
|
||||||
val followAnimeRange by libraryPreferences.followingAnimeExpectedDays().collectAsState()
|
|
||||||
|
|
||||||
var showFetchAnimeRangesDialog by rememberSaveable { mutableStateOf(false) }
|
|
||||||
if (showFetchAnimeRangesDialog) {
|
|
||||||
LibraryExpectedRangeDialog(
|
|
||||||
initialLead = leadAnimeRange,
|
|
||||||
initialFollow = followAnimeRange,
|
|
||||||
onDismissRequest = { showFetchAnimeRangesDialog = false },
|
|
||||||
onValueChanged = { leadValue, followValue ->
|
|
||||||
libraryPreferences.leadingAnimeExpectedDays().set(leadValue)
|
|
||||||
libraryPreferences.followingAnimeExpectedDays().set(followValue)
|
|
||||||
showFetchAnimeRangesDialog = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val libraryUpdateCategoriesPref = libraryPreferences.mangaLibraryUpdateCategories()
|
val libraryUpdateCategoriesPref = libraryPreferences.mangaLibraryUpdateCategories()
|
||||||
val libraryUpdateCategoriesExcludePref =
|
val libraryUpdateCategoriesExcludePref =
|
||||||
libraryPreferences.mangaLibraryUpdateCategoriesExclude()
|
libraryPreferences.mangaLibraryUpdateCategoriesExclude()
|
||||||
val libraryUpdateMangaRestriction by libraryUpdateMangaRestrictionPref.collectAsState()
|
|
||||||
|
|
||||||
val includedManga by libraryUpdateCategoriesPref.collectAsState()
|
val includedManga by libraryUpdateCategoriesPref.collectAsState()
|
||||||
val excludedManga by libraryUpdateCategoriesExcludePref.collectAsState()
|
val excludedManga by libraryUpdateCategoriesExcludePref.collectAsState()
|
||||||
|
@ -255,25 +219,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val leadMangaRange by libraryPreferences.leadingMangaExpectedDays().collectAsState()
|
|
||||||
val followMangaRange by libraryPreferences.followingMangaExpectedDays().collectAsState()
|
|
||||||
|
|
||||||
var showFetchMangaRangesDialog by rememberSaveable { mutableStateOf(false) }
|
|
||||||
if (showFetchMangaRangesDialog) {
|
|
||||||
LibraryExpectedRangeDialog(
|
|
||||||
initialLead = leadMangaRange,
|
|
||||||
initialFollow = followMangaRange,
|
|
||||||
onDismissRequest = { showFetchMangaRangesDialog = false },
|
|
||||||
onValueChanged = { leadValue, followValue ->
|
|
||||||
libraryPreferences.leadingMangaExpectedDays().set(leadValue)
|
|
||||||
libraryPreferences.followingMangaExpectedDays().set(followValue)
|
|
||||||
showFetchMangaRangesDialog = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(R.string.pref_category_library_update),
|
title = stringResource(R.string.pref_category_library_update),
|
||||||
preferenceItems = listOfNotNull(
|
preferenceItems = listOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryUpdateIntervalPref,
|
pref = libraryUpdateIntervalPref,
|
||||||
title = stringResource(R.string.pref_library_update_interval),
|
title = stringResource(R.string.pref_library_update_interval),
|
||||||
|
@ -292,7 +241,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryUpdateDeviceRestrictionPref,
|
pref = libraryPreferences.libraryUpdateDeviceRestriction(),
|
||||||
enabled = libraryUpdateInterval > 0,
|
enabled = libraryUpdateInterval > 0,
|
||||||
title = stringResource(R.string.pref_library_update_restriction),
|
title = stringResource(R.string.pref_library_update_restriction),
|
||||||
subtitle = stringResource(R.string.restrictions),
|
subtitle = stringResource(R.string.restrictions),
|
||||||
|
@ -341,7 +290,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryUpdateMangaRestrictionPref,
|
pref = libraryPreferences.libraryUpdateItemRestriction(),
|
||||||
title = stringResource(R.string.pref_library_update_manga_restriction),
|
title = stringResource(R.string.pref_library_update_manga_restriction),
|
||||||
entries = mapOf(
|
entries = mapOf(
|
||||||
ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
|
ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
|
||||||
|
@ -350,29 +299,6 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
|
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(R.string.pref_update_release_grace_period),
|
|
||||||
subtitle = listOf(
|
|
||||||
pluralStringResource(R.plurals.pref_update_release_leading_days, leadMangaRange, leadMangaRange),
|
|
||||||
pluralStringResource(R.plurals.pref_update_release_following_days, followMangaRange, followMangaRange),
|
|
||||||
).joinToString(),
|
|
||||||
onClick = { showFetchMangaRangesDialog = true },
|
|
||||||
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
|
|
||||||
Preference.PreferenceItem.InfoPreference(
|
|
||||||
title = stringResource(R.string.pref_update_release_grace_period_info),
|
|
||||||
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
|
|
||||||
Preference.PreferenceItem.TextPreference(
|
|
||||||
title = stringResource(R.string.pref_update_anime_release_grace_period),
|
|
||||||
subtitle = listOf(
|
|
||||||
pluralStringResource(R.plurals.pref_update_release_leading_days, leadAnimeRange, leadAnimeRange),
|
|
||||||
pluralStringResource(R.plurals.pref_update_release_following_days, followAnimeRange, followAnimeRange),
|
|
||||||
).joinToString(),
|
|
||||||
onClick = { showFetchAnimeRangesDialog = true },
|
|
||||||
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction },
|
|
||||||
Preference.PreferenceItem.InfoPreference(
|
|
||||||
title = stringResource(R.string.pref_update_release_grace_period_info),
|
|
||||||
).takeIf { ENTRY_OUTSIDE_RELEASE_PERIOD in libraryUpdateAnimeRestriction },
|
|
||||||
|
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = libraryPreferences.newShowUpdatesCount(),
|
pref = libraryPreferences.newShowUpdatesCount(),
|
||||||
title = stringResource(R.string.pref_library_update_show_tab_badge),
|
title = stringResource(R.string.pref_library_update_show_tab_badge),
|
||||||
|
@ -442,79 +368,4 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LibraryExpectedRangeDialog(
|
|
||||||
initialLead: Int,
|
|
||||||
initialFollow: Int,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onValueChanged: (portrait: Int, landscape: Int) -> Unit,
|
|
||||||
) {
|
|
||||||
var leadValue by rememberSaveable { mutableIntStateOf(initialLead) }
|
|
||||||
var followValue by rememberSaveable { mutableIntStateOf(initialFollow) }
|
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = { Text(text = stringResource(R.string.pref_update_release_grace_period)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
text = pluralStringResource(R.plurals.pref_update_release_leading_days, leadValue, leadValue),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
maxLines = 1,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
text = pluralStringResource(R.plurals.pref_update_release_following_days, followValue, followValue),
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
maxLines = 1,
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BoxWithConstraints(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
|
||||||
val items = (0..28).map(Int::toString)
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
WheelTextPicker(
|
|
||||||
size = size,
|
|
||||||
items = items,
|
|
||||||
startIndex = leadValue,
|
|
||||||
onSelectionChanged = {
|
|
||||||
leadValue = it
|
|
||||||
},
|
|
||||||
)
|
|
||||||
WheelTextPicker(
|
|
||||||
size = size,
|
|
||||||
items = items,
|
|
||||||
startIndex = followValue,
|
|
||||||
onSelectionChanged = {
|
|
||||||
followValue = it
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(text = stringResource(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = { onValueChanged(leadValue, followValue) }) {
|
|
||||||
Text(text = stringResource(R.string.action_ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 layoutDirection = if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
// Match with toolbar background color set in ReaderActivity
|
||||||
|
val backgroundColor = MaterialTheme.colorScheme
|
||||||
|
.surfaceColorAtElevation(3.dp)
|
||||||
|
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
|
||||||
|
val buttonColor = IconButtonDefaults.filledIconButtonColors(
|
||||||
|
containerColor = backgroundColor,
|
||||||
|
disabledContainerColor = backgroundColor,
|
||||||
|
)
|
||||||
|
|
||||||
// We explicitly handle direction based on the reader viewer rather than the system direction
|
// We explicitly handle direction based on the reader viewer rather than the system direction
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
Row(
|
Row(
|
||||||
|
@ -61,14 +70,6 @@ fun ChapterNavigator(
|
||||||
.padding(horizontal = horizontalPadding),
|
.padding(horizontal = horizontalPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
// Match with toolbar background color set in ReaderActivity
|
|
||||||
val backgroundColor = MaterialTheme.colorScheme
|
|
||||||
.surfaceColorAtElevation(3.dp)
|
|
||||||
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
|
|
||||||
val buttonColor = IconButtonDefaults.filledIconButtonColors(
|
|
||||||
containerColor = backgroundColor,
|
|
||||||
disabledContainerColor = backgroundColor,
|
|
||||||
)
|
|
||||||
FilledIconButton(
|
FilledIconButton(
|
||||||
enabled = if (isRtl) enabledNext else enabledPrevious,
|
enabled = if (isRtl) enabledNext else enabledPrevious,
|
||||||
onClick = if (isRtl) onNextChapter else onPreviousChapter,
|
onClick = if (isRtl) onNextChapter else onPreviousChapter,
|
||||||
|
|
|
@ -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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
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(),
|
pref = screenModel.preferences.fullscreen(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: hide if there's no cutout
|
if (screenModel.hasDisplayCutout) {
|
||||||
CheckboxItem(
|
CheckboxItem(
|
||||||
label = stringResource(R.string.pref_cutout_short),
|
label = stringResource(R.string.pref_cutout_short),
|
||||||
pref = screenModel.preferences.cutoutShort(),
|
pref = screenModel.preferences.cutoutShort(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
CheckboxItem(
|
CheckboxItem(
|
||||||
label = stringResource(R.string.pref_keep_screen_on),
|
label = stringResource(R.string.pref_keep_screen_on),
|
||||||
|
|
|
@ -60,9 +60,9 @@ class BackupRestorer(
|
||||||
private val episodeRepository: EpisodeRepository = Injekt.get()
|
private val episodeRepository: EpisodeRepository = Injekt.get()
|
||||||
private val setAnimeFetchInterval: SetAnimeFetchInterval = Injekt.get()
|
private val setAnimeFetchInterval: SetAnimeFetchInterval = Injekt.get()
|
||||||
|
|
||||||
private var zonedDateTime = ZonedDateTime.now()
|
private var now = ZonedDateTime.now()
|
||||||
private var currentMangaFetchInterval = setMangaFetchInterval.getCurrent(zonedDateTime)
|
private var currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
|
||||||
private var currentAnimeFetchInterval = setAnimeFetchInterval.getCurrent(zonedDateTime)
|
private var currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
|
||||||
|
|
||||||
private var backupManager = BackupManager(context)
|
private var backupManager = BackupManager(context)
|
||||||
|
|
||||||
|
@ -140,9 +140,9 @@ class BackupRestorer(
|
||||||
val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
|
val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
|
||||||
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
||||||
|
|
||||||
zonedDateTime = ZonedDateTime.now()
|
now = ZonedDateTime.now()
|
||||||
currentMangaFetchInterval = setMangaFetchInterval.getCurrent(zonedDateTime)
|
currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
|
||||||
currentAnimeFetchInterval = setAnimeFetchInterval.getCurrent(zonedDateTime)
|
currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
|
@ -216,8 +216,7 @@ class BackupRestorer(
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
|
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id)
|
updateManga.awaitUpdateFetchInterval(restoredManga, now, currentMangaFetchWindow)
|
||||||
updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentMangaFetchInterval)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
|
@ -291,8 +290,7 @@ class BackupRestorer(
|
||||||
// Fetch rest of anime information
|
// Fetch rest of anime information
|
||||||
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
|
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
val updatedEpisodes = episodeRepository.getEpisodeByAnimeId(restoredAnime.id)
|
updateAnime.awaitUpdateFetchInterval(restoredAnime, now, currentAnimeFetchWindow)
|
||||||
updateAnime.awaitUpdateFetchInterval(restoredAnime, updatedEpisodes, zonedDateTime, currentAnimeFetchInterval)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
|
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
|
||||||
errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.data.backup.models
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
|
@ -30,7 +30,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedAnimeTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewEpisodes
|
import eu.kanade.tachiyomi.util.shouldDownloadNewEpisodes
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
@ -230,10 +230,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
|
val fetchWindow = setAnimeFetchInterval.getWindow(ZonedDateTime.now())
|
||||||
val now = ZonedDateTime.now()
|
|
||||||
val fetchInterval = setAnimeFetchInterval.getCurrent(now)
|
|
||||||
val higherLimit = fetchInterval.second
|
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
animeToUpdate.groupBy { it.anime.source }.values
|
animeToUpdate.groupBy { it.anime.source }.values
|
||||||
|
@ -255,8 +252,8 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
anime,
|
anime,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > higherLimit ->
|
anime.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_in_release_period))
|
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_always_update))
|
||||||
|
|
||||||
ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED ->
|
ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED ->
|
||||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed))
|
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed))
|
||||||
|
@ -267,12 +264,12 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
ENTRY_NON_VIEWED in restrictions && libraryAnime.totalEpisodes > 0L && !libraryAnime.hasStarted ->
|
ENTRY_NON_VIEWED in restrictions && libraryAnime.totalEpisodes > 0L && !libraryAnime.hasStarted ->
|
||||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_started))
|
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_started))
|
||||||
|
|
||||||
anime.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > fetchWindow.second ->
|
||||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_always_update))
|
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
try {
|
try {
|
||||||
val newEpisodes = updateAnime(anime, now, fetchInterval)
|
val newEpisodes = updateAnime(anime, fetchWindow)
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
|
|
||||||
if (newEpisodes.isNotEmpty()) {
|
if (newEpisodes.isNotEmpty()) {
|
||||||
|
@ -328,6 +325,13 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (skippedUpdates.isNotEmpty()) {
|
if (skippedUpdates.isNotEmpty()) {
|
||||||
|
// TODO: surface skipped reasons to user
|
||||||
|
logcat {
|
||||||
|
skippedUpdates
|
||||||
|
.groupBy { it.second }
|
||||||
|
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||||
|
.joinToString()
|
||||||
|
}
|
||||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,7 +348,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
* @param anime the anime to update.
|
* @param anime the anime to update.
|
||||||
* @return a pair of the inserted and removed episodes.
|
* @return a pair of the inserted and removed episodes.
|
||||||
*/
|
*/
|
||||||
private suspend fun updateAnime(anime: Anime, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Episode> {
|
private suspend fun updateAnime(anime: Anime, fetchWindow: Pair<Long, Long>): List<Episode> {
|
||||||
val source = sourceManager.getOrStub(anime.source)
|
val source = sourceManager.getOrStub(anime.source)
|
||||||
|
|
||||||
// Update anime metadata if needed
|
// Update anime metadata if needed
|
||||||
|
@ -359,7 +363,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
// to get latest data so it doesn't get overwritten later on
|
// to get latest data so it doesn't get overwritten later on
|
||||||
val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList()
|
val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList()
|
||||||
|
|
||||||
return syncEpisodesWithSource.await(episodes, dbAnime, source, false, zoneDateTime, fetchRange)
|
return syncEpisodesWithSource.await(episodes, dbAnime, source, false, fetchWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
|
|
|
@ -28,9 +28,9 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedMangaTrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedMangaTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
|
@ -230,10 +230,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
|
val fetchWindow = setMangaFetchInterval.getWindow(ZonedDateTime.now())
|
||||||
val now = ZonedDateTime.now()
|
|
||||||
val fetchInterval = setMangaFetchInterval.getCurrent(now)
|
|
||||||
val higherLimit = fetchInterval.second
|
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
mangaToUpdate.groupBy { it.manga.source }.values
|
mangaToUpdate.groupBy { it.manga.source }.values
|
||||||
|
@ -255,8 +252,8 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
manga,
|
manga,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit ->
|
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
|
||||||
|
|
||||||
ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
||||||
|
@ -267,12 +264,12 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
ENTRY_NON_VIEWED in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
|
ENTRY_NON_VIEWED in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
|
||||||
|
|
||||||
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > fetchWindow.second ->
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
try {
|
try {
|
||||||
val newChapters = updateManga(manga, now, fetchInterval)
|
val newChapters = updateManga(manga, fetchWindow)
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
|
@ -328,6 +325,13 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (skippedUpdates.isNotEmpty()) {
|
if (skippedUpdates.isNotEmpty()) {
|
||||||
|
// TODO: surface skipped reasons to user
|
||||||
|
logcat {
|
||||||
|
skippedUpdates
|
||||||
|
.groupBy { it.second }
|
||||||
|
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||||
|
.joinToString()
|
||||||
|
}
|
||||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,7 +348,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
|
private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> {
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
// Update manga metadata if needed
|
// Update manga metadata if needed
|
||||||
|
@ -359,7 +363,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
// to get latest data so it doesn't get overwritten later on
|
// to get latest data so it doesn't get overwritten later on
|
||||||
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
|
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
|
||||||
|
|
||||||
return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
|
return syncChaptersWithSource.await(chapters, dbManga, source, false, fetchWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.concurrent.TimeUnit
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
private val authClient = client.newBuilder()
|
private val authClient = client.newBuilder()
|
||||||
.addInterceptor(interceptor)
|
.addInterceptor(interceptor)
|
||||||
.rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES)
|
.rateLimit(permits = 85, period = 1.minutes)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
suspend fun addLibManga(track: MangaTrack): MangaTrack {
|
suspend fun addLibManga(track: MangaTrack): MangaTrack {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import androidx.core.app.NotificationCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.notify
|
import eu.kanade.tachiyomi.util.system.notify
|
||||||
|
|
||||||
class ExtensionUpdateNotifier(private val context: Context) {
|
class ExtensionUpdateNotifier(private val context: Context) {
|
||||||
|
@ -29,4 +30,8 @@ class ExtensionUpdateNotifier(private val context: Context) {
|
||||||
setAutoCancel(true)
|
setAutoCancel(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
context.cancelNotification(Notifications.ID_UPDATES_TO_EXTS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.anime.api.AnimeExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
|
||||||
|
@ -195,7 +196,7 @@ class AnimeExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable of the installation process for the given anime extension. It will complete
|
* Returns a flow of the installation process for the given anime extension. It will complete
|
||||||
* once the anime extension is installed or throws an error. The process will be canceled if
|
* once the anime extension is installed or throws an error. The process will be canceled if
|
||||||
* unsubscribed before its completion.
|
* unsubscribed before its completion.
|
||||||
*
|
*
|
||||||
|
@ -206,7 +207,7 @@ class AnimeExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable of the installation process for the given anime extension. It will complete
|
* Returns a flow of the installation process for the given anime extension. It will complete
|
||||||
* once the anime extension is updated or throws an error. The process will be canceled if
|
* once the anime extension is updated or throws an error. The process will be canceled if
|
||||||
* unsubscribed before its completion.
|
* unsubscribed before its completion.
|
||||||
*
|
*
|
||||||
|
@ -356,6 +357,10 @@ class AnimeExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePendingUpdatesCount() {
|
private fun updatePendingUpdatesCount() {
|
||||||
preferences.animeExtensionUpdatesCount().set(_installedAnimeExtensionsFlow.value.count { it.hasUpdate })
|
val pendingUpdateCount = _installedAnimeExtensionsFlow.value.count { it.hasUpdate }
|
||||||
|
preferences.animeExtensionUpdatesCount().set(pendingUpdateCount)
|
||||||
|
if (pendingUpdateCount == 0) {
|
||||||
|
ExtensionUpdateNotifier(context).dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateNotifier
|
||||||
import eu.kanade.tachiyomi.extension.InstallStep
|
import eu.kanade.tachiyomi.extension.InstallStep
|
||||||
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.manga.api.MangaExtensionGithubApi
|
||||||
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
|
||||||
|
@ -195,7 +196,7 @@ class MangaExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable of the installation process for the given extension. It will complete
|
* Returns a flow of the installation process for the given extension. It will complete
|
||||||
* once the extension is installed or throws an error. The process will be canceled if
|
* once the extension is installed or throws an error. The process will be canceled if
|
||||||
* unsubscribed before its completion.
|
* unsubscribed before its completion.
|
||||||
*
|
*
|
||||||
|
@ -206,7 +207,7 @@ class MangaExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable of the installation process for the given extension. It will complete
|
* Returns a flow of the installation process for the given extension. It will complete
|
||||||
* once the extension is updated or throws an error. The process will be canceled if
|
* once the extension is updated or throws an error. The process will be canceled if
|
||||||
* unsubscribed before its completion.
|
* unsubscribed before its completion.
|
||||||
*
|
*
|
||||||
|
@ -356,6 +357,10 @@ class MangaExtensionManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePendingUpdatesCount() {
|
private fun updatePendingUpdatesCount() {
|
||||||
preferences.mangaExtensionUpdatesCount().set(_installedExtensionsFlow.value.count { it.hasUpdate })
|
val pendingUpdateCount = _installedExtensionsFlow.value.count { it.hasUpdate }
|
||||||
|
preferences.mangaExtensionUpdatesCount().set(pendingUpdateCount)
|
||||||
|
if (pendingUpdateCount == 0) {
|
||||||
|
ExtensionUpdateNotifier(context).dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,14 +157,14 @@ class AnimeExtensionsScreenModel(
|
||||||
extensionManager.cancelInstallUpdateExtension(extension)
|
extensionManager.cancelInstallUpdateExtension(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDownloadState(extension: AnimeExtension) {
|
|
||||||
_currentDownloads.update { it - extension.pkgName }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addDownloadState(extension: AnimeExtension, installStep: InstallStep) {
|
private fun addDownloadState(extension: AnimeExtension, installStep: InstallStep) {
|
||||||
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
|
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeDownloadState(extension: AnimeExtension) {
|
||||||
|
_currentDownloads.update { it - extension.pkgName }
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: AnimeExtension) =
|
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: AnimeExtension) =
|
||||||
this
|
this
|
||||||
.onEach { installStep -> addDownloadState(extension, installStep) }
|
.onEach { installStep -> addDownloadState(extension, installStep) }
|
||||||
|
|
|
@ -4,28 +4,36 @@ import eu.kanade.domain.entries.anime.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
data class AnimeMigrationFlag(
|
||||||
|
val flag: Int,
|
||||||
|
val isDefaultSelected: Boolean,
|
||||||
|
val titleId: Int,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(flag: Int, defaultSelectionMap: Int, titleId: Int): AnimeMigrationFlag {
|
||||||
|
return AnimeMigrationFlag(
|
||||||
|
flag = flag,
|
||||||
|
isDefaultSelected = defaultSelectionMap and flag != 0,
|
||||||
|
titleId = titleId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object AnimeMigrationFlags {
|
object AnimeMigrationFlags {
|
||||||
|
|
||||||
private const val EPISODES = 0b00001
|
private const val EPISODES = 0b00001
|
||||||
private const val CATEGORIES = 0b00010
|
private const val CATEGORIES = 0b00010
|
||||||
private const val TRACK = 0b00100
|
|
||||||
private const val CUSTOM_COVER = 0b01000
|
private const val CUSTOM_COVER = 0b01000
|
||||||
private const val DELETE_DOWNLOADED = 0b10000
|
private const val DELETE_DOWNLOADED = 0b10000
|
||||||
|
|
||||||
private val coverCache: AnimeCoverCache by injectLazy()
|
private val coverCache: AnimeCoverCache by injectLazy()
|
||||||
private val getTracks: GetAnimeTracks = Injekt.get()
|
|
||||||
private val downloadCache: AnimeDownloadCache by injectLazy()
|
private val downloadCache: AnimeDownloadCache by injectLazy()
|
||||||
|
|
||||||
val flags get() = arrayOf(EPISODES, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED)
|
|
||||||
private var enableFlags = emptyList<Int>().toMutableList()
|
|
||||||
|
|
||||||
fun hasEpisodes(value: Int): Boolean {
|
fun hasEpisodes(value: Int): Boolean {
|
||||||
return value and EPISODES != 0
|
return value and EPISODES != 0
|
||||||
}
|
}
|
||||||
|
@ -34,10 +42,6 @@ object AnimeMigrationFlags {
|
||||||
return value and CATEGORIES != 0
|
return value and CATEGORIES != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasTracks(value: Int): Boolean {
|
|
||||||
return value and TRACK != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasCustomCover(value: Int): Boolean {
|
fun hasCustomCover(value: Int): Boolean {
|
||||||
return value and CUSTOM_COVER != 0
|
return value and CUSTOM_COVER != 0
|
||||||
}
|
}
|
||||||
|
@ -46,35 +50,32 @@ object AnimeMigrationFlags {
|
||||||
return value and DELETE_DOWNLOADED != 0
|
return value and DELETE_DOWNLOADED != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
/** Returns information about applicable flags with default selections. */
|
||||||
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
fun getFlags(anime: Anime?, defaultSelectedBitMap: Int): List<AnimeMigrationFlag> {
|
||||||
}
|
val flags = mutableListOf<AnimeMigrationFlag>()
|
||||||
|
flags += AnimeMigrationFlag.create(EPISODES, defaultSelectedBitMap, R.string.chapters)
|
||||||
|
flags += AnimeMigrationFlag.create(CATEGORIES, defaultSelectedBitMap, R.string.categories)
|
||||||
|
|
||||||
fun getFlagsFromPositions(positions: Array<Int>): Int {
|
|
||||||
val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] }
|
|
||||||
enableFlags.clear()
|
|
||||||
return fold
|
|
||||||
}
|
|
||||||
|
|
||||||
fun titles(anime: Anime?): Array<Int> {
|
|
||||||
enableFlags.add(EPISODES)
|
|
||||||
enableFlags.add(CATEGORIES)
|
|
||||||
val titles = arrayOf(R.string.episodes, R.string.anime_categories).toMutableList()
|
|
||||||
if (anime != null) {
|
if (anime != null) {
|
||||||
if (runBlocking { getTracks.await(anime.id) }.isNotEmpty()) {
|
|
||||||
titles.add(R.string.track)
|
|
||||||
enableFlags.add(TRACK)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anime.hasCustomCover(coverCache)) {
|
if (anime.hasCustomCover(coverCache)) {
|
||||||
titles.add(R.string.custom_cover)
|
flags += AnimeMigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, R.string.custom_cover)
|
||||||
enableFlags.add(CUSTOM_COVER)
|
|
||||||
}
|
}
|
||||||
if (downloadCache.getDownloadCount(anime) > 0) {
|
if (downloadCache.getDownloadCount(anime) > 0) {
|
||||||
titles.add(R.string.delete_downloaded)
|
flags += AnimeMigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, R.string.delete_downloaded)
|
||||||
enableFlags.add(DELETE_DOWNLOADED)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return titles.toTypedArray()
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a bit map of selected flags. */
|
||||||
|
fun getSelectedFlagsBitMap(
|
||||||
|
selectedFlags: List<Boolean>,
|
||||||
|
flags: List<AnimeMigrationFlag>,
|
||||||
|
): Int {
|
||||||
|
return selectedFlags
|
||||||
|
.zip(flags)
|
||||||
|
.filter { (isSelected, _) -> isSelected }
|
||||||
|
.map { (_, flag) -> flag.flag }
|
||||||
|
.reduceOrNull { acc, mask -> acc or mask } ?: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,14 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastForEachIndexed
|
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||||
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
||||||
|
@ -74,15 +73,8 @@ internal fun MigrateAnimeDialog(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
val activeFlags = remember { AnimeMigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) }
|
val flags = remember { AnimeMigrationFlags.getFlags(oldAnime, screenModel.migrateFlags.get()) }
|
||||||
val items = remember {
|
val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() }
|
||||||
AnimeMigrationFlags.titles(oldAnime)
|
|
||||||
.map { context.getString(it) }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
val selected = remember {
|
|
||||||
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.isMigrating) {
|
if (state.isMigrating) {
|
||||||
LoadingScreen(
|
LoadingScreen(
|
||||||
|
@ -99,18 +91,16 @@ internal fun MigrateAnimeDialog(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
items.forEachIndexed { index, title ->
|
flags.forEachIndexed { index, flag ->
|
||||||
val onChange: () -> Unit = {
|
val onChange = { selectedFlags[index] = !selectedFlags[index] }
|
||||||
selected[index] = !selected[index]
|
|
||||||
}
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onChange),
|
.clickable(onClick = onChange),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Checkbox(checked = selected[index], onCheckedChange = { onChange() })
|
Checkbox(checked = selectedFlags[index], onCheckedChange = { onChange() })
|
||||||
Text(text = title)
|
Text(text = context.getString(flag.titleId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +123,12 @@ internal fun MigrateAnimeDialog(
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
screenModel.migrateAnime(oldAnime, newAnime, false)
|
screenModel.migrateAnime(
|
||||||
|
oldAnime,
|
||||||
|
newAnime,
|
||||||
|
false,
|
||||||
|
AnimeMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
||||||
|
)
|
||||||
withUIContext { onPopScreen() }
|
withUIContext { onPopScreen() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -143,12 +138,13 @@ internal fun MigrateAnimeDialog(
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
val selectedIndices = mutableListOf<Int>()
|
screenModel.migrateAnime(
|
||||||
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) }
|
oldAnime,
|
||||||
val newValue =
|
newAnime,
|
||||||
AnimeMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
|
true,
|
||||||
screenModel.migrateFlags.set(newValue)
|
AnimeMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
||||||
screenModel.migrateAnime(oldAnime, newAnime, true)
|
)
|
||||||
|
|
||||||
withUIContext { onPopScreen() }
|
withUIContext { onPopScreen() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -184,7 +180,13 @@ internal class MigrateAnimeDialogScreenModel(
|
||||||
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedAnimeTrackService>()
|
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedAnimeTrackService>()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun migrateAnime(oldAnime: Anime, newAnime: Anime, replace: Boolean) {
|
suspend fun migrateAnime(
|
||||||
|
oldAnime: Anime,
|
||||||
|
newAnime: Anime,
|
||||||
|
replace: Boolean,
|
||||||
|
flags: Int,
|
||||||
|
) {
|
||||||
|
migrateFlags.set(flags)
|
||||||
val source = sourceManager.get(newAnime.source) ?: return
|
val source = sourceManager.get(newAnime.source) ?: return
|
||||||
val prevSource = sourceManager.get(oldAnime.source)
|
val prevSource = sourceManager.get(oldAnime.source)
|
||||||
|
|
||||||
|
@ -200,6 +202,7 @@ internal class MigrateAnimeDialogScreenModel(
|
||||||
newAnime = newAnime,
|
newAnime = newAnime,
|
||||||
sourceEpisodes = episodes,
|
sourceEpisodes = episodes,
|
||||||
replace = replace,
|
replace = replace,
|
||||||
|
flags = flags,
|
||||||
)
|
)
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
|
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
|
||||||
|
@ -215,12 +218,10 @@ internal class MigrateAnimeDialogScreenModel(
|
||||||
newAnime: Anime,
|
newAnime: Anime,
|
||||||
sourceEpisodes: List<SEpisode>,
|
sourceEpisodes: List<SEpisode>,
|
||||||
replace: Boolean,
|
replace: Boolean,
|
||||||
|
flags: Int,
|
||||||
) {
|
) {
|
||||||
val flags = migrateFlags.get()
|
|
||||||
|
|
||||||
val migrateEpisodes = AnimeMigrationFlags.hasEpisodes(flags)
|
val migrateEpisodes = AnimeMigrationFlags.hasEpisodes(flags)
|
||||||
val migrateCategories = AnimeMigrationFlags.hasCategories(flags)
|
val migrateCategories = AnimeMigrationFlags.hasCategories(flags)
|
||||||
val migrateTracks = AnimeMigrationFlags.hasTracks(flags)
|
|
||||||
val migrateCustomCover = AnimeMigrationFlags.hasCustomCover(flags)
|
val migrateCustomCover = AnimeMigrationFlags.hasCustomCover(flags)
|
||||||
val deleteDownloaded = AnimeMigrationFlags.hasDeleteDownloaded(flags)
|
val deleteDownloaded = AnimeMigrationFlags.hasDeleteDownloaded(flags)
|
||||||
|
|
||||||
|
@ -271,21 +272,20 @@ internal class MigrateAnimeDialogScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update track
|
// Update track
|
||||||
if (migrateTracks) {
|
getTracks.await(oldAnime.id).mapNotNull { track ->
|
||||||
val tracks = getTracks.await(oldAnime.id).mapNotNull { track ->
|
val updatedTrack = track.copy(animeId = newAnime.id)
|
||||||
val updatedTrack = track.copy(animeId = newAnime.id)
|
|
||||||
|
|
||||||
val service = enhancedServices
|
val service = enhancedServices
|
||||||
.firstOrNull { it.isTrackFrom(updatedTrack, oldAnime, oldSource) }
|
.firstOrNull { it.isTrackFrom(updatedTrack, oldAnime, oldSource) }
|
||||||
|
|
||||||
if (service != null) {
|
if (service != null) {
|
||||||
service.migrateTrack(updatedTrack, newAnime, newSource)
|
service.migrateTrack(updatedTrack, newAnime, newSource)
|
||||||
} else {
|
} else {
|
||||||
updatedTrack
|
updatedTrack
|
||||||
}
|
|
||||||
}
|
}
|
||||||
insertTrack.awaitAll(tracks)
|
|
||||||
}
|
}
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { insertTrack.awaitAll(it) }
|
||||||
|
|
||||||
// Delete downloaded
|
// Delete downloaded
|
||||||
if (deleteDownloaded) {
|
if (deleteDownloaded) {
|
||||||
|
|
|
@ -158,14 +158,14 @@ class MangaExtensionsScreenModel(
|
||||||
extensionManager.cancelInstallUpdateExtension(extension)
|
extensionManager.cancelInstallUpdateExtension(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeDownloadState(extension: MangaExtension) {
|
|
||||||
_currentDownloads.update { it - extension.pkgName }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addDownloadState(extension: MangaExtension, installStep: InstallStep) {
|
private fun addDownloadState(extension: MangaExtension, installStep: InstallStep) {
|
||||||
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
|
_currentDownloads.update { it + Pair(extension.pkgName, installStep) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeDownloadState(extension: MangaExtension) {
|
||||||
|
_currentDownloads.update { it - extension.pkgName }
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: MangaExtension) =
|
private suspend fun Flow<InstallStep>.collectToInstallUpdate(extension: MangaExtension) =
|
||||||
this
|
this
|
||||||
.onEach { installStep -> addDownloadState(extension, installStep) }
|
.onEach { installStep -> addDownloadState(extension, installStep) }
|
||||||
|
|
|
@ -4,28 +4,36 @@ import eu.kanade.domain.entries.manga.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||||
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
data class MangaMigrationFlag(
|
||||||
|
val flag: Int,
|
||||||
|
val isDefaultSelected: Boolean,
|
||||||
|
val titleId: Int,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(flag: Int, defaultSelectionMap: Int, titleId: Int): MangaMigrationFlag {
|
||||||
|
return MangaMigrationFlag(
|
||||||
|
flag = flag,
|
||||||
|
isDefaultSelected = defaultSelectionMap and flag != 0,
|
||||||
|
titleId = titleId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object MangaMigrationFlags {
|
object MangaMigrationFlags {
|
||||||
|
|
||||||
private const val CHAPTERS = 0b00001
|
private const val CHAPTERS = 0b00001
|
||||||
private const val CATEGORIES = 0b00010
|
private const val CATEGORIES = 0b00010
|
||||||
private const val TRACK = 0b00100
|
|
||||||
private const val CUSTOM_COVER = 0b01000
|
private const val CUSTOM_COVER = 0b01000
|
||||||
private const val DELETE_DOWNLOADED = 0b10000
|
private const val DELETE_DOWNLOADED = 0b10000
|
||||||
|
|
||||||
private val coverCache: MangaCoverCache by injectLazy()
|
private val coverCache: MangaCoverCache by injectLazy()
|
||||||
private val getTracks: GetMangaTracks = Injekt.get()
|
|
||||||
private val downloadCache: MangaDownloadCache by injectLazy()
|
private val downloadCache: MangaDownloadCache by injectLazy()
|
||||||
|
|
||||||
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER, DELETE_DOWNLOADED)
|
|
||||||
private var enableFlags = emptyList<Int>().toMutableList()
|
|
||||||
|
|
||||||
fun hasChapters(value: Int): Boolean {
|
fun hasChapters(value: Int): Boolean {
|
||||||
return value and CHAPTERS != 0
|
return value and CHAPTERS != 0
|
||||||
}
|
}
|
||||||
|
@ -34,10 +42,6 @@ object MangaMigrationFlags {
|
||||||
return value and CATEGORIES != 0
|
return value and CATEGORIES != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasTracks(value: Int): Boolean {
|
|
||||||
return value and TRACK != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasCustomCover(value: Int): Boolean {
|
fun hasCustomCover(value: Int): Boolean {
|
||||||
return value and CUSTOM_COVER != 0
|
return value and CUSTOM_COVER != 0
|
||||||
}
|
}
|
||||||
|
@ -46,34 +50,32 @@ object MangaMigrationFlags {
|
||||||
return value and DELETE_DOWNLOADED != 0
|
return value and DELETE_DOWNLOADED != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEnabledFlagsPositions(value: Int): List<Int> {
|
/** Returns information about applicable flags with default selections. */
|
||||||
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
|
fun getFlags(manga: Manga?, defaultSelectedBitMap: Int): List<MangaMigrationFlag> {
|
||||||
}
|
val flags = mutableListOf<MangaMigrationFlag>()
|
||||||
|
flags += MangaMigrationFlag.create(CHAPTERS, defaultSelectedBitMap, R.string.chapters)
|
||||||
|
flags += MangaMigrationFlag.create(CATEGORIES, defaultSelectedBitMap, R.string.categories)
|
||||||
|
|
||||||
fun getFlagsFromPositions(positions: Array<Int>): Int {
|
|
||||||
val fold = positions.fold(0) { accumulated, position -> accumulated or enableFlags[position] }
|
|
||||||
enableFlags.clear()
|
|
||||||
return fold
|
|
||||||
}
|
|
||||||
|
|
||||||
fun titles(manga: Manga?): Array<Int> {
|
|
||||||
enableFlags.add(CHAPTERS)
|
|
||||||
enableFlags.add(CATEGORIES)
|
|
||||||
val titles = arrayOf(R.string.chapters, R.string.manga_categories).toMutableList()
|
|
||||||
if (manga != null) {
|
if (manga != null) {
|
||||||
if (runBlocking { getTracks.await(manga.id) }.isNotEmpty()) {
|
|
||||||
titles.add(R.string.track)
|
|
||||||
enableFlags.add(TRACK)
|
|
||||||
}
|
|
||||||
if (manga.hasCustomCover(coverCache)) {
|
if (manga.hasCustomCover(coverCache)) {
|
||||||
titles.add(R.string.custom_cover)
|
flags += MangaMigrationFlag.create(CUSTOM_COVER, defaultSelectedBitMap, R.string.custom_cover)
|
||||||
enableFlags.add(CUSTOM_COVER)
|
|
||||||
}
|
}
|
||||||
if (downloadCache.getDownloadCount(manga) > 0) {
|
if (downloadCache.getDownloadCount(manga) > 0) {
|
||||||
titles.add(R.string.delete_downloaded)
|
flags += MangaMigrationFlag.create(DELETE_DOWNLOADED, defaultSelectedBitMap, R.string.delete_downloaded)
|
||||||
enableFlags.add(DELETE_DOWNLOADED)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return titles.toTypedArray()
|
return flags
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a bit map of selected flags. */
|
||||||
|
fun getSelectedFlagsBitMap(
|
||||||
|
selectedFlags: List<Boolean>,
|
||||||
|
flags: List<MangaMigrationFlag>,
|
||||||
|
): Int {
|
||||||
|
return selectedFlags
|
||||||
|
.zip(flags)
|
||||||
|
.filter { (isSelected, _) -> isSelected }
|
||||||
|
.map { (_, flag) -> flag.flag }
|
||||||
|
.reduceOrNull { acc, mask -> acc or mask } ?: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,15 +19,14 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastForEachIndexed
|
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
||||||
|
@ -74,15 +73,8 @@ internal fun MigrateMangaDialog(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
val activeFlags = remember { MangaMigrationFlags.getEnabledFlagsPositions(screenModel.migrateFlags.get()) }
|
val flags = remember { MangaMigrationFlags.getFlags(oldManga, screenModel.migrateFlags.get()) }
|
||||||
val items = remember {
|
val selectedFlags = remember { flags.map { it.isDefaultSelected }.toMutableStateList() }
|
||||||
MangaMigrationFlags.titles(oldManga)
|
|
||||||
.map { context.getString(it) }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
val selected = remember {
|
|
||||||
mutableStateListOf(*List(items.size) { i -> activeFlags.contains(i) }.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.isMigrating) {
|
if (state.isMigrating) {
|
||||||
LoadingScreen(
|
LoadingScreen(
|
||||||
|
@ -99,18 +91,16 @@ internal fun MigrateMangaDialog(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
items.forEachIndexed { index, title ->
|
flags.forEachIndexed { index, flag ->
|
||||||
val onChange: () -> Unit = {
|
val onChange = { selectedFlags[index] = !selectedFlags[index] }
|
||||||
selected[index] = !selected[index]
|
|
||||||
}
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onChange),
|
.clickable(onClick = onChange),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Checkbox(checked = selected[index], onCheckedChange = { onChange() })
|
Checkbox(checked = selectedFlags[index], onCheckedChange = { onChange() })
|
||||||
Text(text = title)
|
Text(text = context.getString(flag.titleId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,7 +123,12 @@ internal fun MigrateMangaDialog(
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
screenModel.migrateManga(oldManga, newManga, false)
|
screenModel.migrateManga(
|
||||||
|
oldManga,
|
||||||
|
newManga,
|
||||||
|
false,
|
||||||
|
MangaMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
||||||
|
)
|
||||||
withUIContext { onPopScreen() }
|
withUIContext { onPopScreen() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -143,12 +138,13 @@ internal fun MigrateMangaDialog(
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launchIO {
|
scope.launchIO {
|
||||||
val selectedIndices = mutableListOf<Int>()
|
screenModel.migrateManga(
|
||||||
selected.fastForEachIndexed { i, b -> if (b) selectedIndices.add(i) }
|
oldManga,
|
||||||
val newValue =
|
newManga,
|
||||||
MangaMigrationFlags.getFlagsFromPositions(selectedIndices.toTypedArray())
|
true,
|
||||||
screenModel.migrateFlags.set(newValue)
|
MangaMigrationFlags.getSelectedFlagsBitMap(selectedFlags, flags),
|
||||||
screenModel.migrateManga(oldManga, newManga, true)
|
)
|
||||||
|
|
||||||
withUIContext { onPopScreen() }
|
withUIContext { onPopScreen() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -184,7 +180,13 @@ internal class MigrateMangaDialogScreenModel(
|
||||||
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedMangaTrackService>()
|
Injekt.get<TrackManager>().services.filterIsInstance<EnhancedMangaTrackService>()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun migrateManga(oldManga: Manga, newManga: Manga, replace: Boolean) {
|
suspend fun migrateManga(
|
||||||
|
oldManga: Manga,
|
||||||
|
newManga: Manga,
|
||||||
|
replace: Boolean,
|
||||||
|
flags: Int,
|
||||||
|
) {
|
||||||
|
migrateFlags.set(flags)
|
||||||
val source = sourceManager.get(newManga.source) ?: return
|
val source = sourceManager.get(newManga.source) ?: return
|
||||||
val prevSource = sourceManager.get(oldManga.source)
|
val prevSource = sourceManager.get(oldManga.source)
|
||||||
|
|
||||||
|
@ -200,6 +202,7 @@ internal class MigrateMangaDialogScreenModel(
|
||||||
newManga = newManga,
|
newManga = newManga,
|
||||||
sourceChapters = chapters,
|
sourceChapters = chapters,
|
||||||
replace = replace,
|
replace = replace,
|
||||||
|
flags = flags,
|
||||||
)
|
)
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
|
// Explicitly stop if an error occurred; the dialog normally gets popped at the end
|
||||||
|
@ -215,12 +218,10 @@ internal class MigrateMangaDialogScreenModel(
|
||||||
newManga: Manga,
|
newManga: Manga,
|
||||||
sourceChapters: List<SChapter>,
|
sourceChapters: List<SChapter>,
|
||||||
replace: Boolean,
|
replace: Boolean,
|
||||||
|
flags: Int,
|
||||||
) {
|
) {
|
||||||
val flags = migrateFlags.get()
|
|
||||||
|
|
||||||
val migrateChapters = MangaMigrationFlags.hasChapters(flags)
|
val migrateChapters = MangaMigrationFlags.hasChapters(flags)
|
||||||
val migrateCategories = MangaMigrationFlags.hasCategories(flags)
|
val migrateCategories = MangaMigrationFlags.hasCategories(flags)
|
||||||
val migrateTracks = MangaMigrationFlags.hasTracks(flags)
|
|
||||||
val migrateCustomCover = MangaMigrationFlags.hasCustomCover(flags)
|
val migrateCustomCover = MangaMigrationFlags.hasCustomCover(flags)
|
||||||
val deleteDownloaded = MangaMigrationFlags.hasDeleteDownloaded(flags)
|
val deleteDownloaded = MangaMigrationFlags.hasDeleteDownloaded(flags)
|
||||||
|
|
||||||
|
@ -271,21 +272,20 @@ internal class MigrateMangaDialogScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update track
|
// Update track
|
||||||
if (migrateTracks) {
|
getTracks.await(oldManga.id).mapNotNull { track ->
|
||||||
val tracks = getTracks.await(oldManga.id).mapNotNull { track ->
|
val updatedTrack = track.copy(mangaId = newManga.id)
|
||||||
val updatedTrack = track.copy(mangaId = newManga.id)
|
|
||||||
|
|
||||||
val service = enhancedServices
|
val service = enhancedServices
|
||||||
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
|
.firstOrNull { it.isTrackFrom(updatedTrack, oldManga, oldSource) }
|
||||||
|
|
||||||
if (service != null) {
|
if (service != null) {
|
||||||
service.migrateTrack(updatedTrack, newManga, newSource)
|
service.migrateTrack(updatedTrack, newManga, newSource)
|
||||||
} else {
|
} else {
|
||||||
updatedTrack
|
updatedTrack
|
||||||
}
|
|
||||||
}
|
}
|
||||||
insertTrack.awaitAll(tracks)
|
|
||||||
}
|
}
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { insertTrack.awaitAll(it) }
|
||||||
|
|
||||||
// Delete downloaded
|
// Delete downloaded
|
||||||
if (deleteDownloaded) {
|
if (deleteDownloaded) {
|
||||||
|
|
|
@ -89,13 +89,6 @@ class AnimeScreen(
|
||||||
|
|
||||||
val successState = state as AnimeScreenModel.State.Success
|
val successState = state as AnimeScreenModel.State.Success
|
||||||
val isAnimeHttpSource = remember { successState.source is AnimeHttpSource }
|
val isAnimeHttpSource = remember { successState.source is AnimeHttpSource }
|
||||||
val fetchInterval = remember(successState.anime.fetchInterval) {
|
|
||||||
FetchAnimeInterval(
|
|
||||||
interval = successState.anime.fetchInterval,
|
|
||||||
leadDays = screenModel.leadDay,
|
|
||||||
followDays = screenModel.followDay,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(successState.anime, screenModel.source) {
|
LaunchedEffect(successState.anime, screenModel.source) {
|
||||||
if (isAnimeHttpSource) {
|
if (isAnimeHttpSource) {
|
||||||
|
@ -113,7 +106,7 @@ class AnimeScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
dateFormat = screenModel.dateFormat,
|
dateFormat = screenModel.dateFormat,
|
||||||
fetchInterval = fetchInterval,
|
fetchInterval = successState.anime.fetchInterval,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
episodeSwipeStartAction = screenModel.episodeSwipeStartAction,
|
episodeSwipeStartAction = screenModel.episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = screenModel.episodeSwipeEndAction,
|
episodeSwipeEndAction = screenModel.episodeSwipeEndAction,
|
||||||
|
@ -244,7 +237,7 @@ class AnimeScreen(
|
||||||
}
|
}
|
||||||
is AnimeScreenModel.Dialog.SetAnimeFetchInterval -> {
|
is AnimeScreenModel.Dialog.SetAnimeFetchInterval -> {
|
||||||
SetIntervalDialog(
|
SetIntervalDialog(
|
||||||
interval = if (dialog.anime.fetchInterval < 0) -dialog.anime.fetchInterval else 0,
|
interval = dialog.anime.fetchInterval,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onValueChanged = { screenModel.setFetchInterval(dialog.anime, it) },
|
onValueChanged = { screenModel.setFetchInterval(dialog.anime, it) },
|
||||||
)
|
)
|
||||||
|
|
|
@ -135,8 +135,6 @@ class AnimeScreenModel(
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
|
|
||||||
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
val leadDay = libraryPreferences.leadingAnimeExpectedDays().get()
|
|
||||||
val followDay = libraryPreferences.followingAnimeExpectedDays().get()
|
|
||||||
|
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||||
private val selectedEpisodeIds: HashSet<Long> = HashSet()
|
private val selectedEpisodeIds: HashSet<Long> = HashSet()
|
||||||
|
@ -377,20 +375,14 @@ class AnimeScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFetchInterval(anime: Anime, newInterval: Int) {
|
fun setFetchInterval(anime: Anime, interval: Int) {
|
||||||
val interval = when (newInterval) {
|
|
||||||
// reset interval 0 default to trigger recalculation
|
|
||||||
// only reset if interval is custom, which is negative
|
|
||||||
0 -> if (anime.fetchInterval < 0) 0 else anime.fetchInterval
|
|
||||||
else -> -newInterval
|
|
||||||
}
|
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
updateAnime.awaitUpdateFetchInterval(
|
updateAnime.awaitUpdateFetchInterval(
|
||||||
anime.copy(fetchInterval = interval),
|
// Custom intervals are negative
|
||||||
successState?.episodes?.map { it.episode }.orEmpty(),
|
anime.copy(fetchInterval = -interval),
|
||||||
)
|
)
|
||||||
val newAnime = animeRepository.getAnimeById(animeId)
|
val updatedAnime = animeRepository.getAnimeById(anime.id)
|
||||||
updateSuccessState { it.copy(anime = newAnime) }
|
updateSuccessState { it.copy(anime = updatedAnime) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1105,10 +1097,3 @@ data class EpisodeItem(
|
||||||
) {
|
) {
|
||||||
val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED
|
val isDownloaded = downloadState == AnimeDownload.State.DOWNLOADED
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class FetchAnimeInterval(
|
|
||||||
val interval: Int,
|
|
||||||
val leadDays: Int,
|
|
||||||
val followDays: Int,
|
|
||||||
)
|
|
||||||
|
|
|
@ -84,13 +84,6 @@ class MangaScreen(
|
||||||
|
|
||||||
val successState = state as MangaScreenModel.State.Success
|
val successState = state as MangaScreenModel.State.Success
|
||||||
val isHttpSource = remember { successState.source is HttpSource }
|
val isHttpSource = remember { successState.source is HttpSource }
|
||||||
val fetchInterval = remember(successState.manga.fetchInterval) {
|
|
||||||
FetchMangaInterval(
|
|
||||||
interval = successState.manga.fetchInterval,
|
|
||||||
leadDays = screenModel.leadDay,
|
|
||||||
followDays = screenModel.followDay,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(successState.manga, screenModel.source) {
|
LaunchedEffect(successState.manga, screenModel.source) {
|
||||||
if (isHttpSource) {
|
if (isHttpSource) {
|
||||||
|
@ -108,7 +101,7 @@ class MangaScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
dateFormat = screenModel.dateFormat,
|
dateFormat = screenModel.dateFormat,
|
||||||
fetchInterval = fetchInterval,
|
fetchInterval = successState.manga.fetchInterval,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||||
|
@ -226,7 +219,7 @@ class MangaScreen(
|
||||||
}
|
}
|
||||||
is MangaScreenModel.Dialog.SetMangaFetchInterval -> {
|
is MangaScreenModel.Dialog.SetMangaFetchInterval -> {
|
||||||
SetIntervalDialog(
|
SetIntervalDialog(
|
||||||
interval = if (dialog.manga.fetchInterval < 0) -dialog.manga.fetchInterval else 0,
|
interval = dialog.manga.fetchInterval,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
|
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
|
||||||
)
|
)
|
||||||
|
|
|
@ -131,8 +131,6 @@ class MangaScreenModel(
|
||||||
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
||||||
|
|
||||||
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
||||||
val leadDay = libraryPreferences.leadingMangaExpectedDays().get()
|
|
||||||
val followDay = libraryPreferences.followingMangaExpectedDays().get()
|
|
||||||
|
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||||
private val selectedChapterIds: HashSet<Long> = HashSet()
|
private val selectedChapterIds: HashSet<Long> = HashSet()
|
||||||
|
@ -374,20 +372,14 @@ class MangaScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFetchInterval(manga: Manga, newInterval: Int) {
|
fun setFetchInterval(manga: Manga, interval: Int) {
|
||||||
val interval = when (newInterval) {
|
|
||||||
// reset interval 0 default to trigger recalculation
|
|
||||||
// only reset if interval is custom, which is negative
|
|
||||||
0 -> if (manga.fetchInterval < 0) 0 else manga.fetchInterval
|
|
||||||
else -> -newInterval
|
|
||||||
}
|
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
updateManga.awaitUpdateFetchInterval(
|
updateManga.awaitUpdateFetchInterval(
|
||||||
manga.copy(fetchInterval = interval),
|
// Custom intervals are negative
|
||||||
successState?.chapters?.map { it.chapter }.orEmpty(),
|
manga.copy(fetchInterval = -interval),
|
||||||
)
|
)
|
||||||
val newManga = mangaRepository.getMangaById(mangaId)
|
val updatedManga = mangaRepository.getMangaById(manga.id)
|
||||||
updateSuccessState { it.copy(manga = newManga) }
|
updateSuccessState { it.copy(manga = updatedManga) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1073,10 +1065,3 @@ data class ChapterItem(
|
||||||
) {
|
) {
|
||||||
val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED
|
val isDownloaded = downloadState == MangaDownload.State.DOWNLOADED
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class FetchMangaInterval(
|
|
||||||
val interval: Int,
|
|
||||||
val leadDays: Int,
|
|
||||||
val followDays: Int,
|
|
||||||
)
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.view.animation.AnimationUtils
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
@ -48,9 +49,12 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.entries.manga.model.orientationType
|
import eu.kanade.presentation.reader.BottomReaderBar
|
||||||
import eu.kanade.presentation.reader.ChapterNavigator
|
import eu.kanade.presentation.reader.ChapterNavigator
|
||||||
|
import eu.kanade.presentation.reader.OrientationModeSelectDialog
|
||||||
import eu.kanade.presentation.reader.PageIndicatorText
|
import eu.kanade.presentation.reader.PageIndicatorText
|
||||||
|
import eu.kanade.presentation.reader.ReaderPageActionsDialog
|
||||||
|
import eu.kanade.presentation.reader.ReadingModeSelectDialog
|
||||||
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
|
import eu.kanade.presentation.reader.settings.ReaderSettingsDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.core.Constants
|
import eu.kanade.tachiyomi.core.Constants
|
||||||
|
@ -78,10 +82,7 @@ import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import eu.kanade.tachiyomi.util.system.isNightMode
|
import eu.kanade.tachiyomi.util.system.isNightMode
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.copy
|
|
||||||
import eu.kanade.tachiyomi.util.view.popupMenu
|
|
||||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||||
import eu.kanade.tachiyomi.util.view.setTooltip
|
|
||||||
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
|
@ -93,12 +94,12 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.preference.toggle
|
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withUIContext
|
import tachiyomi.core.util.lang.withUIContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import tachiyomi.presentation.widget.util.stringResource
|
import tachiyomi.presentation.widget.util.stringResource
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -124,7 +125,7 @@ class ReaderActivity : BaseActivity() {
|
||||||
val viewModel by viewModels<ReaderViewModel>()
|
val viewModel by viewModels<ReaderViewModel>()
|
||||||
private var assistUrl: String? = null
|
private var assistUrl: String? = null
|
||||||
|
|
||||||
val hasCutout by lazy { hasDisplayCutout() }
|
private val hasCutout by lazy { hasDisplayCutout() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration at reader level, like background color or forced orientation.
|
* Configuration at reader level, like background color or forced orientation.
|
||||||
|
@ -391,11 +392,12 @@ class ReaderActivity : BaseActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.dialogRoot.setComposeContent {
|
binding.readerMenuBottom.setComposeContent {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val settingsScreenModel = remember {
|
val settingsScreenModel = remember {
|
||||||
ReaderSettingsScreenModel(
|
ReaderSettingsScreenModel(
|
||||||
readerState = viewModel.state,
|
readerState = viewModel.state,
|
||||||
|
hasDisplayCutout = hasCutout,
|
||||||
onChangeReadingMode = viewModel::setMangaReadingMode,
|
onChangeReadingMode = viewModel::setMangaReadingMode,
|
||||||
onChangeOrientation = viewModel::setMangaOrientationType,
|
onChangeOrientation = viewModel::setMangaOrientationType,
|
||||||
)
|
)
|
||||||
|
@ -426,6 +428,28 @@ class ReaderActivity : BaseActivity() {
|
||||||
screenModel = settingsScreenModel,
|
screenModel = settingsScreenModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is ReaderViewModel.Dialog.ReadingModeSelect -> {
|
||||||
|
ReadingModeSelectDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
screenModel = settingsScreenModel,
|
||||||
|
onChange = { stringRes ->
|
||||||
|
menuToggleToast?.cancel()
|
||||||
|
if (!readerPreferences.showReadingMode().get()) {
|
||||||
|
menuToggleToast = toast(stringRes)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ReaderViewModel.Dialog.OrientationModeSelect -> {
|
||||||
|
OrientationModeSelectDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
screenModel = settingsScreenModel,
|
||||||
|
onChange = { stringRes ->
|
||||||
|
menuToggleToast?.cancel()
|
||||||
|
menuToggleToast = toast(stringRes)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
is ReaderViewModel.Dialog.PageActions -> {
|
is ReaderViewModel.Dialog.PageActions -> {
|
||||||
ReaderPageActionsDialog(
|
ReaderPageActionsDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -438,36 +462,61 @@ class ReaderActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init listeners on bottom menu
|
binding.readerMenuBottom.setComposeContent {
|
||||||
binding.readerNav.setComposeContent {
|
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
if (state.viewer == null) return@setComposeContent
|
if (state.viewer == null) return@setComposeContent
|
||||||
val isRtl = state.viewer is R2LPagerViewer
|
val isRtl = state.viewer is R2LPagerViewer
|
||||||
|
|
||||||
ChapterNavigator(
|
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
|
||||||
isRtl = isRtl,
|
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
|
||||||
onNextChapter = ::loadNextChapter,
|
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
|
||||||
enabledNext = state.viewerChapters?.nextChapter != null,
|
val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon
|
||||||
onPreviousChapter = ::loadPreviousChapter,
|
|
||||||
enabledPrevious = state.viewerChapters?.prevChapter != null,
|
|
||||||
currentPage = state.currentPage,
|
|
||||||
totalPages = state.totalPages,
|
|
||||||
onSliderValueChange = {
|
|
||||||
isScrollingThroughPages = true
|
|
||||||
moveToPageIndex(it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
initBottomShortcuts()
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
ChapterNavigator(
|
||||||
|
isRtl = isRtl,
|
||||||
|
onNextChapter = ::loadNextChapter,
|
||||||
|
enabledNext = state.viewerChapters?.nextChapter != null,
|
||||||
|
onPreviousChapter = ::loadPreviousChapter,
|
||||||
|
enabledPrevious = state.viewerChapters?.prevChapter != null,
|
||||||
|
currentPage = state.currentPage,
|
||||||
|
totalPages = state.totalPages,
|
||||||
|
onSliderValueChange = {
|
||||||
|
isScrollingThroughPages = true
|
||||||
|
moveToPageIndex(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
BottomReaderBar(
|
||||||
|
readingMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false)),
|
||||||
|
onClickReadingMode = viewModel::openReadingModeSelectDialog,
|
||||||
|
orientationMode = OrientationType.fromPreference(viewModel.getMangaOrientationType(resolveDefault = false)),
|
||||||
|
onClickOrientationMode = viewModel::openOrientationModeSelectDialog,
|
||||||
|
cropEnabled = cropEnabled,
|
||||||
|
onClickCropBorder = {
|
||||||
|
val enabled = viewModel.toggleCropBorders()
|
||||||
|
|
||||||
|
menuToggleToast?.cancel()
|
||||||
|
menuToggleToast = toast(
|
||||||
|
if (enabled) {
|
||||||
|
R.string.on
|
||||||
|
} else {
|
||||||
|
R.string.off
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClickSettings = viewModel::openSettingsDialog,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply {
|
val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply {
|
||||||
elevation = resources.getDimension(R.dimen.m3_sys_elevation_level2)
|
elevation = resources.getDimension(R.dimen.m3_sys_elevation_level2)
|
||||||
alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light
|
alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light
|
||||||
}
|
}
|
||||||
binding.toolbarBottom.background = toolbarBackground.copy(this@ReaderActivity)
|
|
||||||
|
|
||||||
val toolbarColor = ColorUtils.setAlphaComponent(
|
val toolbarColor = ColorUtils.setAlphaComponent(
|
||||||
toolbarBackground.resolvedTintColor,
|
toolbarBackground.resolvedTintColor,
|
||||||
toolbarBackground.alpha,
|
toolbarBackground.alpha,
|
||||||
|
@ -481,112 +530,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
setMenuVisibility(viewModel.state.value.menuVisible)
|
setMenuVisibility(viewModel.state.value.menuVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initBottomShortcuts() {
|
|
||||||
// Reading mode
|
|
||||||
with(binding.actionReadingMode) {
|
|
||||||
setTooltip(R.string.viewer)
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
popupMenu(
|
|
||||||
items = ReadingModeType.entries.map { it.flagValue to it.stringRes },
|
|
||||||
selectedItemId = viewModel.getMangaReadingMode(resolveDefault = false),
|
|
||||||
) {
|
|
||||||
val newReadingMode = ReadingModeType.fromPreference(itemId)
|
|
||||||
|
|
||||||
viewModel.setMangaReadingMode(newReadingMode)
|
|
||||||
|
|
||||||
menuToggleToast?.cancel()
|
|
||||||
if (!readerPreferences.showReadingMode().get()) {
|
|
||||||
menuToggleToast = toast(newReadingMode.stringRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCropBordersShortcut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crop borders
|
|
||||||
with(binding.actionCropBorders) {
|
|
||||||
setTooltip(R.string.pref_crop_borders)
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
|
|
||||||
val enabled = if (isPagerType) {
|
|
||||||
readerPreferences.cropBorders().toggle()
|
|
||||||
} else {
|
|
||||||
readerPreferences.cropBordersWebtoon().toggle()
|
|
||||||
}
|
|
||||||
|
|
||||||
menuToggleToast?.cancel()
|
|
||||||
menuToggleToast = toast(
|
|
||||||
if (enabled) {
|
|
||||||
R.string.on
|
|
||||||
} else {
|
|
||||||
R.string.off
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateCropBordersShortcut()
|
|
||||||
listOf(readerPreferences.cropBorders(), readerPreferences.cropBordersWebtoon())
|
|
||||||
.forEach { pref ->
|
|
||||||
pref.changes()
|
|
||||||
.onEach { updateCropBordersShortcut() }
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotation
|
|
||||||
with(binding.actionRotation) {
|
|
||||||
setTooltip(R.string.rotation_type)
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
popupMenu(
|
|
||||||
items = OrientationType.entries.map { it.flagValue to it.stringRes },
|
|
||||||
selectedItemId = viewModel.manga?.orientationType?.toInt()
|
|
||||||
?: readerPreferences.defaultOrientationType().get(),
|
|
||||||
) {
|
|
||||||
val newOrientation = OrientationType.fromPreference(itemId)
|
|
||||||
|
|
||||||
viewModel.setMangaOrientationType(newOrientation)
|
|
||||||
|
|
||||||
menuToggleToast?.cancel()
|
|
||||||
menuToggleToast = toast(newOrientation.stringRes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings sheet
|
|
||||||
with(binding.actionSettings) {
|
|
||||||
setTooltip(R.string.action_settings)
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
viewModel.openSettingsDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateOrientationShortcut(preference: Int) {
|
|
||||||
val orientation = OrientationType.fromPreference(preference)
|
|
||||||
binding.actionRotation.setImageResource(orientation.iconRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateCropBordersShortcut() {
|
|
||||||
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
|
|
||||||
val enabled = if (isPagerType) {
|
|
||||||
readerPreferences.cropBorders().get()
|
|
||||||
} else {
|
|
||||||
readerPreferences.cropBordersWebtoon().get()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.actionCropBorders.setImageResource(
|
|
||||||
if (enabled) {
|
|
||||||
R.drawable.ic_crop_24dp
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_crop_off_24dp
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the visibility of the menu according to [visible] and with an optional parameter to
|
* Sets the visibility of the menu according to [visible] and with an optional parameter to
|
||||||
* [animate] the views.
|
* [animate] the views.
|
||||||
|
@ -653,13 +596,8 @@ class ReaderActivity : BaseActivity() {
|
||||||
*/
|
*/
|
||||||
private fun setManga(manga: Manga) {
|
private fun setManga(manga: Manga) {
|
||||||
val prevViewer = viewModel.state.value.viewer
|
val prevViewer = viewModel.state.value.viewer
|
||||||
|
|
||||||
val viewerMode = ReadingModeType.fromPreference(viewModel.getMangaReadingMode(resolveDefault = false))
|
|
||||||
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
|
|
||||||
|
|
||||||
val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this)
|
val newViewer = ReadingModeType.toViewer(viewModel.getMangaReadingMode(), this)
|
||||||
|
|
||||||
updateCropBordersShortcut()
|
|
||||||
if (window.sharedElementEnterTransition is MaterialContainerTransform) {
|
if (window.sharedElementEnterTransition is MaterialContainerTransform) {
|
||||||
// Wait until transition is complete to avoid crash on API 26
|
// Wait until transition is complete to avoid crash on API 26
|
||||||
window.sharedElementEnterTransition.doOnEnd {
|
window.sharedElementEnterTransition.doOnEnd {
|
||||||
|
@ -894,7 +832,6 @@ class ReaderActivity : BaseActivity() {
|
||||||
if (newOrientation.flag != requestedOrientation) {
|
if (newOrientation.flag != requestedOrientation) {
|
||||||
requestedOrientation = newOrientation.flag
|
requestedOrientation = newOrientation.flag
|
||||||
}
|
}
|
||||||
updateOrientationShortcut(viewModel.getMangaOrientationType(resolveDefault = false))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -61,7 +61,7 @@ class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet)
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
if (navigation == null) return
|
if (navigation == null) return
|
||||||
|
|
||||||
navigation?.regions?.forEach { region ->
|
navigation?.getRegions()?.forEach { region ->
|
||||||
val rect = region.rectF
|
val rect = region.rectF
|
||||||
|
|
||||||
// Scale rect from 1f,1f to screen width and height
|
// Scale rect from 1f,1f to screen width and height
|
||||||
|
|
|
@ -60,6 +60,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.preference.toggle
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNonCancellable
|
import tachiyomi.core.util.lang.launchNonCancellable
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
|
@ -129,6 +130,15 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
field = value
|
field = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The visible page index of the currently loaded chapter. Used to restore from process kill.
|
||||||
|
*/
|
||||||
|
private var chapterPageIndex = savedState.get<Int>("page_index") ?: -1
|
||||||
|
set(value) {
|
||||||
|
savedState["page_index"] = value
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The chapter loader for the loaded manga. It'll be null until [manga] is set.
|
* The chapter loader for the loaded manga. It'll be null until [manga] is set.
|
||||||
*/
|
*/
|
||||||
|
@ -213,7 +223,10 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.onEach { currentChapter ->
|
.onEach { currentChapter ->
|
||||||
if (!currentChapter.chapter.read) {
|
if (chapterPageIndex >= 0) {
|
||||||
|
// Restore from SavedState
|
||||||
|
currentChapter.requestedPage = chapterPageIndex
|
||||||
|
} else if (!currentChapter.chapter.read) {
|
||||||
currentChapter.requestedPage = currentChapter.chapter.last_page_read
|
currentChapter.requestedPage = currentChapter.chapter.last_page_read
|
||||||
}
|
}
|
||||||
chapterId = currentChapter.chapter.id!!
|
chapterId = currentChapter.chapter.id!!
|
||||||
|
@ -507,6 +520,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
it.copy(currentPage = pageIndex + 1)
|
it.copy(currentPage = pageIndex + 1)
|
||||||
}
|
}
|
||||||
readerChapter.requestedPage = pageIndex
|
readerChapter.requestedPage = pageIndex
|
||||||
|
chapterPageIndex = pageIndex
|
||||||
|
|
||||||
if (!incognitoMode && page.status != Page.State.ERROR) {
|
if (!incognitoMode && page.status != Page.State.ERROR) {
|
||||||
readerChapter.chapter.last_page_read = pageIndex
|
readerChapter.chapter.last_page_read = pageIndex
|
||||||
|
@ -678,6 +692,15 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggleCropBorders(): Boolean {
|
||||||
|
val isPagerType = ReadingModeType.isPagerType(getMangaReadingMode())
|
||||||
|
return if (isPagerType) {
|
||||||
|
readerPreferences.cropBorders().toggle()
|
||||||
|
} else {
|
||||||
|
readerPreferences.cropBordersWebtoon().toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a filename for the given [manga] and [page]
|
* Generate a filename for the given [manga] and [page]
|
||||||
*/
|
*/
|
||||||
|
@ -700,6 +723,14 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
mutableState.update { it.copy(dialog = Dialog.Loading) }
|
mutableState.update { it.copy(dialog = Dialog.Loading) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openReadingModeSelectDialog() {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.ReadingModeSelect) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openOrientationModeSelectDialog() {
|
||||||
|
mutableState.update { it.copy(dialog = Dialog.OrientationModeSelect) }
|
||||||
|
}
|
||||||
|
|
||||||
fun openPageDialog(page: ReaderPage) {
|
fun openPageDialog(page: ReaderPage) {
|
||||||
mutableState.update { it.copy(dialog = Dialog.PageActions(page)) }
|
mutableState.update { it.copy(dialog = Dialog.PageActions(page)) }
|
||||||
}
|
}
|
||||||
|
@ -910,6 +941,8 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data object Loading : Dialog
|
data object Loading : Dialog
|
||||||
data object Settings : Dialog
|
data object Settings : Dialog
|
||||||
|
data object ReadingModeSelect : Dialog
|
||||||
|
data object OrientationModeSelect : Dialog
|
||||||
data class PageActions(val page: ReaderPage) : Dialog
|
data class PageActions(val page: ReaderPage) : Dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class ReaderSettingsScreenModel(
|
class ReaderSettingsScreenModel(
|
||||||
readerState: StateFlow<ReaderViewModel.State>,
|
readerState: StateFlow<ReaderViewModel.State>,
|
||||||
|
val hasDisplayCutout: Boolean,
|
||||||
val onChangeReadingMode: (ReadingModeType) -> Unit,
|
val onChangeReadingMode: (ReadingModeType) -> Unit,
|
||||||
val onChangeOrientation: (OrientationType) -> Unit,
|
val onChangeOrientation: (OrientationType) -> Unit,
|
||||||
val preferences: ReaderPreferences = Injekt.get(),
|
val preferences: ReaderPreferences = Injekt.get(),
|
||||||
|
|
|
@ -32,15 +32,19 @@ abstract class ViewerNavigation {
|
||||||
|
|
||||||
private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f)
|
private var constantMenuRegion: RectF = RectF(0f, 0f, 1f, 0.05f)
|
||||||
|
|
||||||
abstract var regions: List<Region>
|
|
||||||
|
|
||||||
var invertMode: ReaderPreferences.TappingInvertMode = ReaderPreferences.TappingInvertMode.NONE
|
var invertMode: ReaderPreferences.TappingInvertMode = ReaderPreferences.TappingInvertMode.NONE
|
||||||
|
|
||||||
|
protected abstract var regionList: List<Region>
|
||||||
|
|
||||||
|
/** Returns regions with applied inversion. */
|
||||||
|
fun getRegions(): List<Region> {
|
||||||
|
return regionList.map { it.invert(invertMode) }
|
||||||
|
}
|
||||||
|
|
||||||
fun getAction(pos: PointF): NavigationRegion {
|
fun getAction(pos: PointF): NavigationRegion {
|
||||||
val x = pos.x
|
val x = pos.x
|
||||||
val y = pos.y
|
val y = pos.y
|
||||||
val region = regions.map { it.invert(invertMode) }
|
val region = getRegions().find { it.rectF.contains(x, y) }
|
||||||
.find { it.rectF.contains(x, y) }
|
|
||||||
return when {
|
return when {
|
||||||
region != null -> region.type
|
region != null -> region.type
|
||||||
constantMenuRegion.contains(x, y) -> NavigationRegion.MENU
|
constantMenuRegion.contains(x, y) -> NavigationRegion.MENU
|
||||||
|
|
|
@ -14,5 +14,5 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||||
*/
|
*/
|
||||||
class DisabledNavigation : ViewerNavigation() {
|
class DisabledNavigation : ViewerNavigation() {
|
||||||
|
|
||||||
override var regions: List<Region> = emptyList()
|
override var regionList: List<Region> = emptyList()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||||
*/
|
*/
|
||||||
class EdgeNavigation : ViewerNavigation() {
|
class EdgeNavigation : ViewerNavigation() {
|
||||||
|
|
||||||
override var regions: List<Region> = listOf(
|
override var regionList: List<Region> = listOf(
|
||||||
Region(
|
Region(
|
||||||
rectF = RectF(0f, 0f, 0.33f, 1f),
|
rectF = RectF(0f, 0f, 0.33f, 1f),
|
||||||
type = NavigationRegion.NEXT,
|
type = NavigationRegion.NEXT,
|
||||||
|
|
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||||
*/
|
*/
|
||||||
class KindlishNavigation : ViewerNavigation() {
|
class KindlishNavigation : ViewerNavigation() {
|
||||||
|
|
||||||
override var regions: List<Region> = listOf(
|
override var regionList: List<Region> = listOf(
|
||||||
Region(
|
Region(
|
||||||
rectF = RectF(0.33f, 0.33f, 1f, 1f),
|
rectF = RectF(0.33f, 0.33f, 1f, 1f),
|
||||||
type = NavigationRegion.NEXT,
|
type = NavigationRegion.NEXT,
|
||||||
|
|
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||||
*/
|
*/
|
||||||
open class LNavigation : ViewerNavigation() {
|
open class LNavigation : ViewerNavigation() {
|
||||||
|
|
||||||
override var regions: List<Region> = listOf(
|
override var regionList: List<Region> = listOf(
|
||||||
Region(
|
Region(
|
||||||
rectF = RectF(0f, 0.33f, 0.33f, 0.66f),
|
rectF = RectF(0f, 0.33f, 0.33f, 0.66f),
|
||||||
type = NavigationRegion.PREV,
|
type = NavigationRegion.PREV,
|
||||||
|
|
|
@ -15,7 +15,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
|
||||||
*/
|
*/
|
||||||
class RightAndLeftNavigation : ViewerNavigation() {
|
class RightAndLeftNavigation : ViewerNavigation() {
|
||||||
|
|
||||||
override var regions: List<Region> = listOf(
|
override var regionList: List<Region> = listOf(
|
||||||
Region(
|
Region(
|
||||||
rectF = RectF(0f, 0f, 0.33f, 1f),
|
rectF = RectF(0f, 0f, 0.33f, 1f),
|
||||||
type = NavigationRegion.LEFT,
|
type = NavigationRegion.LEFT,
|
||||||
|
|
|
@ -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 eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
|
import tachiyomi.source.local.entries.manga.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the list with not downloaded chapters removed
|
* Returns a copy of the list with not downloaded chapters removed.
|
||||||
*/
|
*/
|
||||||
fun List<Chapter>.filterDownloadedChapters(manga: Manga): List<Chapter> {
|
fun List<Chapter>.filterDownloadedChapters(manga: Manga): List<Chapter> {
|
||||||
|
if (manga.isLocal()) return this
|
||||||
|
|
||||||
val downloadCache: MangaDownloadCache = Injekt.get()
|
val downloadCache: MangaDownloadCache = Injekt.get()
|
||||||
|
|
||||||
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) }
|
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) }
|
||||||
|
|
|
@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.util.episode
|
||||||
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
import tachiyomi.domain.items.episode.model.Episode
|
import tachiyomi.domain.items.episode.model.Episode
|
||||||
|
import tachiyomi.source.local.entries.anime.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the list with not downloaded chapters removed
|
* Returns a copy of the list with not downloaded chapters removed.
|
||||||
*/
|
*/
|
||||||
fun List<Episode>.filterDownloadedEpisodes(anime: Anime): List<Episode> {
|
fun List<Episode>.filterDownloadedEpisodes(anime: Anime): List<Episode> {
|
||||||
|
if (anime.isLocal()) return this
|
||||||
|
|
||||||
val downloadCache: AnimeDownloadCache = Injekt.get()
|
val downloadCache: AnimeDownloadCache = Injekt.get()
|
||||||
|
|
||||||
return filter { downloadCache.isEpisodeDownloaded(it.name, it.scanlator, anime.title, anime.source, false) }
|
return filter { downloadCache.isEpisodeDownloaded(it.name, it.scanlator, anime.title, anime.source, false) }
|
||||||
|
|
|
@ -7,20 +7,13 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.appcompat.view.ContextThemeWrapper
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
import androidx.core.content.PermissionChecker
|
import androidx.core.content.PermissionChecker
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.alpha
|
|
||||||
import androidx.core.graphics.blue
|
|
||||||
import androidx.core.graphics.green
|
|
||||||
import androidx.core.graphics.red
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
|
@ -35,7 +28,6 @@ import tachiyomi.core.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies a string to clipboard
|
* Copies a string to clipboard
|
||||||
|
@ -69,25 +61,6 @@ fun Context.copyToClipboard(label: String, content: String) {
|
||||||
*/
|
*/
|
||||||
fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED
|
fun Context.hasPermission(permission: String) = PermissionChecker.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the color for the given attribute.
|
|
||||||
*
|
|
||||||
* @param resource the attribute.
|
|
||||||
* @param alphaFactor the alpha number [0,1].
|
|
||||||
*/
|
|
||||||
@ColorInt fun Context.getResourceColor(@AttrRes resource: Int, alphaFactor: Float = 1f): Int {
|
|
||||||
val typedArray = obtainStyledAttributes(intArrayOf(resource))
|
|
||||||
val color = typedArray.getColor(0, 0)
|
|
||||||
typedArray.recycle()
|
|
||||||
|
|
||||||
if (alphaFactor < 1f) {
|
|
||||||
val alpha = (color.alpha * alphaFactor).roundToInt()
|
|
||||||
return Color.argb(alpha, color.red, color.green, color.blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
|
|
||||||
val Context.powerManager: PowerManager
|
val Context.powerManager: PowerManager
|
||||||
get() = getSystemService()!!
|
get() = getSystemService()!!
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
|
|
||||||
package eu.kanade.tachiyomi.util.view
|
package eu.kanade.tachiyomi.util.view
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
@ -14,11 +11,7 @@ import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.appcompat.view.menu.MenuBuilder
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.appcompat.widget.TooltipCompat
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
@ -27,11 +20,8 @@ import androidx.compose.runtime.CompositionContext
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
import androidx.core.view.forEach
|
|
||||||
import com.google.android.material.shape.MaterialShapeDrawable
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
|
||||||
|
|
||||||
inline fun ComponentActivity.setComposeContent(
|
inline fun ComponentActivity.setComposeContent(
|
||||||
parent: CompositionContext? = null,
|
parent: CompositionContext? = null,
|
||||||
|
@ -65,24 +55,6 @@ fun ComposeView.setComposeContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a tooltip shown on long press.
|
|
||||||
*
|
|
||||||
* @param stringRes String resource for tooltip.
|
|
||||||
*/
|
|
||||||
inline fun View.setTooltip(@StringRes stringRes: Int) {
|
|
||||||
setTooltip(context.getString(stringRes))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a tooltip shown on long press.
|
|
||||||
*
|
|
||||||
* @param text Text for tooltip.
|
|
||||||
*/
|
|
||||||
inline fun View.setTooltip(text: String) {
|
|
||||||
TooltipCompat.setTooltipText(this, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a popup menu on top of this view.
|
* Shows a popup menu on top of this view.
|
||||||
*
|
*
|
||||||
|
@ -110,57 +82,6 @@ inline fun View.popupMenu(
|
||||||
return popup
|
return popup
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a popup menu on top of this view.
|
|
||||||
*
|
|
||||||
* @param items menu item names to inflate the menu with. List of itemId to stringRes pairs.
|
|
||||||
* @param selectedItemId optionally show a checkmark beside an item with this itemId.
|
|
||||||
* @param onMenuItemClick function to execute when a menu item is clicked.
|
|
||||||
*/
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
inline fun View.popupMenu(
|
|
||||||
items: List<Pair<Int, Int>>,
|
|
||||||
selectedItemId: Int? = null,
|
|
||||||
noinline onMenuItemClick: MenuItem.() -> Unit,
|
|
||||||
): PopupMenu {
|
|
||||||
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
|
|
||||||
items.forEach { (id, stringRes) ->
|
|
||||||
popup.menu.add(0, id, 0, stringRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedItemId != null) {
|
|
||||||
(popup.menu as? MenuBuilder)?.setOptionalIconsVisible(true)
|
|
||||||
val emptyIcon = AppCompatResources.getDrawable(context, R.drawable.ic_blank_24dp)
|
|
||||||
popup.menu.forEach { item ->
|
|
||||||
item.icon = when (item.itemId) {
|
|
||||||
selectedItemId -> AppCompatResources.getDrawable(context, R.drawable.ic_check_24dp)?.mutate()?.apply {
|
|
||||||
setTint(context.getResourceColor(android.R.attr.textColorPrimary))
|
|
||||||
}
|
|
||||||
else -> emptyIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener {
|
|
||||||
it.onMenuItemClick()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.show()
|
|
||||||
return popup
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a deep copy of the provided [Drawable]
|
|
||||||
*/
|
|
||||||
inline fun <reified T : Drawable> T.copy(context: Context): T? {
|
|
||||||
return (constantState?.newDrawable()?.mutate() as? T).apply {
|
|
||||||
if (this is MaterialShapeDrawable) {
|
|
||||||
initializeElevationOverlay(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun View?.isVisibleOnScreen(): Boolean {
|
fun View?.isVisibleOnScreen(): Boolean {
|
||||||
if (this == null) {
|
if (this == null) {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -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"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -58,83 +57,11 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="?attr/actionBarSize" />
|
android:minHeight="?attr/actionBarSize" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/reader_menu_bottom"
|
android:id="@+id/reader_menu_bottom"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom" />
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<androidx.compose.ui.platform.ComposeView
|
|
||||||
android:id="@+id/reader_nav"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:layoutDirection="ltr" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/toolbar_bottom"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:clickable="true"
|
|
||||||
tools:ignore="KeyboardInaccessibleWidget">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/action_reading_mode"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/viewer"
|
|
||||||
android:padding="@dimen/screen_edge_margin"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/action_crop_borders"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:srcCompat="@drawable/ic_reader_default_24dp"
|
|
||||||
app:tint="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/action_crop_borders"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/pref_crop_borders"
|
|
||||||
android:padding="@dimen/screen_edge_margin"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/action_rotation"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/action_reading_mode"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:srcCompat="@drawable/ic_crop_24dp"
|
|
||||||
app:tint="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/action_rotation"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/pref_rotation_type"
|
|
||||||
android:padding="@dimen/screen_edge_margin"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/action_settings"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/action_crop_borders"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:srcCompat="@drawable/ic_screen_rotation_24dp"
|
|
||||||
app:tint="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/action_settings"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/action_settings"
|
|
||||||
android:padding="@dimen/screen_edge_margin"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/action_rotation"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:srcCompat="@drawable/ic_settings_24dp"
|
|
||||||
app:tint="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,17 @@ import java.io.IOException
|
||||||
import java.util.ArrayDeque
|
import java.util.ArrayDeque
|
||||||
import java.util.concurrent.Semaphore
|
import java.util.concurrent.Semaphore
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
import kotlin.time.toDurationUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An OkHttp interceptor that handles rate limiting.
|
* An OkHttp interceptor that handles rate limiting.
|
||||||
*
|
*
|
||||||
|
* This uses `java.time` APIs and is the legacy method, kept
|
||||||
|
* for compatibility reasons with existing extensions.
|
||||||
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
*
|
*
|
||||||
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
* permits = 5, period = 1, unit = seconds => 5 requests per second
|
||||||
|
@ -19,27 +26,43 @@ import java.util.concurrent.TimeUnit
|
||||||
*
|
*
|
||||||
* @since extension-lib 1.3
|
* @since extension-lib 1.3
|
||||||
*
|
*
|
||||||
* @param permits {Int} Number of requests allowed within a period of units.
|
* @param permits [Int] Number of requests allowed within a period of units.
|
||||||
* @param period {Long} The limiting duration. Defaults to 1.
|
* @param period [Long] The limiting duration. Defaults to 1.
|
||||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
* @param unit [TimeUnit] The unit of time for the period. Defaults to seconds.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use the version with kotlin.time APIs instead.")
|
||||||
fun OkHttpClient.Builder.rateLimit(
|
fun OkHttpClient.Builder.rateLimit(
|
||||||
permits: Int,
|
permits: Int,
|
||||||
period: Long = 1,
|
period: Long = 1,
|
||||||
unit: TimeUnit = TimeUnit.SECONDS,
|
unit: TimeUnit = TimeUnit.SECONDS,
|
||||||
) = addInterceptor(RateLimitInterceptor(null, permits, period, unit))
|
) = addInterceptor(RateLimitInterceptor(null, permits, period.toDuration(unit.toDurationUnit())))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An OkHttp interceptor that handles rate limiting.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* permits = 5, period = 1.seconds => 5 requests per second
|
||||||
|
* permits = 10, period = 2.minutes => 10 requests per 2 minutes
|
||||||
|
*
|
||||||
|
* @since extension-lib 1.5
|
||||||
|
*
|
||||||
|
* @param permits [Int] Number of requests allowed within a period of units.
|
||||||
|
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
|
||||||
|
*/
|
||||||
|
fun OkHttpClient.Builder.rateLimit(permits: Int, period: Duration = 1.seconds) =
|
||||||
|
addInterceptor(RateLimitInterceptor(null, permits, period))
|
||||||
|
|
||||||
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
|
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
|
||||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||||
internal class RateLimitInterceptor(
|
internal class RateLimitInterceptor(
|
||||||
private val host: String?,
|
private val host: String?,
|
||||||
private val permits: Int,
|
private val permits: Int,
|
||||||
period: Long,
|
period: Duration,
|
||||||
unit: TimeUnit,
|
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
private val requestQueue = ArrayDeque<Long>(permits)
|
private val requestQueue = ArrayDeque<Long>(permits)
|
||||||
private val rateLimitMillis = unit.toMillis(period)
|
private val rateLimitMillis = period.inWholeMilliseconds
|
||||||
private val fairLock = Semaphore(1, true)
|
private val fairLock = Semaphore(1, true)
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
package eu.kanade.tachiyomi.network.interceptor
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
import kotlin.time.toDurationUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An OkHttp interceptor that handles given url host's rate limiting.
|
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||||
*
|
*
|
||||||
|
* This uses Java Time APIs and is the legacy method, kept
|
||||||
|
* for compatibility reasons with existing extensions.
|
||||||
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
*
|
*
|
||||||
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
|
||||||
|
@ -14,14 +22,55 @@ import java.util.concurrent.TimeUnit
|
||||||
*
|
*
|
||||||
* @since extension-lib 1.3
|
* @since extension-lib 1.3
|
||||||
*
|
*
|
||||||
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
* @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||||
* @param permits {Int} Number of requests allowed within a period of units.
|
* @param permits [Int] Number of requests allowed within a period of units.
|
||||||
* @param period {Long} The limiting duration. Defaults to 1.
|
* @param period [Long] The limiting duration. Defaults to 1.
|
||||||
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
|
* @param unit [TimeUnit] The unit of time for the period. Defaults to seconds.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("Use the version with kotlin.time APIs instead.")
|
||||||
fun OkHttpClient.Builder.rateLimitHost(
|
fun OkHttpClient.Builder.rateLimitHost(
|
||||||
httpUrl: HttpUrl,
|
httpUrl: HttpUrl,
|
||||||
permits: Int,
|
permits: Int,
|
||||||
period: Long = 1,
|
period: Long = 1,
|
||||||
unit: TimeUnit = TimeUnit.SECONDS,
|
unit: TimeUnit = TimeUnit.SECONDS,
|
||||||
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period, unit))
|
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* httpUrl = "https://api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1.seconds => 5 requests per second to api.manga.com
|
||||||
|
* httpUrl = "https://imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
||||||
|
*
|
||||||
|
* @since extension-lib 1.5
|
||||||
|
*
|
||||||
|
* @param httpUrl [HttpUrl] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||||
|
* @param permits [Int] Number of requests allowed within a period of units.
|
||||||
|
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
|
||||||
|
*/
|
||||||
|
fun OkHttpClient.Builder.rateLimitHost(
|
||||||
|
httpUrl: HttpUrl,
|
||||||
|
permits: Int,
|
||||||
|
period: Duration = 1.seconds,
|
||||||
|
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* url = "https://api.manga.com", permits = 5, period = 1.seconds => 5 requests per second to api.manga.com
|
||||||
|
* url = "https://imagecdn.manga.com", permits = 10, period = 2.minutes => 10 requests per 2 minutes to imagecdn.manga.com
|
||||||
|
*
|
||||||
|
* @since extension-lib 1.5
|
||||||
|
*
|
||||||
|
* @param url [String] The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
|
||||||
|
* @param permits [Int] Number of requests allowed within a period of units.
|
||||||
|
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
|
||||||
|
*/
|
||||||
|
fun OkHttpClient.Builder.rateLimitHost(
|
||||||
|
url: String,
|
||||||
|
permits: Int,
|
||||||
|
period: Duration = 1.seconds,
|
||||||
|
) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package tachiyomi.data
|
package tachiyomi.data
|
||||||
|
|
||||||
import app.cash.sqldelight.ColumnAdapter
|
import app.cash.sqldelight.ColumnAdapter
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
object DateColumnAdapter : ColumnAdapter<Date, Long> {
|
object DateColumnAdapter : ColumnAdapter<Date, Long> {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package tachiyomi.data.entries.anime
|
package tachiyomi.data.entries.anime
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
import tachiyomi.domain.library.anime.LibraryAnime
|
import tachiyomi.domain.library.anime.LibraryAnime
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package tachiyomi.data.entries.manga
|
package tachiyomi.data.entries.manga
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.library.manga.LibraryManga
|
import tachiyomi.domain.library.manga.LibraryManga
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
|
import eu.kanade.tachiyomi.model.UpdateStrategy;
|
||||||
import kotlin.collections.List;
|
import kotlin.collections.List;
|
||||||
import kotlin.Boolean;
|
import kotlin.Boolean;
|
||||||
import kotlin.String;
|
import kotlin.String;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
|
import eu.kanade.tachiyomi.model.UpdateStrategy;
|
||||||
import kotlin.collections.List;
|
import kotlin.collections.List;
|
||||||
import kotlin.Boolean;
|
import kotlin.Boolean;
|
||||||
import kotlin.String;
|
import kotlin.String;
|
||||||
|
|
|
@ -2,34 +2,34 @@ package tachiyomi.domain.entries.anime.interactor
|
||||||
|
|
||||||
import tachiyomi.domain.entries.anime.model.Anime
|
import tachiyomi.domain.entries.anime.model.Anime
|
||||||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||||
|
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
||||||
import tachiyomi.domain.items.episode.model.Episode
|
import tachiyomi.domain.items.episode.model.Episode
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_GRACE_PERIOD = 28
|
const val MAX_FETCH_INTERVAL = 28
|
||||||
|
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
||||||
|
|
||||||
class SetAnimeFetchInterval(
|
class SetAnimeFetchInterval(
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun update(
|
suspend fun toAnimeUpdateOrNull(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
episodes: List<Episode>,
|
dateTime: ZonedDateTime,
|
||||||
zonedDateTime: ZonedDateTime,
|
window: Pair<Long, Long>,
|
||||||
fetchRange: Pair<Long, Long>,
|
|
||||||
): AnimeUpdate? {
|
): AnimeUpdate? {
|
||||||
val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||||
getCurrent(ZonedDateTime.now())
|
getWindow(ZonedDateTime.now())
|
||||||
} else {
|
} else {
|
||||||
fetchRange
|
window
|
||||||
}
|
}
|
||||||
val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(episodes, zonedDateTime)
|
val episodes = getEpisodeByAnimeId.await(anime.id)
|
||||||
val nextUpdate = calculateNextUpdate(anime, interval, zonedDateTime, currentInterval)
|
val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(episodes, dateTime)
|
||||||
|
val nextUpdate = calculateNextUpdate(anime, interval, dateTime, currentWindow)
|
||||||
|
|
||||||
return if (anime.nextUpdate == nextUpdate && anime.fetchInterval == interval) {
|
return if (anime.nextUpdate == nextUpdate && anime.fetchInterval == interval) {
|
||||||
null
|
null
|
||||||
|
@ -38,20 +38,11 @@ class SetAnimeFetchInterval(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrent(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||||
var followRange = 0
|
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||||
var leadRange = 0
|
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
|
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||||
followRange = libraryPreferences.followingAnimeExpectedDays().get()
|
|
||||||
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
|
|
||||||
}
|
|
||||||
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
|
|
||||||
// revert math of (next_update + follow < now) become (next_update < now - follow)
|
|
||||||
// so (now - follow) become lower limit
|
|
||||||
val lowerRange = startToday.minusDays(followRange.toLong())
|
|
||||||
val higherRange = startToday.plusDays(leadRange.toLong())
|
|
||||||
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
|
internal fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
|
||||||
|
@ -91,35 +82,41 @@ class SetAnimeFetchInterval(
|
||||||
// Default to 7 days
|
// Default to 7 days
|
||||||
else -> 7
|
else -> 7
|
||||||
}
|
}
|
||||||
// Min 1, max 28 days
|
|
||||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
interval: Int,
|
interval: Int,
|
||||||
zonedDateTime: ZonedDateTime,
|
dateTime: ZonedDateTime,
|
||||||
fetchRange: Pair<Long, Long>,
|
window: Pair<Long, Long>,
|
||||||
): Long {
|
): Long {
|
||||||
return if (
|
return if (
|
||||||
anime.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
anime.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
||||||
anime.fetchInterval == 0
|
anime.fetchInterval == 0
|
||||||
) {
|
) {
|
||||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), dateTime.zone)
|
||||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
.toLocalDate()
|
||||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
.atStartOfDay()
|
||||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
||||||
|
val cycle = timeSinceLatest.floorDiv(
|
||||||
|
interval.absoluteValue.takeIf { interval < 0 }
|
||||||
|
?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
|
||||||
|
)
|
||||||
|
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
|
||||||
} else {
|
} else {
|
||||||
anime.nextUpdate
|
anime.nextUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||||
if (delta >= maxValue) return maxValue
|
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
|
||||||
// double delta again if missed more than 9 check in new delta
|
// double delta again if missed more than 9 check in new delta
|
||||||
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
return if (cycle > doubleWhenOver) {
|
return if (cycle > doubleWhenOver) {
|
||||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
|
||||||
} else {
|
} else {
|
||||||
delta
|
delta
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package tachiyomi.domain.entries.anime.model
|
package tachiyomi.domain.entries.anime.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import tachiyomi.core.preference.TriState
|
import tachiyomi.core.preference.TriState
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package tachiyomi.domain.entries.anime.model
|
package tachiyomi.domain.entries.anime.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
|
|
||||||
data class AnimeUpdate(
|
data class AnimeUpdate(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
|
|
@ -2,34 +2,34 @@ package tachiyomi.domain.entries.manga.interactor
|
||||||
|
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||||
|
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_GRACE_PERIOD = 28
|
const val MAX_FETCH_INTERVAL = 28
|
||||||
|
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
||||||
|
|
||||||
class SetMangaFetchInterval(
|
class SetMangaFetchInterval(
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val getChapterByMangaId: GetChapterByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun update(
|
suspend fun toMangaUpdateOrNull(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
dateTime: ZonedDateTime,
|
||||||
zonedDateTime: ZonedDateTime,
|
window: Pair<Long, Long>,
|
||||||
fetchRange: Pair<Long, Long>,
|
|
||||||
): MangaUpdate? {
|
): MangaUpdate? {
|
||||||
val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||||
getCurrent(ZonedDateTime.now())
|
getWindow(ZonedDateTime.now())
|
||||||
} else {
|
} else {
|
||||||
fetchRange
|
window
|
||||||
}
|
}
|
||||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
val chapters = getChapterByMangaId.await(manga.id)
|
||||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentInterval)
|
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime)
|
||||||
|
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||||
|
|
||||||
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
||||||
null
|
null
|
||||||
|
@ -38,20 +38,11 @@ class SetMangaFetchInterval(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrent(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||||
var followRange = 0
|
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||||
var leadRange = 0
|
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
|
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||||
followRange = libraryPreferences.followingAnimeExpectedDays().get()
|
|
||||||
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
|
|
||||||
}
|
|
||||||
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
|
|
||||||
// revert math of (next_update + follow < now) become (next_update < now - follow)
|
|
||||||
// so (now - follow) become lower limit
|
|
||||||
val lowerRange = startToday.minusDays(followRange.toLong())
|
|
||||||
val higherRange = startToday.plusDays(leadRange.toLong())
|
|
||||||
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||||
|
@ -91,35 +82,41 @@ class SetMangaFetchInterval(
|
||||||
// Default to 7 days
|
// Default to 7 days
|
||||||
else -> 7
|
else -> 7
|
||||||
}
|
}
|
||||||
// Min 1, max 28 days
|
|
||||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
interval: Int,
|
interval: Int,
|
||||||
zonedDateTime: ZonedDateTime,
|
dateTime: ZonedDateTime,
|
||||||
fetchRange: Pair<Long, Long>,
|
window: Pair<Long, Long>,
|
||||||
): Long {
|
): Long {
|
||||||
return if (
|
return if (
|
||||||
manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
||||||
manga.fetchInterval == 0
|
manga.fetchInterval == 0
|
||||||
) {
|
) {
|
||||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
|
||||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
.toLocalDate()
|
||||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
.atStartOfDay()
|
||||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
||||||
|
val cycle = timeSinceLatest.floorDiv(
|
||||||
|
interval.absoluteValue.takeIf { interval < 0 }
|
||||||
|
?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
|
||||||
|
)
|
||||||
|
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
|
||||||
} else {
|
} else {
|
||||||
manga.nextUpdate
|
manga.nextUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||||
if (delta >= maxValue) return maxValue
|
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
|
||||||
// double delta again if missed more than 9 check in new delta
|
// double delta again if missed more than 9 check in new delta
|
||||||
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
return if (cycle > doubleWhenOver) {
|
return if (cycle > doubleWhenOver) {
|
||||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
|
||||||
} else {
|
} else {
|
||||||
delta
|
delta
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package tachiyomi.domain.entries.manga.model
|
package tachiyomi.domain.entries.manga.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import tachiyomi.core.preference.TriState
|
import tachiyomi.core.preference.TriState
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package tachiyomi.domain.entries.manga.model
|
package tachiyomi.domain.entries.manga.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
|
|
||||||
data class MangaUpdate(
|
data class MangaUpdate(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
|
|
|
@ -62,12 +62,6 @@ class LibraryPreferences(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun leadingAnimeExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1)
|
|
||||||
fun followingAnimeExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1)
|
|
||||||
|
|
||||||
fun leadingMangaExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1)
|
|
||||||
fun followingMangaExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1)
|
|
||||||
|
|
||||||
fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false)
|
fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false)
|
||||||
|
|
||||||
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.time.ZonedDateTime
|
||||||
|
|
||||||
@Execution(ExecutionMode.CONCURRENT)
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
class SetAnimeFetchIntervalTest {
|
class SetAnimeFetchIntervalTest {
|
||||||
|
|
||||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||||
private var episode = Episode.create().copy(
|
private var episode = Episode.create().copy(
|
||||||
dateFetch = testTime.toEpochSecond() * 1000,
|
dateFetch = testTime.toEpochSecond() * 1000,
|
||||||
|
@ -19,14 +20,8 @@ class SetAnimeFetchIntervalTest {
|
||||||
|
|
||||||
private val setAnimeFetchInterval = SetAnimeFetchInterval(mockk())
|
private val setAnimeFetchInterval = SetAnimeFetchInterval(mockk())
|
||||||
|
|
||||||
private fun episodeAddTime(episode: Episode, duration: Duration): Episode {
|
|
||||||
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
|
||||||
return episode.copy(dateFetch = newTime, dateUpload = newTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// default 7 when less than 3 distinct day
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 7 when 1 episodes in 1 day`() {
|
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = mutableListOf<Episode>()
|
||||||
(1..1).forEach {
|
(1..1).forEach {
|
||||||
val duration = Duration.ofHours(10)
|
val duration = Duration.ofHours(10)
|
||||||
|
@ -63,9 +58,8 @@ class SetAnimeFetchIntervalTest {
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default 1 if interval less than 1
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 episodes in 75 hours, 3 days`() {
|
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = mutableListOf<Episode>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(15L * it)
|
val duration = Duration.ofHours(15L * it)
|
||||||
|
@ -98,9 +92,8 @@ class SetAnimeFetchIntervalTest {
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 2
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// If interval is decimal, floor to closest integer
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 episodes in 125 hours, 5 days`() {
|
fun `calculateInterval returns floored value when interval is decimal`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = mutableListOf<Episode>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(25L * it)
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
@ -121,9 +114,8 @@ class SetAnimeFetchIntervalTest {
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use fetch time if upload time not available
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 episodes in 125 hours, 5 days of dateFetch`() {
|
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
||||||
val episodes = mutableListOf<Episode>()
|
val episodes = mutableListOf<Episode>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(25L * it)
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
@ -132,4 +124,9 @@ class SetAnimeFetchIntervalTest {
|
||||||
}
|
}
|
||||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun episodeAddTime(episode: Episode, duration: Duration): Episode {
|
||||||
|
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
||||||
|
return episode.copy(dateFetch = newTime, dateUpload = newTime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.time.ZonedDateTime
|
||||||
|
|
||||||
@Execution(ExecutionMode.CONCURRENT)
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
class SetMangaFetchIntervalTest {
|
class SetMangaFetchIntervalTest {
|
||||||
|
|
||||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||||
private var chapter = Chapter.create().copy(
|
private var chapter = Chapter.create().copy(
|
||||||
dateFetch = testTime.toEpochSecond() * 1000,
|
dateFetch = testTime.toEpochSecond() * 1000,
|
||||||
|
@ -19,14 +20,8 @@ class SetMangaFetchIntervalTest {
|
||||||
|
|
||||||
private val setMangaFetchInterval = SetMangaFetchInterval(mockk())
|
private val setMangaFetchInterval = SetMangaFetchInterval(mockk())
|
||||||
|
|
||||||
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
|
|
||||||
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
|
||||||
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// default 7 when less than 3 distinct day
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 7 when 1 chapters in 1 day`() {
|
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
(1..1).forEach {
|
(1..1).forEach {
|
||||||
val duration = Duration.ofHours(10)
|
val duration = Duration.ofHours(10)
|
||||||
|
@ -63,9 +58,8 @@ class SetMangaFetchIntervalTest {
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default 1 if interval less than 1
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 chapters in 75 hours, 3 days`() {
|
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(15L * it)
|
val duration = Duration.ofHours(15L * it)
|
||||||
|
@ -98,9 +92,8 @@ class SetMangaFetchIntervalTest {
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// If interval is decimal, floor to closest integer
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days`() {
|
fun `calculateInterval returns floored value when interval is decimal`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(25L * it)
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
@ -121,9 +114,8 @@ class SetMangaFetchIntervalTest {
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use fetch time if upload time not available
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days of dateFetch`() {
|
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(25L * it)
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
@ -132,4 +124,9 @@ class SetMangaFetchIntervalTest {
|
||||||
}
|
}
|
||||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
|
||||||
|
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
||||||
|
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.r
|
||||||
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
|
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
|
||||||
|
|
||||||
work-runtime = "androidx.work:work-runtime-ktx:2.8.1"
|
work-runtime = "androidx.work:work-runtime-ktx:2.8.1"
|
||||||
guava = "com.google.guava:guava:32.0.1-android"
|
guava = "com.google.guava:guava:32.1.2-android"
|
||||||
|
|
||||||
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
||||||
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
||||||
|
|
|
@ -19,7 +19,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
|
||||||
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
|
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
|
||||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
|
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
|
||||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
|
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
|
||||||
okio = "com.squareup.okio:okio:3.4.0"
|
okio = "com.squareup.okio:okio:3.5.0"
|
||||||
|
|
||||||
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
|
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
<!-- Models -->
|
<!-- Models -->
|
||||||
<string name="name">Name</string>
|
<string name="name">Name</string>
|
||||||
|
<string name="categories">Categories</string>
|
||||||
<string name="manga">Manga</string>
|
<string name="manga">Manga</string>
|
||||||
<string name="chapters">Chapters</string>
|
<string name="chapters">Chapters</string>
|
||||||
<string name="track">Tracking</string>
|
<string name="track">Tracking</string>
|
||||||
|
@ -260,16 +261,6 @@
|
||||||
<string name="pref_update_only_started">That haven\'t been started</string>
|
<string name="pref_update_only_started">That haven\'t been started</string>
|
||||||
<string name="pref_update_only_in_release_period">Outside expected release period</string>
|
<string name="pref_update_only_in_release_period">Outside expected release period</string>
|
||||||
|
|
||||||
<string name="pref_update_release_grace_period">Expected manga release grace period</string>
|
|
||||||
<plurals name="pref_update_release_leading_days">
|
|
||||||
<item quantity="one">%d day before</item>
|
|
||||||
<item quantity="other">%d days before</item>
|
|
||||||
</plurals>
|
|
||||||
<plurals name="pref_update_release_following_days">
|
|
||||||
<item quantity="one">%d day after</item>
|
|
||||||
<item quantity="other">%d days after</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="pref_update_release_grace_period_info">A low grace period is recommended to minimize stress on sources. The more checks for an entry that are missed, the longer the interval in between checks will be with a maximum of 28 days.</string>
|
|
||||||
<string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
|
<string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
|
||||||
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
|
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
|
||||||
<string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
|
<string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
|
||||||
|
@ -466,6 +457,7 @@
|
||||||
<string name="enhanced_services_not_installed">Available but source not installed: %s</string>
|
<string name="enhanced_services_not_installed">Available but source not installed: %s</string>
|
||||||
<string name="enhanced_tracking_info">Services that provide enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
|
<string name="enhanced_tracking_info">Services that provide enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
|
||||||
<string name="action_track">Track</string>
|
<string name="action_track">Track</string>
|
||||||
|
<string name="track_activity_name">Tracking login</string>
|
||||||
|
|
||||||
<!-- Browse section -->
|
<!-- Browse section -->
|
||||||
<string name="pref_hide_in_library_items">Hide entries already in library</string>
|
<string name="pref_hide_in_library_items">Hide entries already in library</string>
|
||||||
|
@ -624,10 +616,6 @@
|
||||||
<item quantity="one">1 day</item>
|
<item quantity="one">1 day</item>
|
||||||
<item quantity="other">%d days</item>
|
<item quantity="other">%d days</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="range_interval_day">
|
|
||||||
<item quantity="one">%1$d - %2$d day</item>
|
|
||||||
<item quantity="other">%1$d - %2$d days</item>
|
|
||||||
</plurals>
|
|
||||||
|
|
||||||
<!-- Item info -->
|
<!-- Item info -->
|
||||||
<plurals name="missing_items">
|
<plurals name="missing_items">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.animesource.model
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SAnime : Serializable {
|
interface SAnime : Serializable {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.animesource.model
|
package eu.kanade.tachiyomi.animesource.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
|
|
||||||
class SAnimeImpl : SAnime {
|
class SAnimeImpl : SAnime {
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the update strategy for a single [SManga].
|
* Define the update strategy for a single SManga or SAnime.
|
||||||
* The strategy used will only take effect on the library update.
|
* The strategy used will only take effect on the library update.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.4
|
|
@ -1,5 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SManga : Serializable {
|
interface SManga : Serializable {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.model.UpdateStrategy
|
||||||
|
|
||||||
class SMangaImpl : SManga {
|
class SMangaImpl : SManga {
|
||||||
|
|
||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
Loading…
Reference in a new issue