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.NetworkToLocalAnime
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags 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.anime.repository.AnimeRepository
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga 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.NetworkToLocalManga
import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
import tachiyomi.domain.entries.manga.repository.MangaRepository import tachiyomi.domain.entries.manga.repository.MangaRepository
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
@ -182,10 +184,11 @@ class DomainModule : InjektModule {
addFactory { GetNextEpisodes(get(), get(), get()) } addFactory { GetNextEpisodes(get(), get(), get()) }
addFactory { ResetAnimeViewerFlags(get()) } addFactory { ResetAnimeViewerFlags(get()) }
addFactory { SetAnimeEpisodeFlags(get()) } addFactory { SetAnimeEpisodeFlags(get()) }
addFactory { SetAnimeUpdateInterval(get()) }
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) } addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
addFactory { SetAnimeViewerFlags(get()) } addFactory { SetAnimeViewerFlags(get()) }
addFactory { NetworkToLocalAnime(get()) } addFactory { NetworkToLocalAnime(get()) }
addFactory { UpdateAnime(get()) } addFactory { UpdateAnime(get(), get()) }
addFactory { SetAnimeCategories(get()) } addFactory { SetAnimeCategories(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
@ -197,6 +200,7 @@ class DomainModule : InjektModule {
addFactory { GetNextChapters(get(), get(), get()) } addFactory { GetNextChapters(get(), get(), get()) }
addFactory { ResetMangaViewerFlags(get()) } addFactory { ResetMangaViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) } addFactory { SetMangaChapterFlags(get()) }
addFactory { SetMangaUpdateInterval(get()) }
addFactory { addFactory {
SetMangaDefaultChapterFlags( SetMangaDefaultChapterFlags(
get(), get(),
@ -206,7 +210,7 @@ class DomainModule : InjektModule {
} }
addFactory { SetMangaViewerFlags(get()) } addFactory { SetMangaViewerFlags(get()) }
addFactory { NetworkToLocalManga(get()) } addFactory { NetworkToLocalManga(get()) }
addFactory { UpdateManga(get()) } addFactory { UpdateManga(get(), get()) }
addFactory { SetMangaCategories(get()) } addFactory { SetMangaCategories(get()) }
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), 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.domain.entries.anime.model.hasCustomCover
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import tachiyomi.domain.entries.anime.interactor.getCurrentFetchRange import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
import tachiyomi.domain.entries.anime.interactor.updateIntervalMeta
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.model.AnimeUpdate import tachiyomi.domain.entries.anime.model.AnimeUpdate
import tachiyomi.domain.entries.anime.repository.AnimeRepository import tachiyomi.domain.entries.anime.repository.AnimeRepository
@ -17,6 +16,7 @@ import java.util.Date
class UpdateAnime( class UpdateAnime(
private val animeRepository: AnimeRepository, private val animeRepository: AnimeRepository,
private val setAnimeUpdateInterval: SetAnimeUpdateInterval,
) { ) {
suspend fun await(animeUpdate: AnimeUpdate): Boolean { suspend fun await(animeUpdate: AnimeUpdate): Boolean {
@ -77,16 +77,15 @@ class UpdateAnime(
) )
} }
suspend fun awaitUpdateIntervalMeta( suspend fun awaitUpdateFetchInterval(
anime: Anime, anime: Anime,
episodes: List<Episode>, episodes: List<Episode>,
zonedDateTime: ZonedDateTime = ZonedDateTime.now(), zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime), fetchRange: Pair<Long, Long> = setAnimeUpdateInterval.getCurrentFetchRange(zonedDateTime),
): Boolean { ): Boolean {
val newMeta = updateIntervalMeta(anime, episodes, zonedDateTime, setCurrentFetchRange) val updateAnime = setAnimeUpdateInterval.updateInterval(anime, episodes, zonedDateTime, fetchRange)
return if (updateAnime != null) {
return if (newMeta != null) { animeRepository.updateAnime(updateAnime)
animeRepository.updateAnime(newMeta)
} else { } else {
true 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.domain.entries.manga.model.hasCustomCover
import eu.kanade.tachiyomi.data.cache.MangaCoverCache import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.entries.manga.interactor.getCurrentFetchRange import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
import tachiyomi.domain.entries.manga.interactor.updateIntervalMeta
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.entries.manga.model.MangaUpdate import tachiyomi.domain.entries.manga.model.MangaUpdate
import tachiyomi.domain.entries.manga.repository.MangaRepository import tachiyomi.domain.entries.manga.repository.MangaRepository
@ -17,6 +16,7 @@ import java.util.Date
class UpdateManga( class UpdateManga(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
private val setMangaUpdateInterval: SetMangaUpdateInterval,
) { ) {
suspend fun await(mangaUpdate: MangaUpdate): Boolean { suspend fun await(mangaUpdate: MangaUpdate): Boolean {
@ -77,16 +77,15 @@ class UpdateManga(
) )
} }
suspend fun awaitUpdateIntervalMeta( suspend fun awaitUpdateFetchInterval(
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
zonedDateTime: ZonedDateTime = ZonedDateTime.now(), zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime), fetchRange: Pair<Long, Long> = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime),
): Boolean { ): Boolean {
val newMeta = updateIntervalMeta(manga, chapters, zonedDateTime, setCurrentFetchRange) val updatedManga = setMangaUpdateInterval.updateInterval(manga, chapters, zonedDateTime, fetchRange)
return if (updatedManga != null) {
return if (newMeta != null) { mangaRepository.updateManga(updatedManga)
mangaRepository.updateManga(newMeta)
} else { } else {
true true
} }

View file

@ -23,6 +23,7 @@ import tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.lang.Long.max import java.lang.Long.max
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
import java.util.TreeSet import java.util.TreeSet
@ -48,6 +49,9 @@ class SyncChaptersWithSource(
rawSourceChapters: List<SChapter>, rawSourceChapters: List<SChapter>,
manga: Manga, manga: Manga,
source: MangaSource, source: MangaSource,
manualFetch: Boolean = false,
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
fetchRange: Pair<Long, Long> = Pair(0, 0),
): List<Chapter> { ): List<Chapter> {
if (rawSourceChapters.isEmpty() && !source.isLocal()) { if (rawSourceChapters.isEmpty() && !source.isLocal()) {
throw NoChaptersException() throw NoChaptersException()
@ -134,6 +138,14 @@ class SyncChaptersWithSource(
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || manga.calculateInterval == 0 || manga.nextUpdate < fetchRange.first) {
updateManga.awaitUpdateFetchInterval(
manga,
dbChapters,
zoneDateTime,
fetchRange,
)
}
return emptyList() return emptyList()
} }
@ -188,6 +200,8 @@ class SyncChaptersWithSource(
val chapterUpdates = toChange.map { it.toChapterUpdate() } val chapterUpdates = toChange.map { it.toChapterUpdate() }
updateChapter.awaitAll(chapterUpdates) updateChapter.awaitAll(chapterUpdates)
} }
val newChapters = chapterRepository.getChapterByMangaId(manga.id)
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
// Set this manga as updated since chapters were changed // Set this manga as updated since chapters were changed
// Note that last_update actually represents last time the chapter list changed at all // Note that last_update actually represents last time the chapter list changed at all

View file

@ -23,6 +23,7 @@ import tachiyomi.source.local.entries.anime.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.lang.Long.max import java.lang.Long.max
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
import java.util.TreeSet import java.util.TreeSet
@ -48,6 +49,9 @@ class SyncEpisodesWithSource(
rawSourceEpisodes: List<SEpisode>, rawSourceEpisodes: List<SEpisode>,
anime: Anime, anime: Anime,
source: AnimeSource, source: AnimeSource,
manualFetch: Boolean = false,
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
fetchRange: Pair<Long, Long> = Pair(0, 0),
): List<Episode> { ): List<Episode> {
if (rawSourceEpisodes.isEmpty() && !source.isLocal()) { if (rawSourceEpisodes.isEmpty() && !source.isLocal()) {
throw NoEpisodesException() throw NoEpisodesException()
@ -134,6 +138,14 @@ class SyncEpisodesWithSource(
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
if (manualFetch || anime.calculateInterval == 0 || anime.nextUpdate < fetchRange.first) {
updateAnime.awaitUpdateFetchInterval(
anime,
dbEpisodes,
zoneDateTime,
fetchRange,
)
}
return emptyList() return emptyList()
} }
@ -188,6 +200,8 @@ class SyncEpisodesWithSource(
val episodeUpdates = toChange.map { it.toEpisodeUpdate() } val episodeUpdates = toChange.map { it.toEpisodeUpdate() }
updateEpisode.awaitAll(episodeUpdates) updateEpisode.awaitAll(episodeUpdates)
} }
val newChapters = episodeRepository.getEpisodeByAnimeId(anime.id)
updateAnime.awaitUpdateFetchInterval(anime, newChapters, zoneDateTime, fetchRange)
// Set this anime as updated since episodes were changed // Set this anime as updated since episodes were changed
// Note that last_update actually represents last time the episode list changed at all // Note that last_update actually represents last time the episode list changed at all

View file

@ -13,18 +13,13 @@ import java.util.Date
fun RelativeDateHeader( fun RelativeDateHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
date: Date, date: Date,
relativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
) { ) {
val context = LocalContext.current val context = LocalContext.current
ListGroupHeader( ListGroupHeader(
modifier = modifier, modifier = modifier,
text = remember { text = remember {
date.toRelativeString( date.toRelativeString(context, dateFormat)
context,
relativeTime,
dateFormat,
)
}, },
) )
} }

View file

@ -1,11 +1,23 @@
package eu.kanade.presentation.entries 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.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable 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.res.stringResource
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import tachiyomi.domain.entries.anime.interactor.MAX_GRACE_PERIOD
import tachiyomi.presentation.core.components.WheelTextPicker
@Composable @Composable
fun DeleteItemsDialog( 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( fun AnimeScreen(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Int, intervalDisplay: () -> Pair<Int, Int>?,
dateFormat: DateFormat, dateFormat: DateFormat,
isTabletUi: Boolean, isTabletUi: Boolean,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
@ -127,6 +127,7 @@ fun AnimeScreen(
onShareClicked: (() -> Unit)?, onShareClicked: (() -> Unit)?,
onDownloadActionClicked: ((DownloadAction) -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
changeAnimeSkipIntro: (() -> Unit)?, changeAnimeSkipIntro: (() -> Unit)?,
@ -160,8 +161,8 @@ fun AnimeScreen(
AnimeScreenSmallImpl( AnimeScreenSmallImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
intervalDisplay = intervalDisplay,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
showNextEpisodeAirTime = showNextEpisodeAirTime, showNextEpisodeAirTime = showNextEpisodeAirTime,
@ -183,6 +184,7 @@ fun AnimeScreen(
onShareClicked = onShareClicked, onShareClicked = onShareClicked,
onDownloadActionClicked = onDownloadActionClicked, onDownloadActionClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked, onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditIntervalClicked,
onMigrateClicked = onMigrateClicked, onMigrateClicked = onMigrateClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro, changeAnimeSkipIntro = changeAnimeSkipIntro,
onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiBookmarkClicked = onMultiBookmarkClicked,
@ -199,12 +201,12 @@ fun AnimeScreen(
AnimeScreenLargeImpl( AnimeScreenLargeImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
showNextEpisodeAirTime = showNextEpisodeAirTime, showNextEpisodeAirTime = showNextEpisodeAirTime,
alwaysUseExternalPlayer = alwaysUseExternalPlayer, alwaysUseExternalPlayer = alwaysUseExternalPlayer,
dateFormat = dateFormat, dateFormat = dateFormat,
intervalDisplay = intervalDisplay,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
onEpisodeClicked = onEpisodeClicked, onEpisodeClicked = onEpisodeClicked,
onDownloadEpisode = onDownloadEpisode, onDownloadEpisode = onDownloadEpisode,
@ -222,6 +224,7 @@ fun AnimeScreen(
onShareClicked = onShareClicked, onShareClicked = onShareClicked,
onDownloadActionClicked = onDownloadActionClicked, onDownloadActionClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked, onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditIntervalClicked,
changeAnimeSkipIntro = changeAnimeSkipIntro, changeAnimeSkipIntro = changeAnimeSkipIntro,
onMigrateClicked = onMigrateClicked, onMigrateClicked = onMigrateClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiBookmarkClicked = onMultiBookmarkClicked,
@ -242,8 +245,8 @@ fun AnimeScreen(
private fun AnimeScreenSmallImpl( private fun AnimeScreenSmallImpl(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
intervalDisplay: () -> Pair<Int, Int>?,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
showNextEpisodeAirTime: Boolean, showNextEpisodeAirTime: Boolean,
@ -272,6 +275,7 @@ private fun AnimeScreenSmallImpl(
onShareClicked: (() -> Unit)?, onShareClicked: (() -> Unit)?,
onDownloadActionClicked: ((DownloadAction) -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
changeAnimeSkipIntro: (() -> Unit)?, changeAnimeSkipIntro: (() -> Unit)?,
onSettingsClicked: (() -> Unit)?, onSettingsClicked: (() -> Unit)?,
@ -312,9 +316,11 @@ private fun AnimeScreenSmallImpl(
} }
val animatedTitleAlpha by animateFloatAsState( val animatedTitleAlpha by animateFloatAsState(
if (firstVisibleItemIndex > 0) 1f else 0f, if (firstVisibleItemIndex > 0) 1f else 0f,
label = "titleAlpha",
) )
val animatedBgAlpha by animateFloatAsState( val animatedBgAlpha by animateFloatAsState(
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f, if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
label = "bgAlpha",
) )
EntryToolbar( EntryToolbar(
title = state.anime.title, title = state.anime.title,
@ -426,10 +432,13 @@ private fun AnimeScreenSmallImpl(
AnimeActionRow( AnimeActionRow(
favorite = state.anime.favorite, favorite = state.anime.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
intervalDisplay = intervalDisplay,
isUserIntervalMode = state.anime.calculateInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked, onWebViewClicked = onWebViewClicked,
onWebViewLongClicked = onWebViewLongClicked, onWebViewLongClicked = onWebViewLongClicked,
onTrackingClicked = onTrackingClicked, onTrackingClicked = onTrackingClicked,
onEditIntervalClicked = onEditIntervalClicked,
onEditCategory = onEditCategoryClicked, onEditCategory = onEditCategoryClicked,
) )
} }
@ -488,7 +497,6 @@ private fun AnimeScreenSmallImpl(
sharedEpisodeItems( sharedEpisodeItems(
anime = state.anime, anime = state.anime,
episodes = episodes, episodes = episodes,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
@ -508,8 +516,8 @@ private fun AnimeScreenSmallImpl(
fun AnimeScreenLargeImpl( fun AnimeScreenLargeImpl(
state: AnimeScreenModel.State.Success, state: AnimeScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
intervalDisplay: () -> Pair<Int, Int>?,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
showNextEpisodeAirTime: Boolean, showNextEpisodeAirTime: Boolean,
@ -538,6 +546,7 @@ fun AnimeScreenLargeImpl(
onShareClicked: (() -> Unit)?, onShareClicked: (() -> Unit)?,
onDownloadActionClicked: ((DownloadAction) -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
changeAnimeSkipIntro: (() -> Unit)?, changeAnimeSkipIntro: (() -> Unit)?,
onSettingsClicked: (() -> Unit)?, onSettingsClicked: (() -> Unit)?,
@ -676,10 +685,13 @@ fun AnimeScreenLargeImpl(
AnimeActionRow( AnimeActionRow(
favorite = state.anime.favorite, favorite = state.anime.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
intervalDisplay = intervalDisplay,
isUserIntervalMode = state.anime.calculateInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked, onWebViewClicked = onWebViewClicked,
onWebViewLongClicked = onWebViewLongClicked, onWebViewLongClicked = onWebViewLongClicked,
onTrackingClicked = onTrackingClicked, onTrackingClicked = onTrackingClicked,
onEditIntervalClicked = onEditIntervalClicked,
onEditCategory = onEditCategoryClicked, onEditCategory = onEditCategoryClicked,
) )
ExpandableAnimeDescription( ExpandableAnimeDescription(
@ -745,7 +757,6 @@ fun AnimeScreenLargeImpl(
sharedEpisodeItems( sharedEpisodeItems(
anime = state.anime, anime = state.anime,
episodes = episodes, episodes = episodes,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
episodeSwipeStartAction = episodeSwipeStartAction, episodeSwipeStartAction = episodeSwipeStartAction,
episodeSwipeEndAction = episodeSwipeEndAction, episodeSwipeEndAction = episodeSwipeEndAction,
@ -816,7 +827,6 @@ private fun SharedAnimeBottomActionMenu(
private fun LazyListScope.sharedEpisodeItems( private fun LazyListScope.sharedEpisodeItems(
anime: Anime, anime: Anime,
episodes: List<EpisodeItem>, episodes: List<EpisodeItem>,
dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction, episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
@ -845,11 +855,7 @@ private fun LazyListScope.sharedEpisodeItems(
date = episodeItem.episode.dateUpload date = episodeItem.episode.dateUpload
.takeIf { it > 0L } .takeIf { it > 0L }
?.let { ?.let {
Date(it).toRelativeString( Date(it).toRelativeString(context, dateFormat)
context,
dateRelativeTime,
dateFormat,
)
}, },
watchProgress = episodeItem.episode.lastSecondSeen watchProgress = episodeItem.episode.lastSecondSeen
.takeIf { !episodeItem.episode.seen && it > 0L } .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.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite 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.AttachMoney
import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Close
@ -166,14 +167,19 @@ fun AnimeActionRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
favorite: Boolean, favorite: Boolean,
trackingCount: Int, trackingCount: Int,
intervalDisplay: () -> Pair<Int, Int>?,
isUserIntervalMode: Boolean,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onEditCategory: (() -> 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)) { Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
AnimeActionButton( AnimeActionButton(
title = if (favorite) { title = if (favorite) {
stringResource(R.string.in_library) stringResource(R.string.in_library)
@ -185,6 +191,19 @@ fun AnimeActionRow(
onClick = onAddToLibraryClicked, onClick = onAddToLibraryClicked,
onLongClick = onEditCategory, 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) { if (onTrackingClicked != null) {
AnimeActionButton( AnimeActionButton(
title = if (trackingCount == 0) { title = if (trackingCount == 0) {

View file

@ -90,7 +90,7 @@ import java.util.Date
fun MangaScreen( fun MangaScreen(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Int, intervalDisplay: () -> Pair<Int, Int>?,
dateFormat: DateFormat, dateFormat: DateFormat,
isTabletUi: Boolean, isTabletUi: Boolean,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@ -118,6 +118,7 @@ fun MangaScreen(
onShareClicked: (() -> Unit)?, onShareClicked: (() -> Unit)?,
onDownloadActionClicked: ((DownloadAction) -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
// For bottom action menu // For bottom action menu
@ -150,8 +151,8 @@ fun MangaScreen(
MangaScreenSmallImpl( MangaScreenSmallImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
intervalDisplay = intervalDisplay,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
@ -171,6 +172,7 @@ fun MangaScreen(
onShareClicked = onShareClicked, onShareClicked = onShareClicked,
onDownloadActionClicked = onDownloadActionClicked, onDownloadActionClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked, onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditIntervalClicked,
onMigrateClicked = onMigrateClicked, onMigrateClicked = onMigrateClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
@ -186,10 +188,10 @@ fun MangaScreen(
MangaScreenLargeImpl( MangaScreenLargeImpl(
state = state, state = state,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
dateRelativeTime = dateRelativeTime,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
dateFormat = dateFormat, dateFormat = dateFormat,
intervalDisplay = intervalDisplay,
onBackClicked = onBackClicked, onBackClicked = onBackClicked,
onChapterClicked = onChapterClicked, onChapterClicked = onChapterClicked,
onDownloadChapter = onDownloadChapter, onDownloadChapter = onDownloadChapter,
@ -207,6 +209,7 @@ fun MangaScreen(
onShareClicked = onShareClicked, onShareClicked = onShareClicked,
onDownloadActionClicked = onDownloadActionClicked, onDownloadActionClicked = onDownloadActionClicked,
onEditCategoryClicked = onEditCategoryClicked, onEditCategoryClicked = onEditCategoryClicked,
onEditIntervalClicked = onEditIntervalClicked,
onMigrateClicked = onMigrateClicked, onMigrateClicked = onMigrateClicked,
onMultiBookmarkClicked = onMultiBookmarkClicked, onMultiBookmarkClicked = onMultiBookmarkClicked,
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked, onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
@ -225,8 +228,8 @@ fun MangaScreen(
private fun MangaScreenSmallImpl( private fun MangaScreenSmallImpl(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
intervalDisplay: () -> Pair<Int, Int>?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
@ -253,6 +256,7 @@ private fun MangaScreenSmallImpl(
onShareClicked: (() -> Unit)?, onShareClicked: (() -> Unit)?,
onDownloadActionClicked: ((DownloadAction) -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
onSettingsClicked: (() -> Unit)?, onSettingsClicked: (() -> Unit)?,
@ -293,9 +297,11 @@ private fun MangaScreenSmallImpl(
} }
val animatedTitleAlpha by animateFloatAsState( val animatedTitleAlpha by animateFloatAsState(
if (firstVisibleItemIndex > 0) 1f else 0f, if (firstVisibleItemIndex > 0) 1f else 0f,
label = "titleAlpha",
) )
val animatedBgAlpha by animateFloatAsState( val animatedBgAlpha by animateFloatAsState(
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f, if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
label = "bgAlpha",
) )
EntryToolbar( EntryToolbar(
title = state.manga.title, title = state.manga.title,
@ -400,10 +406,13 @@ private fun MangaScreenSmallImpl(
MangaActionRow( MangaActionRow(
favorite = state.manga.favorite, favorite = state.manga.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
intervalDisplay = intervalDisplay,
isUserIntervalMode = state.manga.calculateInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked, onWebViewClicked = onWebViewClicked,
onWebViewLongClicked = onWebViewLongClicked, onWebViewLongClicked = onWebViewLongClicked,
onTrackingClicked = onTrackingClicked, onTrackingClicked = onTrackingClicked,
onEditIntervalClicked = onEditIntervalClicked,
onEditCategory = onEditCategoryClicked, onEditCategory = onEditCategoryClicked,
) )
} }
@ -437,7 +446,6 @@ private fun MangaScreenSmallImpl(
sharedChapterItems( sharedChapterItems(
manga = state.manga, manga = state.manga,
chapters = chapters, chapters = chapters,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
@ -456,8 +464,8 @@ private fun MangaScreenSmallImpl(
fun MangaScreenLargeImpl( fun MangaScreenLargeImpl(
state: MangaScreenModel.State.Success, state: MangaScreenModel.State.Success,
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
intervalDisplay: () -> Pair<Int, Int>?,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
onBackClicked: () -> Unit, onBackClicked: () -> Unit,
@ -484,6 +492,7 @@ fun MangaScreenLargeImpl(
onShareClicked: (() -> Unit)?, onShareClicked: (() -> Unit)?,
onDownloadActionClicked: ((DownloadAction) -> Unit)?, onDownloadActionClicked: ((DownloadAction) -> Unit)?,
onEditCategoryClicked: (() -> Unit)?, onEditCategoryClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onMigrateClicked: (() -> Unit)?, onMigrateClicked: (() -> Unit)?,
onSettingsClicked: (() -> Unit)?, onSettingsClicked: (() -> Unit)?,
@ -618,10 +627,13 @@ fun MangaScreenLargeImpl(
MangaActionRow( MangaActionRow(
favorite = state.manga.favorite, favorite = state.manga.favorite,
trackingCount = state.trackingCount, trackingCount = state.trackingCount,
intervalDisplay = intervalDisplay,
isUserIntervalMode = state.manga.calculateInterval < 0,
onAddToLibraryClicked = onAddToLibraryClicked, onAddToLibraryClicked = onAddToLibraryClicked,
onWebViewClicked = onWebViewClicked, onWebViewClicked = onWebViewClicked,
onWebViewLongClicked = onWebViewLongClicked, onWebViewLongClicked = onWebViewLongClicked,
onTrackingClicked = onTrackingClicked, onTrackingClicked = onTrackingClicked,
onEditIntervalClicked = onEditIntervalClicked,
onEditCategory = onEditCategoryClicked, onEditCategory = onEditCategoryClicked,
) )
ExpandableMangaDescription( ExpandableMangaDescription(
@ -662,7 +674,6 @@ fun MangaScreenLargeImpl(
sharedChapterItems( sharedChapterItems(
manga = state.manga, manga = state.manga,
chapters = chapters, chapters = chapters,
dateRelativeTime = dateRelativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeStartAction = chapterSwipeStartAction,
chapterSwipeEndAction = chapterSwipeEndAction, chapterSwipeEndAction = chapterSwipeEndAction,
@ -725,7 +736,6 @@ private fun SharedMangaBottomActionMenu(
private fun LazyListScope.sharedChapterItems( private fun LazyListScope.sharedChapterItems(
manga: Manga, manga: Manga,
chapters: List<ChapterItem>, chapters: List<ChapterItem>,
dateRelativeTime: Int,
dateFormat: DateFormat, dateFormat: DateFormat,
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
@ -754,11 +764,7 @@ private fun LazyListScope.sharedChapterItems(
date = chapterItem.chapter.dateUpload date = chapterItem.chapter.dateUpload
.takeIf { it > 0L } .takeIf { it > 0L }
?.let { ?.let {
Date(it).toRelativeString( Date(it).toRelativeString(context, dateFormat)
context,
dateRelativeTime,
dateFormat,
)
}, },
readProgress = chapterItem.chapter.lastPageRead readProgress = chapterItem.chapter.lastPageRead
.takeIf { !chapterItem.chapter.read && it > 0L } .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.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite 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.filled.Warning
import androidx.compose.material.icons.outlined.AttachMoney import androidx.compose.material.icons.outlined.AttachMoney
import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.Block
@ -166,14 +167,19 @@ fun MangaActionRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
favorite: Boolean, favorite: Boolean,
trackingCount: Int, trackingCount: Int,
intervalDisplay: () -> Pair<Int, Int>?,
isUserIntervalMode: Boolean,
onAddToLibraryClicked: () -> Unit, onAddToLibraryClicked: () -> Unit,
onWebViewClicked: (() -> Unit)?, onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?, onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: (() -> Unit)?, onTrackingClicked: (() -> Unit)?,
onEditIntervalClicked: (() -> Unit)?,
onEditCategory: (() -> 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)) { Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
MangaActionButton( MangaActionButton(
title = if (favorite) { title = if (favorite) {
stringResource(R.string.in_library) stringResource(R.string.in_library)
@ -185,6 +191,19 @@ fun MangaActionRow(
onClick = onAddToLibraryClicked, onClick = onAddToLibraryClicked,
onLongClick = onEditCategory, 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) { if (onTrackingClicked != null) {
MangaActionButton( MangaActionButton(
title = if (trackingCount == 0) { title = if (trackingCount == 0) {

View file

@ -24,7 +24,6 @@ fun AnimeHistoryContent(
onClickDelete: (AnimeHistoryWithRelations) -> Unit, onClickDelete: (AnimeHistoryWithRelations) -> Unit,
preferences: UiPreferences = Injekt.get(), preferences: UiPreferences = Injekt.get(),
) { ) {
val relativeTime: Int = remember { preferences.relativeTime().get() }
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn( FastScrollLazyColumn(
@ -45,7 +44,6 @@ fun AnimeHistoryContent(
RelativeDateHeader( RelativeDateHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
date = item.date, date = item.date,
relativeTime = relativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
) )
} }

View file

@ -22,7 +22,6 @@ fun MangaHistoryContent(
onClickDelete: (MangaHistoryWithRelations) -> Unit, onClickDelete: (MangaHistoryWithRelations) -> Unit,
preferences: UiPreferences = Injekt.get(), preferences: UiPreferences = Injekt.get(),
) { ) {
val relativeTime: Int = remember { preferences.relativeTime().get() }
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) } val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
FastScrollLazyColumn( FastScrollLazyColumn(
@ -43,7 +42,6 @@ fun MangaHistoryContent(
RelativeDateHeader( RelativeDateHeader(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
date = item.date, date = item.date,
relativeTime = relativeTime,
dateFormat = dateFormat, dateFormat = dateFormat,
) )
} }

View file

@ -51,7 +51,6 @@ object SettingsAppearanceScreen : SearchableSettings {
return listOf( return listOf(
getThemeGroup(context = context, uiPreferences = uiPreferences), getThemeGroup(context = context, uiPreferences = uiPreferences),
getDisplayGroup(context = context, uiPreferences = uiPreferences), getDisplayGroup(context = context, uiPreferences = uiPreferences),
getTimestampGroup(uiPreferences = uiPreferences),
) )
} }
@ -124,6 +123,7 @@ object SettingsAppearanceScreen : SearchableSettings {
): Preference.PreferenceGroup { ): Preference.PreferenceGroup {
val langs = remember { getLangs(context) } val langs = remember { getLangs(context) }
var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") } var currentLanguage by remember { mutableStateOf(AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: "") }
val now = remember { Date().time }
LaunchedEffect(currentLanguage) { LaunchedEffect(currentLanguage) {
val locale = if (currentLanguage.isEmpty()) { val locale = if (currentLanguage.isEmpty()) {
@ -186,25 +186,6 @@ object SettingsAppearanceScreen : SearchableSettings {
true 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( Preference.PreferenceItem.ListPreference(
pref = uiPreferences.dateFormat(), pref = uiPreferences.dateFormat(),
title = stringResource(R.string.pref_date_format), title = stringResource(R.string.pref_date_format),
@ -217,7 +198,6 @@ object SettingsAppearanceScreen : SearchableSettings {
), ),
) )
} }
private fun getLangs(context: Context): Map<String, String> { private fun getLangs(context: Context): Map<String, String> {
val langs = mutableListOf<Pair<String, String>>() val langs = mutableListOf<Pair<String, String>>()
val parser = context.resources.getXml(R.xml.locales_config) val parser = context.resources.getXml(R.xml.locales_config)

View file

@ -292,20 +292,17 @@ object SettingsLibraryScreen : SearchableSettings {
true true
}, },
), ),
// TODO: remove isDevFlavor checks once functionality is available
Preference.PreferenceItem.MultiSelectListPreference( Preference.PreferenceItem.MultiSelectListPreference(
pref = libraryUpdateDeviceRestrictionPref, pref = libraryUpdateDeviceRestrictionPref,
enabled = libraryUpdateInterval > 0, enabled = libraryUpdateInterval > 0,
title = stringResource(R.string.pref_library_update_restriction), title = stringResource(R.string.pref_library_update_restriction),
subtitle = stringResource(R.string.restrictions), subtitle = stringResource(R.string.restrictions),
entries = buildMap { entries = mapOf(
put(ENTRY_HAS_UNVIEWED, stringResource(R.string.pref_update_only_completely_read)) ENTRY_HAS_UNVIEWED to stringResource(R.string.pref_update_only_completely_read),
put(ENTRY_NON_VIEWED, stringResource(R.string.pref_update_only_started)) ENTRY_NON_VIEWED to stringResource(R.string.pref_update_only_started),
put(ENTRY_NON_COMPLETED, stringResource(R.string.pref_update_only_non_completed)) ENTRY_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
if (isDevFlavor) { ENTRY_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
put(ENTRY_OUTSIDE_RELEASE_PERIOD, stringResource(R.string.pref_update_only_in_release_period)) ),
}
},
onValueChanged = { onValueChanged = {
// Post to event looper to allow the preference to be updated. // Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { ContextCompat.getMainExecutor(context).execute {

View file

@ -40,7 +40,6 @@ fun AnimeUpdateScreen(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
lastUpdated: Long, lastUpdated: Long,
relativeTime: Int,
onClickCover: (AnimeUpdatesItem) -> Unit, onClickCover: (AnimeUpdatesItem) -> Unit,
onSelectAll: (Boolean) -> Unit, onSelectAll: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
@ -101,7 +100,7 @@ fun AnimeUpdateScreen(
animeUpdatesLastUpdatedItem(lastUpdated) animeUpdatesLastUpdatedItem(lastUpdated)
} }
animeUpdatesUiItems( animeUpdatesUiItems(
uiModels = state.getUiModel(context, relativeTime), uiModels = state.getUiModel(context),
selectionMode = state.selectionMode, selectionMode = state.selectionMode,
onUpdateSelected = onUpdateSelected, onUpdateSelected = onUpdateSelected,
onClickCover = onClickCover, onClickCover = onClickCover,

View file

@ -37,7 +37,6 @@ fun MangaUpdateScreen(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
contentPadding: PaddingValues, contentPadding: PaddingValues,
lastUpdated: Long, lastUpdated: Long,
relativeTime: Int,
onClickCover: (MangaUpdatesItem) -> Unit, onClickCover: (MangaUpdatesItem) -> Unit,
onSelectAll: (Boolean) -> Unit, onSelectAll: (Boolean) -> Unit,
onInvertSelection: () -> Unit, onInvertSelection: () -> Unit,
@ -98,7 +97,7 @@ fun MangaUpdateScreen(
} }
mangaUpdatesUiItems( mangaUpdatesUiItems(
uiModels = state.getUiModel(context, relativeTime), uiModels = state.getUiModel(context),
selectionMode = state.selectionMode, selectionMode = state.selectionMode,
onUpdateSelected = onUpdateSelected, onUpdateSelected = onUpdateSelected,
onClickCover = onClickCover, onClickCover = onClickCover,

View file

@ -72,7 +72,7 @@ fun WebViewScreenContent(
super.onPageFinished(view, url) super.onPageFinished(view, url)
scope.launch { scope.launch {
val html = view.getHtml() 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.content.SharedPreferences
import android.net.Uri import android.net.Uri
import androidx.preference.PreferenceManager 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.R
import eu.kanade.tachiyomi.data.backup.models.BackupAnime import eu.kanade.tachiyomi.data.backup.models.BackupAnime
import eu.kanade.tachiyomi.data.backup.models.BackupAnimeHistory 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.coroutineScope
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.anime.interactor.SetAnimeUpdateInterval
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.manga.interactor.SetMangaUpdateInterval
import tachiyomi.domain.entries.manga.model.Manga import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.domain.items.chapter.repository.ChapterRepository
import tachiyomi.domain.items.episode.model.Episode 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.anime.model.AnimeTrack
import tachiyomi.domain.track.manga.model.MangaTrack import tachiyomi.domain.track.manga.model.MangaTrack
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -43,6 +52,17 @@ class BackupRestorer(
private val context: Context, private val context: Context,
private val notifier: BackupNotifier, 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) private var backupManager = BackupManager(context)
@ -120,6 +140,10 @@ class BackupRestorer(
val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources val backupAnimeMaps = backup.backupBrokenAnimeSources.map { BackupAnimeSource(it.name, it.sourceId) } + backup.backupAnimeSources
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name } animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
zonedDateTime = ZonedDateTime.now()
currentMangaRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
currentAnimeRange = setAnimeUpdateInterval.getCurrentFetchRange(zonedDateTime)
return coroutineScope { return coroutineScope {
// Restore individual manga // Restore individual manga
backup.backupManga.forEach { backup.backupManga.forEach {
@ -182,7 +206,7 @@ class BackupRestorer(
try { try {
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source) val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
if (dbManga == null) { val restoredManga = if (dbManga == null) {
// Manga not in database // Manga not in database
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories) restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
} else { } else {
@ -192,6 +216,8 @@ class BackupRestorer(
// Fetch rest of manga information // Fetch rest of manga information
restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories) restoreNewManga(updateManga, chapters, categories, history, tracks, backupCategories)
} }
val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id)
updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentMangaRange)
} catch (e: Exception) { } catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString() val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
@ -219,10 +245,11 @@ class BackupRestorer(
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<MangaTrack>, tracks: List<MangaTrack>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
) { ): Manga {
val fetchedManga = backupManager.restoreNewManga(manga) val fetchedManga = backupManager.restoreNewManga(manga)
backupManager.restoreChapters(fetchedManga, chapters) backupManager.restoreChapters(fetchedManga, chapters)
restoreExtras(fetchedManga, categories, history, tracks, backupCategories) restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
return fetchedManga
} }
private suspend fun restoreNewManga( private suspend fun restoreNewManga(
@ -232,9 +259,10 @@ class BackupRestorer(
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<MangaTrack>, tracks: List<MangaTrack>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
) { ): Manga {
backupManager.restoreChapters(backupManga, chapters) backupManager.restoreChapters(backupManga, chapters)
restoreExtras(backupManga, categories, history, tracks, backupCategories) 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>) { 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 { try {
val dbAnime = backupManager.getAnimeFromDatabase(anime.url, anime.source) val dbAnime = backupManager.getAnimeFromDatabase(anime.url, anime.source)
if (dbAnime == null) { val restoredAnime = if (dbAnime == null) {
// Anime not in database // Anime not in database
restoreExistingAnime(anime, episodes, categories, history, tracks, backupCategories) restoreExistingAnime(anime, episodes, categories, history, tracks, backupCategories)
} else { } else {
@ -263,6 +291,8 @@ class BackupRestorer(
// Fetch rest of anime information // Fetch rest of anime information
restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories) restoreNewAnime(updateAnime, episodes, categories, history, tracks, backupCategories)
} }
val updatedEpisodes = episodeRepository.getEpisodeByAnimeId(restoredAnime.id)
updateAnime.awaitUpdateFetchInterval(restoredAnime, updatedEpisodes, zonedDateTime, currentAnimeRange)
} catch (e: Exception) { } catch (e: Exception) {
val sourceName = sourceMapping[anime.source] ?: anime.source.toString() val sourceName = sourceMapping[anime.source] ?: anime.source.toString()
errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}") errors.add(Date() to "${anime.title} [$sourceName]: ${e.message}")
@ -290,10 +320,11 @@ class BackupRestorer(
history: List<BackupAnimeHistory>, history: List<BackupAnimeHistory>,
tracks: List<AnimeTrack>, tracks: List<AnimeTrack>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
) { ): Anime {
val fetchedAnime = backupManager.restoreNewAnime(anime) val fetchedAnime = backupManager.restoreNewAnime(anime)
backupManager.restoreEpisodes(fetchedAnime, episodes) backupManager.restoreEpisodes(fetchedAnime, episodes)
restoreExtras(fetchedAnime, categories, history, tracks, backupCategories) restoreExtras(fetchedAnime, categories, history, tracks, backupCategories)
return fetchedAnime
} }
private suspend fun restoreNewAnime( private suspend fun restoreNewAnime(
@ -303,9 +334,10 @@ class BackupRestorer(
history: List<BackupAnimeHistory>, history: List<BackupAnimeHistory>,
tracks: List<AnimeTrack>, tracks: List<AnimeTrack>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
) { ): Anime {
backupManager.restoreEpisodes(backupAnime, episodes) backupManager.restoreEpisodes(backupAnime, episodes)
restoreExtras(backupAnime, categories, history, tracks, backupCategories) 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>) { 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.download.service.DownloadPreferences
import tachiyomi.domain.entries.anime.interactor.GetAnime import tachiyomi.domain.entries.anime.interactor.GetAnime
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime 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.Anime
import tachiyomi.domain.entries.anime.model.toAnimeUpdate import tachiyomi.domain.entries.anime.model.toAnimeUpdate
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId 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_HAS_UNVIEWED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
import tachiyomi.domain.source.anime.model.AnimeSourceNotInstalledException import tachiyomi.domain.source.anime.model.AnimeSourceNotInstalledException
import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.track.anime.interactor.GetAnimeTracks 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit 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 getTracks: GetAnimeTracks = Injekt.get()
private val insertTrack: InsertAnimeTrack = Injekt.get() private val insertTrack: InsertAnimeTrack = Injekt.get()
private val syncEpisodesWithTrackServiceTwoWay: SyncEpisodesWithTrackServiceTwoWay = Injekt.get() private val syncEpisodesWithTrackServiceTwoWay: SyncEpisodesWithTrackServiceTwoWay = Injekt.get()
private val setAnimeUpdateInterval: SetAnimeUpdateInterval = Injekt.get()
private val notifier = AnimeLibraryUpdateNotifier(context) private val notifier = AnimeLibraryUpdateNotifier(context)
@ -227,6 +231,10 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
val hasDownloads = AtomicBoolean(false) val hasDownloads = AtomicBoolean(false)
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get() val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
val now = ZonedDateTime.now()
val fetchRange = setAnimeUpdateInterval.getCurrentFetchRange(now)
val higherLimit = fetchRange.second
coroutineScope { coroutineScope {
animeToUpdate.groupBy { it.anime.source }.values animeToUpdate.groupBy { it.anime.source }.values
.map { animeInSource -> .map { animeInSource ->
@ -247,6 +255,9 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
anime, anime,
) { ) {
when { 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 -> ENTRY_NON_COMPLETED in restrictions && anime.status.toInt() == SAnime.COMPLETED ->
skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed)) skippedUpdates.add(anime to context.getString(R.string.skipped_reason_completed))
@ -261,7 +272,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
else -> { else -> {
try { try {
val newEpisodes = updateAnime(anime) val newEpisodes = updateAnime(anime, now, fetchRange)
.sortedByDescending { it.sourceOrder } .sortedByDescending { it.sourceOrder }
if (newEpisodes.isNotEmpty()) { if (newEpisodes.isNotEmpty()) {
@ -333,7 +344,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
* @param anime the anime to update. * @param anime the anime to update.
* @return a pair of the inserted and removed episodes. * @return a pair of the inserted and removed episodes.
*/ */
private suspend fun updateAnime(anime: Anime): List<Episode> { private suspend fun updateAnime(anime: Anime, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Episode> {
val source = sourceManager.getOrStub(anime.source) val source = sourceManager.getOrStub(anime.source)
// Update anime metadata if needed // Update anime metadata if needed
@ -348,7 +359,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
// to get latest data so it doesn't get overwritten later on // to get latest data so it doesn't get overwritten later on
val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList() val dbAnime = getAnime.await(anime.id)?.takeIf { it.favorite } ?: return emptyList()
return syncEpisodesWithSource.await(episodes, dbAnime, source) return syncEpisodesWithSource.await(episodes, dbAnime, source, false, zoneDateTime, fetchRange)
} }
private suspend fun updateCovers() { 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.download.service.DownloadPreferences
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
import tachiyomi.domain.entries.manga.interactor.GetManga 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.Manga
import tachiyomi.domain.entries.manga.model.toMangaUpdate import tachiyomi.domain.entries.manga.model.toMangaUpdate
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId 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_HAS_UNVIEWED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_COMPLETED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_NON_VIEWED
import tachiyomi.domain.library.service.LibraryPreferences.Companion.ENTRY_OUTSIDE_RELEASE_PERIOD
import tachiyomi.domain.source.manga.model.SourceNotInstalledException import tachiyomi.domain.source.manga.model.SourceNotInstalledException
import tachiyomi.domain.source.manga.service.MangaSourceManager import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.domain.track.manga.interactor.GetMangaTracks 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import java.time.ZonedDateTime
import java.util.Date import java.util.Date
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit 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 getTracks: GetMangaTracks = Injekt.get()
private val insertTrack: InsertMangaTrack = Injekt.get() private val insertTrack: InsertMangaTrack = Injekt.get()
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get() private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
private val notifier = MangaLibraryUpdateNotifier(context) private val notifier = MangaLibraryUpdateNotifier(context)
@ -227,6 +231,10 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
val hasDownloads = AtomicBoolean(false) val hasDownloads = AtomicBoolean(false)
val restrictions = libraryPreferences.libraryUpdateItemRestriction().get() val restrictions = libraryPreferences.libraryUpdateItemRestriction().get()
val now = ZonedDateTime.now()
val fetchRange = setMangaUpdateInterval.getCurrentFetchRange(now)
val higherLimit = fetchRange.second
coroutineScope { coroutineScope {
mangaToUpdate.groupBy { it.manga.source }.values mangaToUpdate.groupBy { it.manga.source }.values
.map { mangaInSource -> .map { mangaInSource ->
@ -247,6 +255,9 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
manga, manga,
) { ) {
when { 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 -> ENTRY_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed)) skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
@ -261,7 +272,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
else -> { else -> {
try { try {
val newChapters = updateManga(manga) val newChapters = updateManga(manga, now, fetchRange)
.sortedByDescending { it.sourceOrder } .sortedByDescending { it.sourceOrder }
if (newChapters.isNotEmpty()) { if (newChapters.isNotEmpty()) {
@ -333,7 +344,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
* @param manga the manga to update. * @param manga the manga to update.
* @return a pair of the inserted and removed chapters. * @return a pair of the inserted and removed chapters.
*/ */
private suspend fun updateManga(manga: Manga): List<Chapter> { private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
val source = sourceManager.getOrStub(manga.source) val source = sourceManager.getOrStub(manga.source)
// Update manga metadata if needed // Update manga metadata if needed
@ -348,7 +359,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
// to get latest data so it doesn't get overwritten later on // to get latest data so it doesn't get overwritten later on
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList() val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
return syncChaptersWithSource.await(chapters, dbManga, source) return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
} }
private suspend fun updateCovers() { 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.components.NavigatorAdaptiveSheet
import eu.kanade.presentation.entries.DeleteItemsDialog import eu.kanade.presentation.entries.DeleteItemsDialog
import eu.kanade.presentation.entries.EditCoverAction 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.AnimeScreen
import eu.kanade.presentation.entries.anime.DuplicateAnimeDialog import eu.kanade.presentation.entries.anime.DuplicateAnimeDialog
import eu.kanade.presentation.entries.anime.EpisodeOptionsDialogScreen import eu.kanade.presentation.entries.anime.EpisodeOptionsDialogScreen
@ -104,8 +105,8 @@ class AnimeScreen(
AnimeScreen( AnimeScreen(
state = successState, state = successState,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
dateRelativeTime = screenModel.relativeTime,
dateFormat = screenModel.dateFormat, dateFormat = screenModel.dateFormat,
intervalDisplay = screenModel::intervalDisplay,
isTabletUi = isTabletUi(), isTabletUi = isTabletUi(),
episodeSwipeStartAction = screenModel.episodeSwipeStartAction, episodeSwipeStartAction = screenModel.episodeSwipeStartAction,
episodeSwipeEndAction = screenModel.episodeSwipeEndAction, episodeSwipeEndAction = screenModel.episodeSwipeEndAction,
@ -139,7 +140,8 @@ class AnimeScreen(
onCoverClicked = screenModel::showCoverDialog, onCoverClicked = screenModel::showCoverDialog,
onShareClicked = { shareAnime(context, screenModel.anime, screenModel.source) }.takeIf { isAnimeHttpSource }, onShareClicked = { shareAnime(context, screenModel.anime, screenModel.source) }.takeIf { isAnimeHttpSource },
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() }, 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 }, onMigrateClicked = { navigator.push(MigrateAnimeSearchScreen(successState.anime.id)) }.takeIf { successState.anime.favorite },
changeAnimeSkipIntro = screenModel::showAnimeSkipIntroDialog.takeIf { successState.anime.favorite }, changeAnimeSkipIntro = screenModel::showAnimeSkipIntroDialog.takeIf { successState.anime.favorite },
onMultiBookmarkClicked = screenModel::bookmarkEpisodes, onMultiBookmarkClicked = screenModel::bookmarkEpisodes,
@ -233,6 +235,13 @@ class AnimeScreen(
LoadingScreen(Modifier.systemBarsPadding()) 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 -> { AnimeScreenModel.Dialog.ChangeAnimeSkipIntro -> {
fun updateSkipIntroLength(newLength: Long) { fun updateSkipIntroLength(newLength: Long) {
scope.launchIO { scope.launchIO {

View file

@ -8,7 +8,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove import eu.kanade.core.util.addOrRemove
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
import eu.kanade.domain.entries.anime.interactor.UpdateAnime 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.GetDuplicateLibraryAnime
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
import tachiyomi.domain.entries.anime.model.Anime import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.entries.anime.repository.AnimeRepository
import tachiyomi.domain.entries.applyFilter import tachiyomi.domain.entries.applyFilter
import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
import tachiyomi.domain.items.episode.interactor.UpdateEpisode 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Calendar import java.util.Calendar
import kotlin.math.absoluteValue
class AnimeScreenModel( class AnimeScreenModel(
val context: Context, val context: Context,
@ -103,6 +104,7 @@ class AnimeScreenModel(
private val getCategories: GetAnimeCategories = Injekt.get(), private val getCategories: GetAnimeCategories = Injekt.get(),
private val getTracks: GetAnimeTracks = Injekt.get(), private val getTracks: GetAnimeTracks = Injekt.get(),
private val setAnimeCategories: SetAnimeCategories = Injekt.get(), private val setAnimeCategories: SetAnimeCategories = Injekt.get(),
private val animeRepository: AnimeRepository = Injekt.get(),
internal val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(), internal val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<AnimeScreenModel.State>(State.Loading) { ) : StateScreenModel<AnimeScreenModel.State>(State.Loading) {
@ -131,9 +133,12 @@ class AnimeScreenModel(
val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get() val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get()
val useExternalDownloader = downloadPreferences.useExternalDownloader().get() val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) 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 selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
private val selectedEpisodeIds: HashSet<Long> = HashSet() private val selectedEpisodeIds: HashSet<Long> = HashSet()
@ -320,7 +325,7 @@ class AnimeScreenModel(
// Choose a category // Choose a category
else -> { else -> {
isFromChangeCategory = true isFromChangeCategory = true
promptChangeCategories() showChangeCategoryDialog()
} }
} }
@ -350,7 +355,7 @@ class AnimeScreenModel(
} }
} }
fun promptChangeCategories() { fun showChangeCategoryDialog() {
val anime = successState?.anime ?: return val anime = successState?.anime ?: return
coroutineScope.launch { coroutineScope.launch {
val categories = getCategories() 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. * Returns true if the anime has any downloads.
*/ */
@ -519,6 +555,7 @@ class AnimeScreenModel(
episodes, episodes,
state.anime, state.anime,
state.source, state.source,
manualFetch,
) )
if (manualFetch) { if (manualFetch) {
@ -536,6 +573,8 @@ class AnimeScreenModel(
coroutineScope.launch { coroutineScope.launch {
snackbarHostState.showSnackbar(message = message) 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 ChangeCategory(val anime: Anime, val initialSelection: List<CheckboxState<Category>>) : Dialog
data class DeleteEpisodes(val episodes: List<Episode>) : Dialog data class DeleteEpisodes(val episodes: List<Episode>) : Dialog
data class DuplicateAnime(val anime: Anime, val duplicate: Anime) : 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 class ShowQualities(val episode: Episode, val anime: Anime, val source: AnimeSource) : Dialog
data object ChangeAnimeSkipIntro : Dialog data object ChangeAnimeSkipIntro : Dialog
data object SettingsSheet : Dialog data object SettingsSheet : Dialog
@ -1009,7 +1049,7 @@ class AnimeScreenModel(
sealed interface State { sealed interface State {
@Immutable @Immutable
object Loading : State data object Loading : State
@Immutable @Immutable
data class Success( 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.components.NavigatorAdaptiveSheet
import eu.kanade.presentation.entries.DeleteItemsDialog import eu.kanade.presentation.entries.DeleteItemsDialog
import eu.kanade.presentation.entries.EditCoverAction 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.ChapterSettingsDialog
import eu.kanade.presentation.entries.manga.DuplicateMangaDialog import eu.kanade.presentation.entries.manga.DuplicateMangaDialog
import eu.kanade.presentation.entries.manga.MangaScreen import eu.kanade.presentation.entries.manga.MangaScreen
@ -99,8 +100,8 @@ class MangaScreen(
MangaScreen( MangaScreen(
state = successState, state = successState,
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
dateRelativeTime = screenModel.relativeTime,
dateFormat = screenModel.dateFormat, dateFormat = screenModel.dateFormat,
intervalDisplay = screenModel::intervalDisplay,
isTabletUi = isTabletUi(), isTabletUi = isTabletUi(),
chapterSwipeStartAction = screenModel.chapterSwipeStartAction, chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
chapterSwipeEndAction = screenModel.chapterSwipeEndAction, chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
@ -122,7 +123,8 @@ class MangaScreen(
onCoverClicked = screenModel::showCoverDialog, onCoverClicked = screenModel::showCoverDialog,
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource }, onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() }, 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 }, onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
onMultiBookmarkClicked = screenModel::bookmarkChapters, onMultiBookmarkClicked = screenModel::bookmarkChapters,
onMultiMarkAsReadClicked = screenModel::markChaptersRead, onMultiMarkAsReadClicked = screenModel::markChaptersRead,
@ -215,6 +217,13 @@ class MangaScreen(
LoadingScreen(Modifier.systemBarsPadding()) 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.GetMangaWithChapters
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
import tachiyomi.domain.entries.manga.model.Manga 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.SetMangaDefaultChapterFlags
import tachiyomi.domain.items.chapter.interactor.UpdateChapter import tachiyomi.domain.items.chapter.interactor.UpdateChapter
import tachiyomi.domain.items.chapter.model.Chapter 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 tachiyomi.source.local.entries.manga.isLocal
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import kotlin.math.absoluteValue
class MangaScreenModel( class MangaScreenModel(
val context: Context, val context: Context,
@ -99,6 +101,7 @@ class MangaScreenModel(
private val getCategories: GetMangaCategories = Injekt.get(), private val getCategories: GetMangaCategories = Injekt.get(),
private val getTracks: GetMangaTracks = Injekt.get(), private val getTracks: GetMangaTracks = Injekt.get(),
private val setMangaCategories: SetMangaCategories = Injekt.get(), private val setMangaCategories: SetMangaCategories = Injekt.get(),
private val mangaRepository: MangaRepository = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
) : StateScreenModel<MangaScreenModel.State>(State.Loading) { ) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
@ -125,10 +128,13 @@ class MangaScreenModel(
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get() val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get() val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get()
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get())) val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope) 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 selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
private val selectedChapterIds: HashSet<Long> = HashSet() private val selectedChapterIds: HashSet<Long> = HashSet()
@ -316,7 +322,7 @@ class MangaScreenModel(
// Choose a category // Choose a category
else -> { else -> {
isFromChangeCategory = true isFromChangeCategory = true
promptChangeCategories() showChangeCategoryDialog()
} }
} }
@ -346,7 +352,7 @@ class MangaScreenModel(
} }
} }
fun promptChangeCategories() { fun showChangeCategoryDialog() {
val manga = successState?.manga ?: return val manga = successState?.manga ?: return
coroutineScope.launch { coroutineScope.launch {
val categories = getCategories() 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. * Returns true if the manga has any downloads.
*/ */
@ -515,6 +552,7 @@ class MangaScreenModel(
chapters, chapters,
state.manga, state.manga,
state.source, state.source,
manualFetch,
) )
if (manualFetch) { if (manualFetch) {
@ -532,6 +570,8 @@ class MangaScreenModel(
coroutineScope.launch { coroutineScope.launch {
snackbarHostState.showSnackbar(message = message) 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 ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
data class DeleteChapters(val chapters: List<Chapter>) : Dialog data class DeleteChapters(val chapters: List<Chapter>) : Dialog
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
data class SetMangaInterval(val manga: Manga) : Dialog
data object SettingsSheet : Dialog data object SettingsSheet : Dialog
data object TrackSheet : Dialog data object TrackSheet : Dialog
data object FullCover : Dialog data object FullCover : Dialog
@ -983,7 +1024,7 @@ class MangaScreenModel(
sealed interface State { sealed interface State {
@Immutable @Immutable
object Loading : State data object Loading : State
@Immutable @Immutable
data class Success( data class Success(

View file

@ -92,11 +92,7 @@ fun EpisodeListDialog(
val date = episode.date_upload val date = episode.date_upload
.takeIf { it > 0L } .takeIf { it > 0L }
?.let { ?.let {
Date(it).toRelativeString( Date(it).toRelativeString(context, dateFormat)
context,
relativeTime,
dateFormat,
)
} ?: "" } ?: ""
EpisodeListItem( EpisodeListItem(

View file

@ -61,14 +61,12 @@ class AnimeUpdatesScreenModel(
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
downloadPreferences: DownloadPreferences = Injekt.get(), downloadPreferences: DownloadPreferences = Injekt.get(),
uiPreferences: UiPreferences = Injekt.get(),
) : StateScreenModel<AnimeUpdatesScreenModel.State>(State()) { ) : StateScreenModel<AnimeUpdatesScreenModel.State>(State()) {
private val _events: Channel<Event> = Channel(Int.MAX_VALUE) private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
val events: Flow<Event> = _events.receiveAsFlow() val events: Flow<Event> = _events.receiveAsFlow()
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope) val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
val useExternalDownloader = downloadPreferences.useExternalDownloader().get() val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
@ -382,12 +380,12 @@ class AnimeUpdatesScreenModel(
data class State( data class State(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val items: List<AnimeUpdatesItem> = emptyList(), val items: List<AnimeUpdatesItem> = emptyList(),
val dialog: AnimeUpdatesScreenModel.Dialog? = null, val dialog: Dialog? = null,
) { ) {
val selected = items.filter { it.selected } val selected = items.filter { it.selected }
val selectionMode = selected.isNotEmpty() 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())) val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
return items return items
@ -397,11 +395,7 @@ class AnimeUpdatesScreenModel(
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0) val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
when { when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> { beforeDate.time != afterDate.time && afterDate.time != 0L -> {
val text = afterDate.toRelativeString( val text = afterDate.toRelativeString(context, dateFormat)
context = context,
range = relativeTime,
dateFormat = dateFormat,
)
AnimeUpdatesUiModel.Header(text) AnimeUpdatesUiModel.Header(text)
} }
// Return null to avoid adding a separator between two items. // Return null to avoid adding a separator between two items.

View file

@ -59,7 +59,6 @@ fun Screen.animeUpdatesTab(
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
contentPadding = contentPadding, contentPadding = contentPadding,
lastUpdated = screenModel.lastUpdated, lastUpdated = screenModel.lastUpdated,
relativeTime = screenModel.relativeTime,
onClickCover = { item -> navigator.push(AnimeScreen(item.update.animeId)) }, onClickCover = { item -> navigator.push(AnimeScreen(item.update.animeId)) },
onSelectAll = screenModel::toggleAllSelection, onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection, onInvertSelection = screenModel::invertSelection,

View file

@ -59,14 +59,12 @@ class MangaUpdatesScreenModel(
private val getChapter: GetChapter = Injekt.get(), private val getChapter: GetChapter = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(), private val libraryPreferences: LibraryPreferences = Injekt.get(),
val snackbarHostState: SnackbarHostState = SnackbarHostState(), val snackbarHostState: SnackbarHostState = SnackbarHostState(),
uiPreferences: UiPreferences = Injekt.get(),
) : StateScreenModel<MangaUpdatesScreenModel.State>(State()) { ) : StateScreenModel<MangaUpdatesScreenModel.State>(State()) {
private val _events: Channel<Event> = Channel(Int.MAX_VALUE) private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
val events: Flow<Event> = _events.receiveAsFlow() val events: Flow<Event> = _events.receiveAsFlow()
val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope) val lastUpdated by libraryPreferences.libraryUpdateLastTimestamp().asState(coroutineScope)
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
// First and last selected index in list // First and last selected index in list
private val selectedPositions: Array<Int> = arrayOf(-1, -1) private val selectedPositions: Array<Int> = arrayOf(-1, -1)
@ -370,12 +368,12 @@ class MangaUpdatesScreenModel(
data class State( data class State(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val items: List<MangaUpdatesItem> = emptyList(), val items: List<MangaUpdatesItem> = emptyList(),
val dialog: MangaUpdatesScreenModel.Dialog? = null, val dialog: Dialog? = null,
) { ) {
val selected = items.filter { it.selected } val selected = items.filter { it.selected }
val selectionMode = selected.isNotEmpty() 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())) val dateFormat by mutableStateOf(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
return items return items
@ -385,11 +383,7 @@ class MangaUpdatesScreenModel(
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0) val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
when { when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> { beforeDate.time != afterDate.time && afterDate.time != 0L -> {
val text = afterDate.toRelativeString( val text = afterDate.toRelativeString(context, dateFormat)
context = context,
range = relativeTime,
dateFormat = dateFormat,
)
MangaUpdatesUiModel.Header(text) MangaUpdatesUiModel.Header(text)
} }
// Return null to avoid adding a separator between two items. // Return null to avoid adding a separator between two items.

View file

@ -46,7 +46,6 @@ fun Screen.mangaUpdatesTab(
snackbarHostState = screenModel.snackbarHostState, snackbarHostState = screenModel.snackbarHostState,
contentPadding = contentPadding, contentPadding = contentPadding,
lastUpdated = screenModel.lastUpdated, lastUpdated = screenModel.lastUpdated,
relativeTime = screenModel.relativeTime,
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) }, onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
onSelectAll = screenModel::toggleAllSelection, onSelectAll = screenModel::toggleAllSelection,
onInvertSelection = screenModel::invertSelection, onInvertSelection = screenModel::invertSelection,

View file

@ -114,19 +114,15 @@ private const val MILLISECONDS_IN_DAY = 86_400_000L
fun Date.toRelativeString( fun Date.toRelativeString(
context: Context, context: Context,
range: Int = 7,
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT), dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
): String { ): String {
if (range == 0) {
return dateFormat.format(this)
}
val now = Date() val now = Date()
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt() val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
return when { return when {
difference < 0 -> context.getString(R.string.recently) difference < 0 -> context.getString(R.string.recently)
difference < MILLISECONDS_IN_DAY -> context.getString(R.string.relative_time_today) 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, R.plurals.relative_time,
days, days,
days, days,

View file

@ -13,111 +13,115 @@ import kotlin.math.absoluteValue
const val MAX_GRACE_PERIOD = 28 const val MAX_GRACE_PERIOD = 28
fun updateIntervalMeta( class SetAnimeUpdateInterval(
anime: Anime, private val libraryPreferences: LibraryPreferences = Injekt.get(),
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)
return if (anime.nextUpdate == nextUpdate && anime.calculateInterval == interval) { fun updateInterval(
null anime: Anime,
} else { AnimeUpdate(id = anime.id, nextUpdate = nextUpdate, calculateInterval = interval) } episodes: List<Episode>,
} zonedDateTime: ZonedDateTime,
fetchRange: Pair<Long, Long>,
fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int { ): AnimeUpdate? {
val sortedEpisodes = episodes val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
.sortedWith(compareByDescending<Episode> { it.dateUpload }.thenByDescending { it.dateFetch }) getCurrentFetchRange(ZonedDateTime.now())
.take(50) } else {
fetchRange
val uploadDates = sortedEpisodes
.filter { it.dateUpload > 0L }
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
.toLocalDate()
.atStartOfDay()
} }
.distinct() val interval = anime.calculateInterval.takeIf { it < 0 } ?: calculateInterval(episodes, zonedDateTime)
val fetchDates = sortedEpisodes val nextUpdate = calculateNextUpdate(anime, interval, zonedDateTime, currentFetchRange)
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone) return if (anime.nextUpdate == nextUpdate && anime.calculateInterval == interval) {
.toLocalDate() null
.atStartOfDay() } else {
AnimeUpdate(id = anime.id, nextUpdate = nextUpdate, calculateInterval = interval)
} }
.distinct() }
val newInterval = when { fun getCurrentFetchRange(timeToCal: ZonedDateTime): Pair<Long, Long> {
// Enough upload date from source // lead range and the following range depend on if updateOnlyExpectedPeriod set.
uploadDates.size >= 3 -> { var followRange = 0
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS) var leadRange = 0
val uploadPeriod = uploadDates.indexOf(uploadDates.last()) if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
(uploadDelta).floorDiv(uploadPeriod).toInt() followRange = libraryPreferences.followingAnimeExpectedDays().get()
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
} }
// Enough fetch date from client val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
fetchDates.size >= 3 -> { // revert math of (next_update + follow < now) become (next_update < now - follow)
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS) // so (now - follow) become lower limit
val uploadPeriod = fetchDates.indexOf(fetchDates.last()) val lowerRange = startToday.minusDays(followRange.toLong())
(fetchDelta).floorDiv(uploadPeriod).toInt() 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 // Min 1, max 28 days
else -> 7 return interval.coerceIn(1, MAX_GRACE_PERIOD)
} }
// min 1, max 28 days
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
}
private fun calculateNextUpdate( private fun calculateNextUpdate(
anime: Anime, anime: Anime,
interval: Int, interval: Int,
zonedDateTime: ZonedDateTime, zonedDateTime: ZonedDateTime,
currentFetchRange: Pair<Long, Long>, fetchRange: Pair<Long, Long>,
): Long { ): Long {
return if (anime.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) || return if (
anime.calculateInterval == 0 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 latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(anime.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28)) val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000 val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
} else { latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
anime.nextUpdate } 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 const val MAX_GRACE_PERIOD = 28
fun updateIntervalMeta( class SetMangaUpdateInterval(
manga: Manga, private val libraryPreferences: LibraryPreferences = Injekt.get(),
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)
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) { fun updateInterval(
null manga: Manga,
} else { MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval) } chapters: List<Chapter>,
} zonedDateTime: ZonedDateTime,
fetchRange: Pair<Long, Long>,
fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int { ): MangaUpdate? {
val sortedChapters = chapters val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch }) getCurrentFetchRange(ZonedDateTime.now())
.take(50) } else {
fetchRange
val uploadDates = sortedChapters
.filter { it.dateUpload > 0L }
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
.toLocalDate()
.atStartOfDay()
} }
.distinct() val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
val fetchDates = sortedChapters val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone) return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
.toLocalDate() null
.atStartOfDay() } else {
MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval)
} }
.distinct() }
val newInterval = when { fun getCurrentFetchRange(timeToCal: ZonedDateTime): Pair<Long, Long> {
// Enough upload date from source // lead range and the following range depend on if updateOnlyExpectedPeriod set.
uploadDates.size >= 3 -> { var followRange = 0
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS) var leadRange = 0
val uploadPeriod = uploadDates.indexOf(uploadDates.last()) if (LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateItemRestriction().get()) {
(uploadDelta).floorDiv(uploadPeriod).toInt() followRange = libraryPreferences.followingAnimeExpectedDays().get()
leadRange = libraryPreferences.leadingAnimeExpectedDays().get()
} }
// Enough fetch date from client val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
fetchDates.size >= 3 -> { // revert math of (next_update + follow < now) become (next_update < now - follow)
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS) // so (now - follow) become lower limit
val uploadPeriod = fetchDates.indexOf(fetchDates.last()) val lowerRange = startToday.minusDays(followRange.toLong())
(fetchDelta).floorDiv(uploadPeriod).toInt() 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 // Min 1, max 28 days
else -> 7 return interval.coerceIn(1, MAX_GRACE_PERIOD)
} }
// min 1, max 28 days
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
}
private fun calculateNextUpdate( private fun calculateNextUpdate(
manga: Manga, manga: Manga,
interval: Int, interval: Int,
zonedDateTime: ZonedDateTime, zonedDateTime: ZonedDateTime,
currentFetchRange: Pair<Long, Long>, fetchRange: Pair<Long, Long>,
): Long { ): Long {
return if (manga.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) || return if (
manga.calculateInterval == 0 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 latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28)) val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000 val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
} else { latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
manga.nextUpdate } 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_matrix">Matrix</string>
<string name="theme_tidalwave">Tidal Wave</string> <string name="theme_tidalwave">Tidal Wave</string>
<string name="pref_dark_theme_pure_black">Pure black dark mode</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_date_format">Date format</string>
<string name="pref_manage_notifications">Manage notifications</string> <string name="pref_manage_notifications">Manage notifications</string>
@ -628,6 +624,10 @@
<item quantity="one">1 day</item> <item quantity="one">1 day</item>
<item quantity="other">%d days</item> <item quantity="other">%d days</item>
</plurals> </plurals>
<plurals name="range_interval_day">
<item quantity="one">%1$d - %2$d day</item>
<item quantity="other">%1$d - %2$d days</item>
</plurals>
<!-- Item info --> <!-- Item info -->
<plurals name="missing_items"> <plurals name="missing_items">
@ -669,8 +669,7 @@
<string name="display_mode_chapter">Chapter %1$s</string> <string name="display_mode_chapter">Chapter %1$s</string>
<string name="manga_display_interval_title">Estimate every</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_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="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
<string name="show_title">Source title</string> <string name="show_title">Source title</string>
<string name="show_chapter_number">Chapter number</string> <string name="show_chapter_number">Chapter number</string>

View file

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