mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
merge24
Also a little test to see how this whole github team works.
Last commit merged: fe90546821
This commit is contained in:
parent
4cbf9a813e
commit
1b6301cc95
37 changed files with 863 additions and 348 deletions
|
@ -81,6 +81,7 @@ import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
|||
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
|
||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||
|
@ -90,6 +91,7 @@ import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
|||
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
||||
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
|
||||
|
@ -182,10 +184,11 @@ class DomainModule : InjektModule {
|
|||
addFactory { GetNextEpisodes(get(), get(), get()) }
|
||||
addFactory { ResetAnimeViewerFlags(get()) }
|
||||
addFactory { SetAnimeEpisodeFlags(get()) }
|
||||
addFactory { SetAnimeUpdateInterval(get()) }
|
||||
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
|
||||
addFactory { SetAnimeViewerFlags(get()) }
|
||||
addFactory { NetworkToLocalAnime(get()) }
|
||||
addFactory { UpdateAnime(get()) }
|
||||
addFactory { UpdateAnime(get(), get()) }
|
||||
addFactory { SetAnimeCategories(get()) }
|
||||
|
||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
||||
|
@ -197,6 +200,7 @@ class DomainModule : InjektModule {
|
|||
addFactory { GetNextChapters(get(), get(), get()) }
|
||||
addFactory { ResetMangaViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { SetMangaUpdateInterval(get()) }
|
||||
addFactory {
|
||||
SetMangaDefaultChapterFlags(
|
||||
get(),
|
||||
|
@ -206,7 +210,7 @@ class DomainModule : InjektModule {
|
|||
}
|
||||
addFactory { SetMangaViewerFlags(get()) }
|
||||
addFactory { NetworkToLocalManga(get()) }
|
||||
addFactory { UpdateManga(get()) }
|
||||
addFactory { UpdateManga(get(), get()) }
|
||||
addFactory { SetMangaCategories(get()) }
|
||||
|
||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||
|
|
|
@ -3,8 +3,7 @@ package eu.kanade.domain.entries.anime.interactor
|
|||
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||
import tachiyomi.domain.entries.anime.interactor.getCurrentFetchRange
|
||||
import tachiyomi.domain.entries.anime.interactor.updateIntervalMeta
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||
|
@ -17,6 +16,7 @@ import java.util.Date
|
|||
|
||||
class UpdateAnime(
|
||||
private val animeRepository: AnimeRepository,
|
||||
private val setAnimeUpdateInterval: SetAnimeUpdateInterval,
|
||||
) {
|
||||
|
||||
suspend fun await(animeUpdate: AnimeUpdate): Boolean {
|
||||
|
@ -77,16 +77,15 @@ class UpdateAnime(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateIntervalMeta(
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
anime: Anime,
|
||||
episodes: List<Episode>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
||||
fetchRange: Pair<Long, Long> = setAnimeUpdateInterval.getCurrentFetchRange(zonedDateTime),
|
||||
): Boolean {
|
||||
val newMeta = updateIntervalMeta(anime, episodes, zonedDateTime, setCurrentFetchRange)
|
||||
|
||||
return if (newMeta != null) {
|
||||
animeRepository.updateAnime(newMeta)
|
||||
val updateAnime = setAnimeUpdateInterval.updateInterval(anime, episodes, zonedDateTime, fetchRange)
|
||||
return if (updateAnime != null) {
|
||||
animeRepository.updateAnime(updateAnime)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@ package eu.kanade.domain.entries.manga.interactor
|
|||
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
||||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.entries.manga.interactor.getCurrentFetchRange
|
||||
import tachiyomi.domain.entries.manga.interactor.updateIntervalMeta
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||
|
@ -17,6 +16,7 @@ import java.util.Date
|
|||
|
||||
class UpdateManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val setMangaUpdateInterval: SetMangaUpdateInterval,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||
|
@ -77,16 +77,15 @@ class UpdateManga(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateIntervalMeta(
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
||||
fetchRange: Pair<Long, Long> = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime),
|
||||
): Boolean {
|
||||
val newMeta = updateIntervalMeta(manga, chapters, zonedDateTime, setCurrentFetchRange)
|
||||
|
||||
return if (newMeta != null) {
|
||||
mangaRepository.updateManga(newMeta)
|
||||
val updatedManga = setMangaUpdateInterval.updateInterval(manga, chapters, zonedDateTime, fetchRange)
|
||||
return if (updatedManga != null) {
|
||||
mangaRepository.updateManga(updatedManga)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import tachiyomi.source.local.entries.manga.isLocal
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.Long.max
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.TreeSet
|
||||
|
||||
|
@ -48,6 +49,9 @@ class SyncChaptersWithSource(
|
|||
rawSourceChapters: List<SChapter>,
|
||||
manga: Manga,
|
||||
source: MangaSource,
|
||||
manualFetch: Boolean = false,
|
||||
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
||||
): List<Chapter> {
|
||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||
throw NoChaptersException()
|
||||
|
@ -134,6 +138,14 @@ class SyncChaptersWithSource(
|
|||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
if (manualFetch || manga.calculateInterval == 0 || manga.nextUpdate < fetchRange.first) {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga,
|
||||
dbChapters,
|
||||
zoneDateTime,
|
||||
fetchRange,
|
||||
)
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
|
@ -188,6 +200,8 @@ class SyncChaptersWithSource(
|
|||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
val newChapters = chapterRepository.getChapterByMangaId(manga.id)
|
||||
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
|
||||
|
||||
// Set this manga as updated since chapters were changed
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
|
|
|
@ -23,6 +23,7 @@ import tachiyomi.source.local.entries.anime.isLocal
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.Long.max
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.TreeSet
|
||||
|
||||
|
@ -48,6 +49,9 @@ class SyncEpisodesWithSource(
|
|||
rawSourceEpisodes: List<SEpisode>,
|
||||
anime: Anime,
|
||||
source: AnimeSource,
|
||||
manualFetch: Boolean = false,
|
||||
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
||||
): List<Episode> {
|
||||
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
|
||||
throw NoEpisodesException()
|
||||
|
@ -134,6 +138,14 @@ class SyncEpisodesWithSource(
|
|||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
if (manualFetch || anime.calculateInterval == 0 || anime.nextUpdate < fetchRange.first) {
|
||||
updateAnime.awaitUpdateFetchInterval(
|
||||
anime,
|
||||
dbEpisodes,
|
||||
zoneDateTime,
|
||||
fetchRange,
|
||||
)
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
|
@ -188,6 +200,8 @@ class SyncEpisodesWithSource(
|
|||
val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
|
||||
updateEpisode.awaitAll(episodeUpdates)
|
||||
}
|
||||
val newChapters = episodeRepository.getEpisodeByAnimeId(anime.id)
|
||||
updateAnime.awaitUpdateFetchInterval(anime, newChapters, zoneDateTime, fetchRange)
|
||||
|
||||
// Set this anime as updated since episodes were changed
|
||||
// Note that last_update actually represents last time the episode list changed at all
|
||||
|
|
|
@ -13,18 +13,13 @@ import java.util.Date
|
|||
fun RelativeDateHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
date: Date,
|
||||
relativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
ListGroupHeader(
|
||||
modifier = modifier,
|
||||
text = remember {
|
||||
date.toRelativeString(
|
||||
context,
|
||||
relativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
date.toRelativeString(context, dateFormat)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
package eu.kanade.presentation.entries
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.entries.anime.interactor.MAX_GRACE_PERIOD
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
|
||||
@Composable
|
||||
fun DeleteItemsDialog(
|
||||
|
@ -39,3 +51,51 @@ fun DeleteItemsDialog(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetIntervalDialog(
|
||||
interval: Int,
|
||||
onDismissRequest: () -> Unit,
|
||||
onValueChanged: (Int) -> Unit,
|
||||
) {
|
||||
var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = { Text(text = stringResource(R.string.manga_modify_calculated_interval_title)) },
|
||||
text = {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||
val items = (0..MAX_GRACE_PERIOD).map {
|
||||
if (it == 0) {
|
||||
stringResource(R.string.label_default)
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
WheelTextPicker(
|
||||
size = size,
|
||||
items = items,
|
||||
startIndex = intervalValue,
|
||||
onSelectionChanged = { intervalValue = it },
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onValueChanged(intervalValue)
|
||||
onDismissRequest()
|
||||
},) {
|
||||
Text(text = stringResource(R.string.action_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ import java.util.concurrent.TimeUnit
|
|||
fun AnimeScreen(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
dateFormat: DateFormat,
|
||||
isTabletUi: Boolean,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -127,6 +127,7 @@ fun AnimeScreen(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
changeAnimeSkipIntro: (() -> Unit)?,
|
||||
|
||||
|
@ -160,8 +161,8 @@ fun AnimeScreen(
|
|||
AnimeScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
intervalDisplay = intervalDisplay,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||
|
@ -183,6 +184,7 @@ fun AnimeScreen(
|
|||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
|
@ -199,12 +201,12 @@ fun AnimeScreen(
|
|||
AnimeScreenLargeImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||
alwaysUseExternalPlayer = alwaysUseExternalPlayer,
|
||||
dateFormat = dateFormat,
|
||||
intervalDisplay = intervalDisplay,
|
||||
onBackClicked = onBackClicked,
|
||||
onEpisodeClicked = onEpisodeClicked,
|
||||
onDownloadEpisode = onDownloadEpisode,
|
||||
|
@ -222,6 +224,7 @@ fun AnimeScreen(
|
|||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
changeAnimeSkipIntro = changeAnimeSkipIntro,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
|
@ -242,8 +245,8 @@ fun AnimeScreen(
|
|||
private fun AnimeScreenSmallImpl(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
showNextEpisodeAirTime: Boolean,
|
||||
|
@ -272,6 +275,7 @@ private fun AnimeScreenSmallImpl(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
changeAnimeSkipIntro: (() -> Unit)?,
|
||||
onSettingsClicked: (() -> Unit)?,
|
||||
|
@ -312,9 +316,11 @@ private fun AnimeScreenSmallImpl(
|
|||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||
label = "titleAlpha",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||
label = "bgAlpha",
|
||||
)
|
||||
EntryToolbar(
|
||||
title = state.anime.title,
|
||||
|
@ -426,10 +432,13 @@ private fun AnimeScreenSmallImpl(
|
|||
AnimeActionRow(
|
||||
favorite = state.anime.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
intervalDisplay = intervalDisplay,
|
||||
isUserIntervalMode = state.anime.calculateInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onWebViewLongClicked = onWebViewLongClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
)
|
||||
}
|
||||
|
@ -488,7 +497,6 @@ private fun AnimeScreenSmallImpl(
|
|||
sharedEpisodeItems(
|
||||
anime = state.anime,
|
||||
episodes = episodes,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
|
@ -508,8 +516,8 @@ private fun AnimeScreenSmallImpl(
|
|||
fun AnimeScreenLargeImpl(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
showNextEpisodeAirTime: Boolean,
|
||||
|
@ -538,6 +546,7 @@ fun AnimeScreenLargeImpl(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
changeAnimeSkipIntro: (() -> Unit)?,
|
||||
onSettingsClicked: (() -> Unit)?,
|
||||
|
@ -676,10 +685,13 @@ fun AnimeScreenLargeImpl(
|
|||
AnimeActionRow(
|
||||
favorite = state.anime.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
intervalDisplay = intervalDisplay,
|
||||
isUserIntervalMode = state.anime.calculateInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onWebViewLongClicked = onWebViewLongClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
)
|
||||
ExpandableAnimeDescription(
|
||||
|
@ -745,7 +757,6 @@ fun AnimeScreenLargeImpl(
|
|||
sharedEpisodeItems(
|
||||
anime = state.anime,
|
||||
episodes = episodes,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
|
@ -816,7 +827,6 @@ private fun SharedAnimeBottomActionMenu(
|
|||
private fun LazyListScope.sharedEpisodeItems(
|
||||
anime: Anime,
|
||||
episodes: List<EpisodeItem>,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -845,11 +855,7 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
date = episodeItem.episode.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
Date(it).toRelativeString(context, dateFormat)
|
||||
},
|
||||
watchProgress = episodeItem.episode.lastSecondSeen
|
||||
.takeIf { !episodeItem.episode.seen && it > 0L }
|
||||
|
|
|
@ -26,6 +26,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
|||
import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||
import androidx.compose.material.icons.outlined.AttachMoney
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
|
@ -166,14 +167,19 @@ fun AnimeActionRow(
|
|||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onWebViewLongClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
) {
|
||||
val interval: Pair<Int, Int>? = intervalDisplay()
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
AnimeActionButton(
|
||||
title = if (favorite) {
|
||||
stringResource(R.string.in_library)
|
||||
|
@ -185,6 +191,19 @@ fun AnimeActionRow(
|
|||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && interval != null) {
|
||||
AnimeActionButton(
|
||||
title =
|
||||
if (interval.first == interval.second) {
|
||||
pluralStringResource(id = R.plurals.day, count = interval.second, interval.second)
|
||||
} else {
|
||||
pluralStringResource(id = R.plurals.range_interval_day, count = interval.second, interval.first, interval.second)
|
||||
},
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
)
|
||||
}
|
||||
if (onTrackingClicked != null) {
|
||||
AnimeActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
|
|
|
@ -90,7 +90,7 @@ import java.util.Date
|
|||
fun MangaScreen(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
dateFormat: DateFormat,
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -118,6 +118,7 @@ fun MangaScreen(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
|
||||
// For bottom action menu
|
||||
|
@ -150,8 +151,8 @@ fun MangaScreen(
|
|||
MangaScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
intervalDisplay = intervalDisplay,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onBackClicked = onBackClicked,
|
||||
|
@ -171,6 +172,7 @@ fun MangaScreen(
|
|||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
|
@ -186,10 +188,10 @@ fun MangaScreen(
|
|||
MangaScreenLargeImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
dateFormat = dateFormat,
|
||||
intervalDisplay = intervalDisplay,
|
||||
onBackClicked = onBackClicked,
|
||||
onChapterClicked = onChapterClicked,
|
||||
onDownloadChapter = onDownloadChapter,
|
||||
|
@ -207,6 +209,7 @@ fun MangaScreen(
|
|||
onShareClicked = onShareClicked,
|
||||
onDownloadActionClicked = onDownloadActionClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
|
@ -225,8 +228,8 @@ fun MangaScreen(
|
|||
private fun MangaScreenSmallImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -253,6 +256,7 @@ private fun MangaScreenSmallImpl(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
onSettingsClicked: (() -> Unit)?,
|
||||
|
||||
|
@ -293,9 +297,11 @@ private fun MangaScreenSmallImpl(
|
|||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||
label = "titleAlpha",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||
label = "bgAlpha",
|
||||
)
|
||||
EntryToolbar(
|
||||
title = state.manga.title,
|
||||
|
@ -400,10 +406,13 @@ private fun MangaScreenSmallImpl(
|
|||
MangaActionRow(
|
||||
favorite = state.manga.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
intervalDisplay = intervalDisplay,
|
||||
isUserIntervalMode = state.manga.calculateInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onWebViewLongClicked = onWebViewLongClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
)
|
||||
}
|
||||
|
@ -437,7 +446,6 @@ private fun MangaScreenSmallImpl(
|
|||
sharedChapterItems(
|
||||
manga = state.manga,
|
||||
chapters = chapters,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
|
@ -456,8 +464,8 @@ private fun MangaScreenSmallImpl(
|
|||
fun MangaScreenLargeImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
onBackClicked: () -> Unit,
|
||||
|
@ -484,6 +492,7 @@ fun MangaScreenLargeImpl(
|
|||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
onSettingsClicked: (() -> Unit)?,
|
||||
|
||||
|
@ -618,10 +627,13 @@ fun MangaScreenLargeImpl(
|
|||
MangaActionRow(
|
||||
favorite = state.manga.favorite,
|
||||
trackingCount = state.trackingCount,
|
||||
intervalDisplay = intervalDisplay,
|
||||
isUserIntervalMode = state.manga.calculateInterval < 0,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onWebViewLongClicked = onWebViewLongClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onEditIntervalClicked = onEditIntervalClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
)
|
||||
ExpandableMangaDescription(
|
||||
|
@ -662,7 +674,6 @@ fun MangaScreenLargeImpl(
|
|||
sharedChapterItems(
|
||||
manga = state.manga,
|
||||
chapters = chapters,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
|
@ -725,7 +736,6 @@ private fun SharedMangaBottomActionMenu(
|
|||
private fun LazyListScope.sharedChapterItems(
|
||||
manga: Manga,
|
||||
chapters: List<ChapterItem>,
|
||||
dateRelativeTime: Int,
|
||||
dateFormat: DateFormat,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -754,11 +764,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||
date = chapterItem.chapter.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
Date(it).toRelativeString(context, dateFormat)
|
||||
},
|
||||
readProgress = chapterItem.chapter.lastPageRead
|
||||
.takeIf { !chapterItem.chapter.read && it > 0L }
|
||||
|
|
|
@ -26,6 +26,7 @@ import androidx.compose.foundation.text.selection.SelectionContainer
|
|||
import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.AttachMoney
|
||||
import androidx.compose.material.icons.outlined.Block
|
||||
|
@ -166,14 +167,19 @@ fun MangaActionRow(
|
|||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
intervalDisplay: () -> Pair<Int, Int>?,
|
||||
isUserIntervalMode: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onWebViewLongClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onEditIntervalClicked: (() -> Unit)?,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
) {
|
||||
val interval: Pair<Int, Int>? = intervalDisplay()
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
stringResource(R.string.in_library)
|
||||
|
@ -185,6 +191,19 @@ fun MangaActionRow(
|
|||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onEditIntervalClicked != null && interval != null) {
|
||||
MangaActionButton(
|
||||
title =
|
||||
if (interval.first == interval.second) {
|
||||
pluralStringResource(id = R.plurals.day, count = interval.second, interval.second)
|
||||
} else {
|
||||
pluralStringResource(id = R.plurals.range_interval_day, count = interval.second, interval.first, interval.second)
|
||||
},
|
||||
icon = Icons.Default.HourglassEmpty,
|
||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onEditIntervalClicked,
|
||||
)
|
||||
}
|
||||
if (onTrackingClicked != null) {
|
||||
MangaActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
|
|
|
@ -24,7 +24,6 @@ fun AnimeHistoryContent(
|
|||
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
||||
preferences: UiPreferences = Injekt.get(),
|
||||
) {
|
||||
val relativeTime: Int = remember { preferences.relativeTime().get() }
|
||||
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
|
@ -45,7 +44,6 @@ fun AnimeHistoryContent(
|
|||
RelativeDateHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ fun MangaHistoryContent(
|
|||
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
||||
preferences: UiPreferences = Injekt.get(),
|
||||
) {
|
||||
val relativeTime: Int = remember { preferences.relativeTime().get() }
|
||||
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
|
@ -43,7 +42,6 @@ fun MangaHistoryContent(
|
|||
RelativeDateHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
return listOf(
|
||||
getThemeGroup(context = context, uiPreferences = uiPreferences),
|
||||
getDisplayGroup(context = context, uiPreferences = uiPreferences),
|
||||
getTimestampGroup(uiPreferences = uiPreferences),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -124,6 +123,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
): Preference.PreferenceGroup {
|
||||
val langs = remember { getLangs(context) }
|
||||
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
|
||||
val now = remember { Date().time }
|
||||
|
||||
LaunchedEffect(currentLanguage) {
|
||||
val locale = if (currentLanguage.isEmpty()) {
|
||||
|
@ -186,25 +186,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getTimestampGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup {
|
||||
val now = remember { Date().time }
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(R.string.pref_category_timestamps),
|
||||
preferenceItems = listOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.relativeTime(),
|
||||
title = stringResource(R.string.pref_relative_format),
|
||||
entries = mapOf(
|
||||
0 to stringResource(R.string.off),
|
||||
2 to stringResource(R.string.pref_relative_time_short),
|
||||
7 to stringResource(R.string.pref_relative_time_long),
|
||||
),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
pref = uiPreferences.dateFormat(),
|
||||
title = stringResource(R.string.pref_date_format),
|
||||
|
@ -217,7 +198,6 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLangs(context: Context): Map<String, String> {
|
||||
val langs = mutableListOf<Pair<String, String>>()
|
||||
val parser = context.resources.getXml(R.xml.locales_config)
|
||||
|
|
|
@ -292,20 +292,17 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
true
|
||||
},
|
||||
),
|
||||
// TODO: remove isDevFlavor checks once functionality is available
|
||||
Preference.PreferenceItem.MultiSelectListPreference(
|
||||
pref = libraryUpdateDeviceRestrictionPref,
|
||||
enabled = libraryUpdateInterval > 0,
|
||||
title = stringResource(R.string.pref_library_update_restriction),
|
||||
subtitle = stringResource(R.string.restrictions),
|
||||
entries = buildMap {
|
||||
put(ENTRY_HAS_UNVIEWED, stringResource(R.string.pref_update_only_completely_read))
|
||||
put(ENTRY_NON_VIEWED, stringResource(R.string.pref_update_only_started))
|
||||
put(ENTRY_NON_COMPLETED, stringResource(R.string.pref_update_only_non_completed))
|
||||
if (isDevFlavor) {
|
||||
put(ENTRY_OUTSIDE_RELEASE_PERIOD, stringResource(R.string.pref_update_only_in_release_period))
|
||||
}
|
||||
},
|
||||
entries = mapOf(
|
||||
ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
|
||||
ENTRY_NON_VIEWED to stringResource(R.string.pref_update_only_started),
|
||||
ENTRY_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
|
||||
),
|
||||
onValueChanged = {
|
||||
// Post to event looper to allow the preference to be updated.
|
||||
ContextCompat.getMainExecutor(context).execute {
|
||||
|
|
|
@ -40,7 +40,6 @@ fun AnimeUpdateScreen(
|
|||
snackbarHostState: SnackbarHostState,
|
||||
contentPadding: PaddingValues,
|
||||
lastUpdated: Long,
|
||||
relativeTime: Int,
|
||||
onClickCover: (AnimeUpdatesItem) -> Unit,
|
||||
onSelectAll: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
|
@ -101,7 +100,7 @@ fun AnimeUpdateScreen(
|
|||
animeUpdatesLastUpdatedItem(lastUpdated)
|
||||
}
|
||||
animeUpdatesUiItems(
|
||||
uiModels = state.getUiModel(context, relativeTime),
|
||||
uiModels = state.getUiModel(context),
|
||||
selectionMode = state.selectionMode,
|
||||
onUpdateSelected = onUpdateSelected,
|
||||
onClickCover = onClickCover,
|
||||
|
|
|
@ -37,7 +37,6 @@ fun MangaUpdateScreen(
|
|||
snackbarHostState: SnackbarHostState,
|
||||
contentPadding: PaddingValues,
|
||||
lastUpdated: Long,
|
||||
relativeTime: Int,
|
||||
onClickCover: (MangaUpdatesItem) -> Unit,
|
||||
onSelectAll: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
|
@ -98,7 +97,7 @@ fun MangaUpdateScreen(
|
|||
}
|
||||
|
||||
mangaUpdatesUiItems(
|
||||
uiModels = state.getUiModel(context, relativeTime),
|
||||
uiModels = state.getUiModel(context),
|
||||
selectionMode = state.selectionMode,
|
||||
onUpdateSelected = onUpdateSelected,
|
||||
onClickCover = onClickCover,
|
||||
|
|
|
@ -72,7 +72,7 @@ fun WebViewScreenContent(
|
|||
super.onPageFinished(view, url)
|
||||
scope.launch {
|
||||
val html = view.getHtml()
|
||||
showCloudflareHelp = "Checking if the site connection is secure" in html
|
||||
showCloudflareHelp = "window._cf_chl_opt" in html
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Intent
|
|||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
import eu.kanade.domain.entries.manga.interactor.UpdateManga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnime
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory
|
||||
|
@ -28,14 +30,21 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
|||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import tachiyomi.domain.items.episode.repository.EpisodeRepository
|
||||
import tachiyomi.domain.track.anime.model.AnimeTrack
|
||||
import tachiyomi.domain.track.manga.model.MangaTrack
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
|
@ -43,6 +52,17 @@ class BackupRestorer(
|
|||
private val context: Context,
|
||||
private val notifier: BackupNotifier,
|
||||
) {
|
||||
private val updateManga: UpdateManga = Injekt.get()
|
||||
private val chapterRepository: ChapterRepository = Injekt.get()
|
||||
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
|
||||
|
||||
private val updateAnime: UpdateAnime = Injekt.get()
|
||||
private val episodeRepository: EpisodeRepository = Injekt.get()
|
||||
private val setAnimeUpdateInterval: SetAnimeUpdateInterval = Injekt.get()
|
||||
|
||||
private var zonedDateTime = ZonedDateTime.now()
|
||||
private var currentMangaRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||
private var currentAnimeRange = setAnimeUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||
|
||||
private var backupManager = BackupManager(context)
|
||||
|
||||
|
@ -120,6 +140,10 @@ class BackupRestorer(
|
|||
val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
|
||||
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
||||
|
||||
zonedDateTime = ZonedDateTime.now()
|
||||
currentMangaRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||
currentAnimeRange = setAnimeUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||
|
||||
return coroutineScope {
|
||||
// Restore individual manga
|
||||
backup.backupManga.forEach {
|
||||
|
@ -182,7 +206,7 @@ class BackupRestorer(
|
|||
|
||||
try {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
||||
if (dbManga == null) {
|
||||
val restoredManga = if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||
} else {
|
||||
|
@ -192,6 +216,8 @@ class BackupRestorer(
|
|||
// Fetch rest of manga information
|
||||
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
|
||||
}
|
||||
val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id)
|
||||
updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentMangaRange)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||
|
@ -219,10 +245,11 @@ class BackupRestorer(
|
|||
history: List<BackupHistory>,
|
||||
tracks: List<MangaTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
): Manga {
|
||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
||||
backupManager.restoreChapters(fetchedManga, chapters)
|
||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||
return fetchedManga
|
||||
}
|
||||
|
||||
private suspend fun restoreNewManga(
|
||||
|
@ -232,9 +259,10 @@ class BackupRestorer(
|
|||
history: List<BackupHistory>,
|
||||
tracks: List<MangaTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
): Manga {
|
||||
backupManager.restoreChapters(backupManga, chapters)
|
||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||
return backupManga
|
||||
}
|
||||
|
||||
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<MangaTrack>, backupCategories: List<BackupCategory>) {
|
||||
|
@ -253,7 +281,7 @@ class BackupRestorer(
|
|||
|
||||
try {
|
||||
val dbAnime = backupManager.getAnimeFromDatabase(anime.url, anime.source)
|
||||
if (dbAnime == null) {
|
||||
val restoredAnime = if (dbAnime == null) {
|
||||
// Anime not in database
|
||||
restoreExistingAnime(anime, episodes, categories, history, tracks, backupCategories)
|
||||
} else {
|
||||
|
@ -263,6 +291,8 @@ class BackupRestorer(
|
|||
// Fetch rest of anime information
|
||||
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
|
||||
}
|
||||
val updatedEpisodes = episodeRepository.getEpisodeByAnimeId(restoredAnime.id)
|
||||
updateAnime.awaitUpdateFetchInterval(restoredAnime, updatedEpisodes, zonedDateTime, currentAnimeRange)
|
||||
} catch (e: Exception) {
|
||||
val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
|
||||
errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}")
|
||||
|
@ -290,10 +320,11 @@ class BackupRestorer(
|
|||
history: List<BackupAnimeHistory>,
|
||||
tracks: List<AnimeTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
): Anime {
|
||||
val fetchedAnime = backupManager.restoreNewAnime(anime)
|
||||
backupManager.restoreEpisodes(fetchedAnime, episodes)
|
||||
restoreExtras(fetchedAnime, categories, history, tracks, backupCategories)
|
||||
return fetchedAnime
|
||||
}
|
||||
|
||||
private suspend fun restoreNewAnime(
|
||||
|
@ -303,9 +334,10 @@ class BackupRestorer(
|
|||
history: List<BackupAnimeHistory>,
|
||||
tracks: List<AnimeTrack>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
) {
|
||||
): Anime {
|
||||
backupManager.restoreEpisodes(backupAnime, episodes)
|
||||
restoreExtras(backupAnime, categories, history, tracks, backupCategories)
|
||||
return backupAnime
|
||||
}
|
||||
|
||||
private suspend fun restoreExtras(anime: Anime, categories: List<Int>, history: List<BackupAnimeHistory>, tracks: List<AnimeTrack>, backupCategories: List<BackupCategory>) {
|
||||
|
|
|
@ -56,6 +56,7 @@ import tachiyomi.domain.category.model.Category
|
|||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.anime.model.toAnimeUpdate
|
||||
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
||||
|
@ -70,6 +71,7 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY
|
|||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
|
||||
import tachiyomi.domain.source.anime.model.AnimeSourceNotInstalledException
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks
|
||||
|
@ -77,6 +79,7 @@ import tachiyomi.domain.track.anime.interactor.InsertAnimeTrack
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -101,6 +104,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
private val getTracks: GetAnimeTracks = Injekt.get()
|
||||
private val insertTrack: InsertAnimeTrack = Injekt.get()
|
||||
private val syncEpisodesWithTrackServiceTwoWay: SyncEpisodesWithTrackServiceTwoWay = Injekt.get()
|
||||
private val setAnimeUpdateInterval: SetAnimeUpdateInterval = Injekt.get()
|
||||
|
||||
private val notifier = AnimeLibraryUpdateNotifier(context)
|
||||
|
||||
|
@ -227,6 +231,10 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
val hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val fetchRange = setAnimeUpdateInterval.getCurrentFetchRange(now)
|
||||
val higherLimit = fetchRange.second
|
||||
|
||||
coroutineScope {
|
||||
animeToUpdate.groupBy { it.anime.source }.values
|
||||
.map { animeInSource ->
|
||||
|
@ -247,6 +255,9 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
anime,
|
||||
) {
|
||||
when {
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && anime.nextUpdate > higherLimit ->
|
||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||
|
||||
ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED ->
|
||||
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed))
|
||||
|
||||
|
@ -261,7 +272,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
|
||||
else -> {
|
||||
try {
|
||||
val newEpisodes = updateAnime(anime)
|
||||
val newEpisodes = updateAnime(anime, now, fetchRange)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
if (newEpisodes.isNotEmpty()) {
|
||||
|
@ -333,7 +344,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
* @param anime the anime to update.
|
||||
* @return a pair of the inserted and removed episodes.
|
||||
*/
|
||||
private suspend fun updateAnime(anime: Anime): List<Episode> {
|
||||
private suspend fun updateAnime(anime: Anime, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Episode> {
|
||||
val source = sourceManager.getOrStub(anime.source)
|
||||
|
||||
// Update anime metadata if needed
|
||||
|
@ -348,7 +359,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
// to get latest data so it doesn't get overwritten later on
|
||||
val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList()
|
||||
|
||||
return syncEpisodesWithSource.await(episodes, dbAnime, source)
|
||||
return syncEpisodesWithSource.await(episodes, dbAnime, source, false, zoneDateTime, fetchRange)
|
||||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
|
|
|
@ -56,6 +56,7 @@ import tachiyomi.domain.category.model.Category
|
|||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.entries.manga.model.toMangaUpdate
|
||||
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
||||
|
@ -70,6 +71,7 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY
|
|||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_HAS_UNVIEWED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
|
||||
import tachiyomi.domain.source.manga.model.SourceNotInstalledException
|
||||
import tachiyomi.domain.source.manga.service.MangaSourceManager
|
||||
import tachiyomi.domain.track.manga.interactor.GetMangaTracks
|
||||
|
@ -77,6 +79,7 @@ import tachiyomi.domain.track.manga.interactor.InsertMangaTrack
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -101,6 +104,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
private val getTracks: GetMangaTracks = Injekt.get()
|
||||
private val insertTrack: InsertMangaTrack = Injekt.get()
|
||||
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
|
||||
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
|
||||
|
||||
private val notifier = MangaLibraryUpdateNotifier(context)
|
||||
|
||||
|
@ -227,6 +231,10 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
val hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val fetchRange = setMangaUpdateInterval.getCurrentFetchRange(now)
|
||||
val higherLimit = fetchRange.second
|
||||
|
||||
coroutineScope {
|
||||
mangaToUpdate.groupBy { it.manga.source }.values
|
||||
.map { mangaInSource ->
|
||||
|
@ -247,6 +255,9 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
manga,
|
||||
) {
|
||||
when {
|
||||
ENTRY_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit ->
|
||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||
|
||||
ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
||||
|
||||
|
@ -261,7 +272,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
|
||||
else -> {
|
||||
try {
|
||||
val newChapters = updateManga(manga)
|
||||
val newChapters = updateManga(manga, now, fetchRange)
|
||||
.sortedByDescending { it.sourceOrder }
|
||||
|
||||
if (newChapters.isNotEmpty()) {
|
||||
|
@ -333,7 +344,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
* @param manga the manga to update.
|
||||
* @return a pair of the inserted and removed chapters.
|
||||
*/
|
||||
private suspend fun updateManga(manga: Manga): List<Chapter> {
|
||||
private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
|
||||
// Update manga metadata if needed
|
||||
|
@ -348,7 +359,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
// to get latest data so it doesn't get overwritten later on
|
||||
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
|
||||
|
||||
return syncChaptersWithSource.await(chapters, dbManga, source)
|
||||
return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
|
||||
}
|
||||
|
||||
private suspend fun updateCovers() {
|
||||
|
|
|
@ -27,6 +27,7 @@ import eu.kanade.presentation.category.ChangeCategoryDialog
|
|||
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
|
||||
import eu.kanade.presentation.entries.DeleteItemsDialog
|
||||
import eu.kanade.presentation.entries.EditCoverAction
|
||||
import eu.kanade.presentation.entries.SetIntervalDialog
|
||||
import eu.kanade.presentation.entries.anime.AnimeScreen
|
||||
import eu.kanade.presentation.entries.anime.DuplicateAnimeDialog
|
||||
import eu.kanade.presentation.entries.anime.EpisodeOptionsDialogScreen
|
||||
|
@ -104,8 +105,8 @@ class AnimeScreen(
|
|||
AnimeScreen(
|
||||
state = successState,
|
||||
snackbarHostState = screenModel.snackbarHostState,
|
||||
dateRelativeTime = screenModel.relativeTime,
|
||||
dateFormat = screenModel.dateFormat,
|
||||
intervalDisplay = screenModel::intervalDisplay,
|
||||
isTabletUi = isTabletUi(),
|
||||
episodeSwipeStartAction = screenModel.episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = screenModel.episodeSwipeEndAction,
|
||||
|
@ -139,7 +140,8 @@ class AnimeScreen(
|
|||
onCoverClicked = screenModel::showCoverDialog,
|
||||
onShareClicked = { shareAnime(context, screenModel.anime, screenModel.source) }.takeIf { isAnimeHttpSource },
|
||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.anime.favorite },
|
||||
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.anime.favorite },
|
||||
onEditIntervalClicked = screenModel::showSetAnimeIntervalDialog.takeIf { screenModel.isIntervalEnabled && successState.anime.favorite },
|
||||
onMigrateClicked = { navigator.push(MigrateAnimeSearchScreen(successState.anime.id)) }.takeIf { successState.anime.favorite },
|
||||
changeAnimeSkipIntro = screenModel::showAnimeSkipIntroDialog.takeIf { successState.anime.favorite },
|
||||
onMultiBookmarkClicked = screenModel::bookmarkEpisodes,
|
||||
|
@ -233,6 +235,13 @@ class AnimeScreen(
|
|||
LoadingScreen(Modifier.systemBarsPadding())
|
||||
}
|
||||
}
|
||||
is AnimeScreenModel.Dialog.SetAnimeInterval -> {
|
||||
SetIntervalDialog(
|
||||
interval = if (dialog.anime.calculateInterval < 0) -dialog.anime.calculateInterval else 0,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onValueChanged = { screenModel.setFetchRangeInterval(dialog.anime, it) },
|
||||
)
|
||||
}
|
||||
AnimeScreenModel.Dialog.ChangeAnimeSkipIntro -> {
|
||||
fun updateSkipIntroLength(newLength: Long) {
|
||||
scope.launchIO {
|
||||
|
|
|
@ -8,7 +8,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import eu.kanade.core.preference.asState
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
|
@ -65,6 +64,7 @@ import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
|
|||
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||
import tachiyomi.domain.entries.applyFilter
|
||||
import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
|
||||
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
|
||||
|
@ -79,6 +79,7 @@ import tachiyomi.source.local.entries.anime.isLocal
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Calendar
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class AnimeScreenModel(
|
||||
val context: Context,
|
||||
|
@ -103,6 +104,7 @@ class AnimeScreenModel(
|
|||
private val getCategories: GetAnimeCategories = Injekt.get(),
|
||||
private val getTracks: GetAnimeTracks = Injekt.get(),
|
||||
private val setAnimeCategories: SetAnimeCategories = Injekt.get(),
|
||||
private val animeRepository: AnimeRepository = Injekt.get(),
|
||||
internal val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
) : StateScreenModel<AnimeScreenModel.State>(State.Loading) {
|
||||
|
@ -131,9 +133,12 @@ class AnimeScreenModel(
|
|||
val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get()
|
||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||
|
||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||
|
||||
val isIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
||||
private val leadDay = libraryPreferences.leadingAnimeExpectedDays().get()
|
||||
private val followDay = libraryPreferences.followingAnimeExpectedDays().get()
|
||||
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||
private val selectedEpisodeIds: HashSet<Long> = HashSet()
|
||||
|
||||
|
@ -320,7 +325,7 @@ class AnimeScreenModel(
|
|||
// Choose a category
|
||||
else -> {
|
||||
isFromChangeCategory = true
|
||||
promptChangeCategories()
|
||||
showChangeCategoryDialog()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -350,7 +355,7 @@ class AnimeScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun promptChangeCategories() {
|
||||
fun showChangeCategoryDialog() {
|
||||
val anime = successState?.anime ?: return
|
||||
coroutineScope.launch {
|
||||
val categories = getCategories()
|
||||
|
@ -366,6 +371,37 @@ class AnimeScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun showSetAnimeIntervalDialog() {
|
||||
val anime = successState?.anime ?: return
|
||||
updateSuccessState {
|
||||
it.copy(dialog = Dialog.SetAnimeInterval(anime))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this should be in the state/composables
|
||||
fun intervalDisplay(): Pair<Int, Int>? {
|
||||
val anime = successState?.anime ?: return null
|
||||
val effInterval = anime.calculateInterval
|
||||
return 1.coerceAtLeast(effInterval.absoluteValue - leadDay) to (effInterval.absoluteValue + followDay)
|
||||
}
|
||||
|
||||
fun setFetchRangeInterval(anime: Anime, newInterval: Int) {
|
||||
val interval = when (newInterval) {
|
||||
// reset interval 0 default to trigger recalculation
|
||||
// only reset if interval is custom, which is negative
|
||||
0 -> if (anime.calculateInterval < 0) 0 else anime.calculateInterval
|
||||
else -> -newInterval
|
||||
}
|
||||
coroutineScope.launchIO {
|
||||
updateAnime.awaitUpdateFetchInterval(
|
||||
anime.copy(calculateInterval = interval),
|
||||
successState?.episodes?.map { it.episode }.orEmpty(),
|
||||
)
|
||||
val newAnime = animeRepository.getAnimeById(animeId)
|
||||
updateSuccessState { it.copy(anime = newAnime) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the anime has any downloads.
|
||||
*/
|
||||
|
@ -519,6 +555,7 @@ class AnimeScreenModel(
|
|||
episodes,
|
||||
state.anime,
|
||||
state.source,
|
||||
manualFetch,
|
||||
)
|
||||
|
||||
if (manualFetch) {
|
||||
|
@ -536,6 +573,8 @@ class AnimeScreenModel(
|
|||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(message = message)
|
||||
}
|
||||
val newAnime = animeRepository.getAnimeById(animeId)
|
||||
updateSuccessState { it.copy(anime = newAnime, isRefreshingData = false) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -972,6 +1011,7 @@ class AnimeScreenModel(
|
|||
data class ChangeCategory(val anime: Anime, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
||||
data class DeleteEpisodes(val episodes: List<Episode>) : Dialog
|
||||
data class DuplicateAnime(val anime: Anime, val duplicate: Anime) : Dialog
|
||||
data class SetAnimeInterval(val anime: Anime) : Dialog
|
||||
data class ShowQualities(val episode: Episode, val anime: Anime, val source: AnimeSource) : Dialog
|
||||
data object ChangeAnimeSkipIntro : Dialog
|
||||
data object SettingsSheet : Dialog
|
||||
|
@ -1009,7 +1049,7 @@ class AnimeScreenModel(
|
|||
|
||||
sealed interface State {
|
||||
@Immutable
|
||||
object Loading : State
|
||||
data object Loading : State
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
|
|
|
@ -26,6 +26,7 @@ import eu.kanade.presentation.category.ChangeCategoryDialog
|
|||
import eu.kanade.presentation.components.NavigatorAdaptiveSheet
|
||||
import eu.kanade.presentation.entries.DeleteItemsDialog
|
||||
import eu.kanade.presentation.entries.EditCoverAction
|
||||
import eu.kanade.presentation.entries.SetIntervalDialog
|
||||
import eu.kanade.presentation.entries.manga.ChapterSettingsDialog
|
||||
import eu.kanade.presentation.entries.manga.DuplicateMangaDialog
|
||||
import eu.kanade.presentation.entries.manga.MangaScreen
|
||||
|
@ -99,8 +100,8 @@ class MangaScreen(
|
|||
MangaScreen(
|
||||
state = successState,
|
||||
snackbarHostState = screenModel.snackbarHostState,
|
||||
dateRelativeTime = screenModel.relativeTime,
|
||||
dateFormat = screenModel.dateFormat,
|
||||
intervalDisplay = screenModel::intervalDisplay,
|
||||
isTabletUi = isTabletUi(),
|
||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||
|
@ -122,7 +123,8 @@ class MangaScreen(
|
|||
onCoverClicked = screenModel::showCoverDialog,
|
||||
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
|
||||
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
|
||||
onEditIntervalClicked = screenModel::showSetMangaIntervalDialog.takeIf { screenModel.isIntervalEnabled && successState.manga.favorite },
|
||||
onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
|
||||
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
||||
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
|
||||
|
@ -215,6 +217,13 @@ class MangaScreen(
|
|||
LoadingScreen(Modifier.systemBarsPadding())
|
||||
}
|
||||
}
|
||||
is MangaScreenModel.Dialog.SetMangaInterval -> {
|
||||
SetIntervalDialog(
|
||||
interval = if (dialog.manga.calculateInterval < 0) -dialog.manga.calculateInterval else 0,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onValueChanged = { screenModel.setFetchRangeInterval(dialog.manga, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,7 @@ import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
|||
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.items.chapter.interactor.SetMangaDefaultChapterFlags
|
||||
import tachiyomi.domain.items.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
|
@ -75,6 +76,7 @@ import tachiyomi.domain.track.manga.interactor.GetMangaTracks
|
|||
import tachiyomi.source.local.entries.manga.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class MangaScreenModel(
|
||||
val context: Context,
|
||||
|
@ -99,6 +101,7 @@ class MangaScreenModel(
|
|||
private val getCategories: GetMangaCategories = Injekt.get(),
|
||||
private val getTracks: GetMangaTracks = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
private val mangaRepository: MangaRepository = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
|
||||
|
||||
|
@ -125,10 +128,13 @@ class MangaScreenModel(
|
|||
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
|
||||
val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get()
|
||||
|
||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
||||
|
||||
val isIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()
|
||||
private val leadDay = libraryPreferences.leadingMangaExpectedDays().get()
|
||||
private val followDay = libraryPreferences.followingMangaExpectedDays().get()
|
||||
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||
private val selectedChapterIds: HashSet<Long> = HashSet()
|
||||
|
||||
|
@ -316,7 +322,7 @@ class MangaScreenModel(
|
|||
// Choose a category
|
||||
else -> {
|
||||
isFromChangeCategory = true
|
||||
promptChangeCategories()
|
||||
showChangeCategoryDialog()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -346,7 +352,7 @@ class MangaScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun promptChangeCategories() {
|
||||
fun showChangeCategoryDialog() {
|
||||
val manga = successState?.manga ?: return
|
||||
coroutineScope.launch {
|
||||
val categories = getCategories()
|
||||
|
@ -362,6 +368,37 @@ class MangaScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun showSetMangaIntervalDialog() {
|
||||
val manga = successState?.manga ?: return
|
||||
updateSuccessState {
|
||||
it.copy(dialog = Dialog.SetMangaInterval(manga))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this should be in the state/composables
|
||||
fun intervalDisplay(): Pair<Int, Int>? {
|
||||
val manga = successState?.manga ?: return null
|
||||
val effInterval = manga.calculateInterval
|
||||
return 1.coerceAtLeast(effInterval.absoluteValue - leadDay) to (effInterval.absoluteValue + followDay)
|
||||
}
|
||||
|
||||
fun setFetchRangeInterval(manga: Manga, newInterval: Int) {
|
||||
val interval = when (newInterval) {
|
||||
// reset interval 0 default to trigger recalculation
|
||||
// only reset if interval is custom, which is negative
|
||||
0 -> if (manga.calculateInterval < 0) 0 else manga.calculateInterval
|
||||
else -> -newInterval
|
||||
}
|
||||
coroutineScope.launchIO {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga.copy(calculateInterval = interval),
|
||||
successState?.chapters?.map { it.chapter }.orEmpty(),
|
||||
)
|
||||
val newManga = mangaRepository.getMangaById(mangaId)
|
||||
updateSuccessState { it.copy(manga = newManga) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the manga has any downloads.
|
||||
*/
|
||||
|
@ -515,6 +552,7 @@ class MangaScreenModel(
|
|||
chapters,
|
||||
state.manga,
|
||||
state.source,
|
||||
manualFetch,
|
||||
)
|
||||
|
||||
if (manualFetch) {
|
||||
|
@ -532,6 +570,8 @@ class MangaScreenModel(
|
|||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(message = message)
|
||||
}
|
||||
val newManga = mangaRepository.getMangaById(mangaId)
|
||||
updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -956,6 +996,7 @@ class MangaScreenModel(
|
|||
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||
data class SetMangaInterval(val manga: Manga) : Dialog
|
||||
data object SettingsSheet : Dialog
|
||||
data object TrackSheet : Dialog
|
||||
data object FullCover : Dialog
|
||||
|
@ -983,7 +1024,7 @@ class MangaScreenModel(
|
|||
|
||||
sealed interface State {
|
||||
@Immutable
|
||||
object Loading : State
|
||||
data object Loading : State
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
|
|
|
@ -92,11 +92,7 @@ fun EpisodeListDialog(
|
|||
val date = episode.date_upload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
relativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
Date(it).toRelativeString(context, dateFormat)
|
||||
} ?: ""
|
||||
|
||||
EpisodeListItem(
|
||||
|
|
|
@ -61,14 +61,12 @@ class AnimeUpdatesScreenModel(
|
|||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
) : StateScreenModel<AnimeUpdatesScreenModel.State>(State()) {
|
||||
|
||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||
val events: Flow<Event> = _events.receiveAsFlow()
|
||||
|
||||
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||
|
||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||
|
||||
|
@ -382,12 +380,12 @@ class AnimeUpdatesScreenModel(
|
|||
data class State(
|
||||
val isLoading: Boolean = true,
|
||||
val items: List<AnimeUpdatesItem> = emptyList(),
|
||||
val dialog: AnimeUpdatesScreenModel.Dialog? = null,
|
||||
val dialog: Dialog? = null,
|
||||
) {
|
||||
val selected = items.filter { it.selected }
|
||||
val selectionMode = selected.isNotEmpty()
|
||||
|
||||
fun getUiModel(context: Context, relativeTime: Int): List<AnimeUpdatesUiModel> {
|
||||
fun getUiModel(context: Context): List<AnimeUpdatesUiModel> {
|
||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
||||
|
||||
return items
|
||||
|
@ -397,11 +395,7 @@ class AnimeUpdatesScreenModel(
|
|||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
when {
|
||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||
val text = afterDate.toRelativeString(
|
||||
context = context,
|
||||
range = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
val text = afterDate.toRelativeString(context, dateFormat)
|
||||
AnimeUpdatesUiModel.Header(text)
|
||||
}
|
||||
// Return null to avoid adding a separator between two items.
|
||||
|
|
|
@ -59,7 +59,6 @@ fun Screen.animeUpdatesTab(
|
|||
snackbarHostState = screenModel.snackbarHostState,
|
||||
contentPadding = contentPadding,
|
||||
lastUpdated = screenModel.lastUpdated,
|
||||
relativeTime = screenModel.relativeTime,
|
||||
onClickCover = { item -> navigator.push(AnimeScreen(item.update.animeId)) },
|
||||
onSelectAll = screenModel::toggleAllSelection,
|
||||
onInvertSelection = screenModel::invertSelection,
|
||||
|
|
|
@ -59,14 +59,12 @@ class MangaUpdatesScreenModel(
|
|||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
) : StateScreenModel<MangaUpdatesScreenModel.State>(State()) {
|
||||
|
||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||
val events: Flow<Event> = _events.receiveAsFlow()
|
||||
|
||||
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
|
||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||
|
||||
// First and last selected index in list
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||
|
@ -370,12 +368,12 @@ class MangaUpdatesScreenModel(
|
|||
data class State(
|
||||
val isLoading: Boolean = true,
|
||||
val items: List<MangaUpdatesItem> = emptyList(),
|
||||
val dialog: MangaUpdatesScreenModel.Dialog? = null,
|
||||
val dialog: Dialog? = null,
|
||||
) {
|
||||
val selected = items.filter { it.selected }
|
||||
val selectionMode = selected.isNotEmpty()
|
||||
|
||||
fun getUiModel(context: Context, relativeTime: Int): List<MangaUpdatesUiModel> {
|
||||
fun getUiModel(context: Context): List<MangaUpdatesUiModel> {
|
||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
||||
|
||||
return items
|
||||
|
@ -385,11 +383,7 @@ class MangaUpdatesScreenModel(
|
|||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
when {
|
||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||
val text = afterDate.toRelativeString(
|
||||
context = context,
|
||||
range = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
val text = afterDate.toRelativeString(context, dateFormat)
|
||||
MangaUpdatesUiModel.Header(text)
|
||||
}
|
||||
// Return null to avoid adding a separator between two items.
|
||||
|
|
|
@ -46,7 +46,6 @@ fun Screen.mangaUpdatesTab(
|
|||
snackbarHostState = screenModel.snackbarHostState,
|
||||
contentPadding = contentPadding,
|
||||
lastUpdated = screenModel.lastUpdated,
|
||||
relativeTime = screenModel.relativeTime,
|
||||
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
||||
onSelectAll = screenModel::toggleAllSelection,
|
||||
onInvertSelection = screenModel::invertSelection,
|
||||
|
|
|
@ -114,19 +114,15 @@ private const val MILLISECONDS_IN_DAY = 86_400_000L
|
|||
|
||||
fun Date.toRelativeString(
|
||||
context: Context,
|
||||
range: Int = 7,
|
||||
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
|
||||
): String {
|
||||
if (range == 0) {
|
||||
return dateFormat.format(this)
|
||||
}
|
||||
val now = Date()
|
||||
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
|
||||
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
|
||||
return when {
|
||||
difference < 0 -> context.getString(R.string.recently)
|
||||
difference < MILLISECONDS_IN_DAY -> context.getString(R.string.relative_time_today)
|
||||
difference < MILLISECONDS_IN_DAY.times(range) -> context.resources.getQuantityString(
|
||||
difference < MILLISECONDS_IN_DAY.times(7) -> context.resources.getQuantityString(
|
||||
R.plurals.relative_time,
|
||||
days,
|
||||
days,
|
||||
|
|
|
@ -13,111 +13,115 @@ import kotlin.math.absoluteValue
|
|||
|
||||
const val MAX_GRACE_PERIOD = 28
|
||||
|
||||
fun updateIntervalMeta(
|
||||
anime: Anime,
|
||||
episodes: List<Episode>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
||||
): AnimeUpdate? {
|
||||
val currentFetchRange = if (setCurrentFetchRange.first == 0L && setCurrentFetchRange.second == 0L) {
|
||||
getCurrentFetchRange(ZonedDateTime.now())
|
||||
} else {
|
||||
setCurrentFetchRange
|
||||
}
|
||||
val interval = anime.calculateInterval.takeIf { it < 0 } ?: calculateInterval(episodes, zonedDateTime)
|
||||
val nextUpdate = calculateNextUpdate(anime, interval, zonedDateTime, currentFetchRange)
|
||||
class SetAnimeUpdateInterval(
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
) {
|
||||
|
||||
return if (anime.nextUpdate == nextUpdate && anime.calculateInterval == interval) {
|
||||
null
|
||||
} else { AnimeUpdate(id = anime.id, nextUpdate = nextUpdate, calculateInterval = interval) }
|
||||
}
|
||||
|
||||
fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
|
||||
val sortedEpisodes = episodes
|
||||
.sortedWith(compareByDescending<Episode> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||
.take(50)
|
||||
|
||||
val uploadDates = sortedEpisodes
|
||||
.filter { it.dateUpload > 0L }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
fun updateInterval(
|
||||
anime: Anime,
|
||||
episodes: List<Episode>,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
): AnimeUpdate? {
|
||||
val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||
getCurrentFetchRange(ZonedDateTime.now())
|
||||
} else {
|
||||
fetchRange
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedEpisodes
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
val interval = anime.calculateInterval.takeIf { it < 0 } ?: calculateInterval(episodes, zonedDateTime)
|
||||
val nextUpdate = calculateNextUpdate(anime, interval, zonedDateTime, currentFetchRange)
|
||||
|
||||
return if (anime.nextUpdate == nextUpdate && anime.calculateInterval == interval) {
|
||||
null
|
||||
} else {
|
||||
AnimeUpdate(id = anime.id, nextUpdate = nextUpdate, calculateInterval = interval)
|
||||
}
|
||||
.distinct()
|
||||
}
|
||||
|
||||
val newInterval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||
(uploadDelta).floorDiv(uploadPeriod).toInt()
|
||||
fun getCurrentFetchRange(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||
var followRange = 0
|
||||
var leadRange = 0
|
||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
|
||||
followRange = libraryPreferences.followingAnimeExpectedDays().get()
|
||||
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
|
||||
}
|
||||
// Enough fetch date from client
|
||||
fetchDates.size >= 3 -> {
|
||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||
(fetchDelta).floorDiv(uploadPeriod).toInt()
|
||||
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 {
|
||||
val sortedEpisodes = episodes
|
||||
.sortedWith(compareByDescending<Episode> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||
.take(50)
|
||||
|
||||
val uploadDates = sortedEpisodes
|
||||
.filter { it.dateUpload > 0L }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedEpisodes
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
|
||||
val interval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||
uploadDelta.floorDiv(uploadPeriod).toInt()
|
||||
}
|
||||
// Enough fetch date from client
|
||||
fetchDates.size >= 3 -> {
|
||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||
fetchDelta.floorDiv(uploadPeriod).toInt()
|
||||
}
|
||||
// Default to 7 days
|
||||
else -> 7
|
||||
}
|
||||
// Default to 7 days
|
||||
else -> 7
|
||||
// Min 1, max 28 days
|
||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
}
|
||||
// min 1, max 28 days
|
||||
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
anime: Anime,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
currentFetchRange: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (anime.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) ||
|
||||
anime.calculateInterval == 0
|
||||
) {
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||
} else {
|
||||
anime.nextUpdate
|
||||
private fun calculateNextUpdate(
|
||||
anime: Anime,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (
|
||||
anime.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
||||
anime.calculateInterval == 0
|
||||
) {
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||
} else {
|
||||
anime.nextUpdate
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||
if (delta >= maxValue) return maxValue
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||
if (delta >= maxValue) return maxValue
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentFetchRange(
|
||||
timeToCal: ZonedDateTime,
|
||||
): Pair<Long, Long> {
|
||||
val preferences: LibraryPreferences = Injekt.get()
|
||||
|
||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||
var followRange = 0
|
||||
var leadRange = 0
|
||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in preferences.libraryUpdateItemRestriction().get()) {
|
||||
followRange = preferences.followingAnimeExpectedDays().get()
|
||||
leadRange = preferences.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)
|
||||
}
|
||||
|
|
|
@ -13,111 +13,115 @@ import kotlin.math.absoluteValue
|
|||
|
||||
const val MAX_GRACE_PERIOD = 28
|
||||
|
||||
fun updateIntervalMeta(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
||||
): MangaUpdate? {
|
||||
val currentFetchRange = if (setCurrentFetchRange.first == 0L && setCurrentFetchRange.second == 0L) {
|
||||
getCurrentFetchRange(ZonedDateTime.now())
|
||||
} else {
|
||||
setCurrentFetchRange
|
||||
}
|
||||
val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
|
||||
class SetMangaUpdateInterval(
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
) {
|
||||
|
||||
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
|
||||
null
|
||||
} else { MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval) }
|
||||
}
|
||||
|
||||
fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||
val sortedChapters = chapters
|
||||
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||
.take(50)
|
||||
|
||||
val uploadDates = sortedChapters
|
||||
.filter { it.dateUpload > 0L }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
fun updateInterval(
|
||||
manga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
): MangaUpdate? {
|
||||
val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||
getCurrentFetchRange(ZonedDateTime.now())
|
||||
} else {
|
||||
fetchRange
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedChapters
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
|
||||
|
||||
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
|
||||
null
|
||||
} else {
|
||||
MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval)
|
||||
}
|
||||
.distinct()
|
||||
}
|
||||
|
||||
val newInterval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||
(uploadDelta).floorDiv(uploadPeriod).toInt()
|
||||
fun getCurrentFetchRange(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||
var followRange = 0
|
||||
var leadRange = 0
|
||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
|
||||
followRange = libraryPreferences.followingAnimeExpectedDays().get()
|
||||
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
|
||||
}
|
||||
// Enough fetch date from client
|
||||
fetchDates.size >= 3 -> {
|
||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||
(fetchDelta).floorDiv(uploadPeriod).toInt()
|
||||
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 {
|
||||
val sortedChapters = chapters
|
||||
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||
.take(50)
|
||||
|
||||
val uploadDates = sortedChapters
|
||||
.filter { it.dateUpload > 0L }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedChapters
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
|
||||
val interval = when {
|
||||
// Enough upload date from source
|
||||
uploadDates.size >= 3 -> {
|
||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||
uploadDelta.floorDiv(uploadPeriod).toInt()
|
||||
}
|
||||
// Enough fetch date from client
|
||||
fetchDates.size >= 3 -> {
|
||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||
fetchDelta.floorDiv(uploadPeriod).toInt()
|
||||
}
|
||||
// Default to 7 days
|
||||
else -> 7
|
||||
}
|
||||
// Default to 7 days
|
||||
else -> 7
|
||||
// Min 1, max 28 days
|
||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
}
|
||||
// min 1, max 28 days
|
||||
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
currentFetchRange: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (manga.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) ||
|
||||
manga.calculateInterval == 0
|
||||
) {
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||
} else {
|
||||
manga.nextUpdate
|
||||
private fun calculateNextUpdate(
|
||||
manga: Manga,
|
||||
interval: Int,
|
||||
zonedDateTime: ZonedDateTime,
|
||||
fetchRange: Pair<Long, Long>,
|
||||
): Long {
|
||||
return if (
|
||||
manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
||||
manga.calculateInterval == 0
|
||||
) {
|
||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||
} else {
|
||||
manga.nextUpdate
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||
if (delta >= maxValue) return maxValue
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||
if (delta >= maxValue) return maxValue
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
return if (cycle > doubleWhenOver) {
|
||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||
} else {
|
||||
delta
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentFetchRange(
|
||||
timeToCal: ZonedDateTime,
|
||||
): Pair<Long, Long> {
|
||||
val preferences: LibraryPreferences = Injekt.get()
|
||||
|
||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||
var followRange = 0
|
||||
var leadRange = 0
|
||||
if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in preferences.libraryUpdateItemRestriction().get()) {
|
||||
followRange = preferences.followingMangaExpectedDays().get()
|
||||
leadRange = preferences.leadingMangaExpectedDays().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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
package tachiyomi.domain.entries.anime.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class SetAnimeUpdateIntervalTest {
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private var episode = Episode.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
)
|
||||
|
||||
private val setAnimeUpdateInterval = SetAnimeUpdateInterval(mockk())
|
||||
|
||||
private fun episodeAddTime(episode: Episode, duration: Duration): Episode {
|
||||
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
||||
return episode.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
|
||||
// default 7 when less than 3 distinct day
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 1 episodes in 1 day`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..1).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 5 episodes in 1 day`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 7 episodes in 48 hours, 2 day`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..2).forEach {
|
||||
val duration = Duration.ofHours(24L)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(48L)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
// Default 1 if interval less than 1
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 episodes in 75 hours, 3 days`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(15L * it)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Normal interval calculation
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 episodes in 120 hours, 5 days`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(24L * it)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 2 when 5 episodes in 240 hours, 10 days`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(48L * it)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 2
|
||||
}
|
||||
|
||||
// If interval is decimal, floor to closest integer
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 episodes in 125 hours, 5 days`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 episodes in 215 hours, 5 days`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(43L * it)
|
||||
val newEpisode = episodeAddTime(episode, duration)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Use fetch time if upload time not available
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 episodes in 125 hours, 5 days of dateFetch`() {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
val newEpisode = episodeAddTime(episode, duration).copy(dateUpload = 0L)
|
||||
episodes.add(newEpisode)
|
||||
}
|
||||
setAnimeUpdateInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package tachiyomi.domain.entries.manga.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class SetMangaUpdateIntervalTest {
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private var chapter = Chapter.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
)
|
||||
|
||||
private val setMangaUpdateInterval = SetMangaUpdateInterval(mockk())
|
||||
|
||||
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
|
||||
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
||||
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
|
||||
// default 7 when less than 3 distinct day
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 1 chapters in 1 day`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..1).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(10)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..2).forEach {
|
||||
val duration = Duration.ofHours(24L)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(48L)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
// Default 1 if interval less than 1
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 75 hours, 3 days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(15L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Normal interval calculation
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(24L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(48L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||
}
|
||||
|
||||
// If interval is decimal, floor to closest integer
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(43L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Use fetch time if upload time not available
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days of dateFetch`() {
|
||||
val chapters = mutableListOf<Chapter>()
|
||||
(1..5).forEach {
|
||||
val duration = Duration.ofHours(25L * it)
|
||||
val newChapter = chapterAddTime(chapter, duration).copy(dateUpload = 0L)
|
||||
chapters.add(newChapter)
|
||||
}
|
||||
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
}
|
|
@ -204,10 +204,6 @@
|
|||
<string name="theme_matrix">Matrix</string>
|
||||
<string name="theme_tidalwave">Tidal Wave</string>
|
||||
<string name="pref_dark_theme_pure_black">Pure black dark mode</string>
|
||||
<string name="pref_category_timestamps">Timestamps</string>
|
||||
<string name="pref_relative_format">Relative timestamps</string>
|
||||
<string name="pref_relative_time_short">Short (Today, Yesterday)</string>
|
||||
<string name="pref_relative_time_long">Long (Short+, n days ago)</string>
|
||||
<string name="pref_date_format">Date format</string>
|
||||
|
||||
<string name="pref_manage_notifications">Manage notifications</string>
|
||||
|
@ -628,6 +624,10 @@
|
|||
<item quantity="one">1 day</item>
|
||||
<item quantity="other">%d days</item>
|
||||
</plurals>
|
||||
<plurals name="range_interval_day">
|
||||
<item quantity="one">%1$d - %2$d day</item>
|
||||
<item quantity="other">%1$d - %2$d days</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Item info -->
|
||||
<plurals name="missing_items">
|
||||
|
@ -669,8 +669,7 @@
|
|||
<string name="display_mode_chapter">Chapter %1$s</string>
|
||||
<string name="manga_display_interval_title">Estimate every</string>
|
||||
<string name="manga_display_modified_interval_title">Set to update every</string>
|
||||
<string name="manga_modify_interval_title">Modify interval</string>
|
||||
<string name="manga_modify_calculated_interval_title">Customize Interval</string>
|
||||
<string name="manga_modify_calculated_interval_title">Customize interval</string>
|
||||
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
||||
<string name="show_title">Source title</string>
|
||||
<string name="show_chapter_number">Chapter number</string>
|
||||
|
|
|
@ -78,6 +78,7 @@ fun AdaptiveSheet(
|
|||
val alpha by animateFloatAsState(
|
||||
targetValue = targetAlpha,
|
||||
animationSpec = sheetAnimationSpec,
|
||||
label = "alpha",
|
||||
)
|
||||
val internalOnDismissRequest: () -> Unit = {
|
||||
scope.launch {
|
||||
|
|
Loading…
Reference in a new issue