Also a little test to see how this whole github team works.

Last commit merged: fe90546821
This commit is contained in:
LuftVerbot 2023-11-18 00:21:17 +01:00
parent 4cbf9a813e
commit 1b6301cc95
37 changed files with 863 additions and 348 deletions

View file

@ -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()) }

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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)
},
)
}

View file

@ -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))
}
},
)
}

View file

@ -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 }

View file

@ -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) {

View file

@ -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 }

View file

@ -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) {

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -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)

View file

@ -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 {

View file

@ -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,

View file

@ -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,

View file

@ -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
}
}

View file

@ -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>) {

View file

@ -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() {

View file

@ -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() {

View file

@ -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 {

View file

@ -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(

View file

@ -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) },
)
}
}
}

View file

@ -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(

View file

@ -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(

View file

@ -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.

View file

@ -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,

View file

@ -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.

View file

@ -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,

View file

@ -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,

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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>

View file

@ -78,6 +78,7 @@ fun AdaptiveSheet(
val alpha by animateFloatAsState(
targetValue = targetAlpha,
animationSpec = sheetAnimationSpec,
label = "alpha",
)
val internalOnDismissRequest: () -> Unit = {
scope.launch {