Procházet zdrojové kódy

More refactoring of expected next update logic

arkon před 1 rokem
rodič
revize
81cd765543

+ 7 - 8
app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt

@@ -50,13 +50,14 @@ class SyncChaptersWithSource(
         manga: Manga,
         source: Source,
         manualFetch: Boolean = false,
-        zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
-        fetchRange: Pair<Long, Long> = Pair(0, 0),
+        fetchWindow: Pair<Long, Long> = Pair(0, 0),
     ): List<Chapter> {
         if (rawSourceChapters.isEmpty() && !source.isLocal()) {
             throw NoChaptersException()
         }
 
+        val now = ZonedDateTime.now()
+
         val sourceChapters = rawSourceChapters
             .distinctBy { it.url }
             .mapIndexed { i, sChapter ->
@@ -138,12 +139,11 @@ class SyncChaptersWithSource(
 
         // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
         if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
-            if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchRange.first) {
+            if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
                 updateManga.awaitUpdateFetchInterval(
                     manga,
-                    dbChapters,
-                    zoneDateTime,
-                    fetchRange,
+                    now,
+                    fetchWindow,
                 )
             }
             return emptyList()
@@ -200,8 +200,7 @@ class SyncChaptersWithSource(
             val chapterUpdates = toChange.map { it.toChapterUpdate() }
             updateChapter.awaitAll(chapterUpdates)
         }
-        val newChapters = chapterRepository.getChapterByMangaId(manga.id)
-        updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
+        updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
 
         // Set this manga as updated since chapters were changed
         // Note that last_update actually represents last time the chapter list changed at all

+ 5 - 10
app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt

@@ -3,7 +3,6 @@ package eu.kanade.domain.manga.interactor
 import eu.kanade.domain.manga.model.hasCustomCover
 import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.source.model.SManga
-import tachiyomi.domain.chapter.model.Chapter
 import tachiyomi.domain.manga.interactor.SetFetchInterval
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.domain.manga.model.MangaUpdate
@@ -79,16 +78,12 @@ class UpdateManga(
 
     suspend fun awaitUpdateFetchInterval(
         manga: Manga,
-        chapters: List<Chapter>,
-        zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
-        fetchRange: Pair<Long, Long> = setFetchInterval.getCurrent(zonedDateTime),
+        dateTime: ZonedDateTime = ZonedDateTime.now(),
+        window: Pair<Long, Long> = setFetchInterval.getWindow(dateTime),
     ): Boolean {
-        val updatedManga = setFetchInterval.update(manga, chapters, zonedDateTime, fetchRange)
-        return if (updatedManga != null) {
-            mangaRepository.update(updatedManga)
-        } else {
-            true
-        }
+        return setFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
+            ?.let { mangaRepository.update(it) }
+            ?: false
     }
 
     suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {

+ 3 - 4
app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt

@@ -62,7 +62,6 @@ import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.source.getNameForMangaInfo
 import eu.kanade.tachiyomi.ui.manga.ChapterItem
-import eu.kanade.tachiyomi.ui.manga.FetchInterval
 import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
 import eu.kanade.tachiyomi.util.lang.toRelativeString
 import eu.kanade.tachiyomi.util.system.copyToClipboard
@@ -85,7 +84,7 @@ import java.util.Date
 fun MangaScreen(
     state: MangaScreenModel.State.Success,
     snackbarHostState: SnackbarHostState,
-    fetchInterval: FetchInterval?,
+    fetchInterval: Int?,
     dateFormat: DateFormat,
     isTabletUi: Boolean,
     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
@@ -217,7 +216,7 @@ private fun MangaScreenSmallImpl(
     state: MangaScreenModel.State.Success,
     snackbarHostState: SnackbarHostState,
     dateFormat: DateFormat,
-    fetchInterval: FetchInterval?,
+    fetchInterval: Int?,
     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
     onBackClicked: () -> Unit,
@@ -448,7 +447,7 @@ fun MangaScreenLargeImpl(
     state: MangaScreenModel.State.Success,
     snackbarHostState: SnackbarHostState,
     dateFormat: DateFormat,
-    fetchInterval: FetchInterval?,
+    fetchInterval: Int?,
     chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
     chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
     onBackClicked: () -> Unit,

+ 6 - 6
app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt

@@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import eu.kanade.tachiyomi.R
-import tachiyomi.domain.manga.interactor.MAX_GRACE_PERIOD
+import tachiyomi.domain.manga.interactor.MAX_FETCH_INTERVAL
 import tachiyomi.presentation.core.components.WheelTextPicker
 
 @Composable
@@ -56,7 +56,7 @@ fun SetIntervalDialog(
     onDismissRequest: () -> Unit,
     onValueChanged: (Int) -> Unit,
 ) {
-    var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
+    var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
 
     AlertDialog(
         onDismissRequest = onDismissRequest,
@@ -67,7 +67,7 @@ fun SetIntervalDialog(
                 contentAlignment = Alignment.Center,
             ) {
                 val size = DpSize(width = maxWidth / 2, height = 128.dp)
-                val items = (0..MAX_GRACE_PERIOD).map {
+                val items = (0..MAX_FETCH_INTERVAL).map {
                     if (it == 0) {
                         stringResource(R.string.label_default)
                     } else {
@@ -77,8 +77,8 @@ fun SetIntervalDialog(
                 WheelTextPicker(
                     size = size,
                     items = items,
-                    startIndex = intervalValue,
-                    onSelectionChanged = { intervalValue = it },
+                    startIndex = selectedInterval,
+                    onSelectionChanged = { selectedInterval = it },
                 )
             }
         },
@@ -89,7 +89,7 @@ fun SetIntervalDialog(
         },
         confirmButton = {
             TextButton(onClick = {
-                onValueChanged(intervalValue)
+                onValueChanged(selectedInterval)
                 onDismissRequest()
             },) {
                 Text(text = stringResource(R.string.action_ok))

+ 3 - 9
app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt

@@ -78,13 +78,13 @@ import coil.compose.AsyncImage
 import eu.kanade.presentation.components.DropdownMenu
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.ui.manga.FetchInterval
 import eu.kanade.tachiyomi.util.system.copyToClipboard
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.presentation.core.components.material.TextButton
 import tachiyomi.presentation.core.components.material.padding
 import tachiyomi.presentation.core.util.clickableNoIndication
 import tachiyomi.presentation.core.util.secondaryItemAlpha
+import kotlin.math.absoluteValue
 import kotlin.math.roundToInt
 
 private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@@ -166,7 +166,7 @@ fun MangaActionRow(
     modifier: Modifier = Modifier,
     favorite: Boolean,
     trackingCount: Int,
-    fetchInterval: FetchInterval?,
+    fetchInterval: Int?,
     isUserIntervalMode: Boolean,
     onAddToLibraryClicked: () -> Unit,
     onWebViewClicked: (() -> Unit)?,
@@ -190,14 +190,8 @@ fun MangaActionRow(
             onLongClick = onEditCategory,
         )
         if (onEditIntervalClicked != null && fetchInterval != null) {
-            val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays)
             MangaActionButton(
-                title =
-                if (intervalPair.first == intervalPair.second) {
-                    pluralStringResource(id = R.plurals.day, count = intervalPair.second, intervalPair.second)
-                } else {
-                    pluralStringResource(id = R.plurals.range_interval_day, count = intervalPair.second, intervalPair.first, intervalPair.second)
-                },
+                title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
                 icon = Icons.Default.HourglassEmpty,
                 color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
                 onClick = onEditIntervalClicked,

+ 3 - 124
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt

@@ -1,33 +1,18 @@
 package eu.kanade.presentation.more.settings.screen
 
 import androidx.annotation.StringRes
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.pluralStringResource
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastMap
 import androidx.core.content.ContextCompat
 import cafe.adriel.voyager.navigator.LocalNavigator
@@ -54,8 +39,6 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_U
 import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_COMPLETED
 import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
 import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
-import tachiyomi.domain.manga.interactor.MAX_GRACE_PERIOD
-import tachiyomi.presentation.core.components.WheelTextPicker
 import tachiyomi.presentation.core.util.collectAsState
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
@@ -141,13 +124,10 @@ object SettingsLibraryScreen : SearchableSettings {
         val context = LocalContext.current
 
         val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
-        val libraryUpdateDeviceRestrictionPref = libraryPreferences.libraryUpdateDeviceRestriction()
-        val libraryUpdateMangaRestrictionPref = libraryPreferences.libraryUpdateMangaRestriction()
         val libraryUpdateCategoriesPref = libraryPreferences.libraryUpdateCategories()
         val libraryUpdateCategoriesExcludePref = libraryPreferences.libraryUpdateCategoriesExclude()
 
         val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
-        val libraryUpdateMangaRestriction by libraryUpdateMangaRestrictionPref.collectAsState()
 
         val included by libraryUpdateCategoriesPref.collectAsState()
         val excluded by libraryUpdateCategoriesExcludePref.collectAsState()
@@ -168,25 +148,10 @@ object SettingsLibraryScreen : SearchableSettings {
                 },
             )
         }
-        val leadRange by libraryPreferences.leadingExpectedDays().collectAsState()
-        val followRange by libraryPreferences.followingExpectedDays().collectAsState()
 
-        var showFetchRangesDialog by rememberSaveable { mutableStateOf(false) }
-        if (showFetchRangesDialog) {
-            LibraryExpectedRangeDialog(
-                initialLead = leadRange,
-                initialFollow = followRange,
-                onDismissRequest = { showFetchRangesDialog = false },
-                onValueChanged = { leadValue, followValue ->
-                    libraryPreferences.leadingExpectedDays().set(leadValue)
-                    libraryPreferences.followingExpectedDays().set(followValue)
-                    showFetchRangesDialog = false
-                },
-            )
-        }
         return Preference.PreferenceGroup(
             title = stringResource(R.string.pref_category_library_update),
-            preferenceItems = listOfNotNull(
+            preferenceItems = listOf(
                 Preference.PreferenceItem.ListPreference(
                     pref = libraryUpdateIntervalPref,
                     title = stringResource(R.string.pref_library_update_interval),
@@ -204,7 +169,7 @@ object SettingsLibraryScreen : SearchableSettings {
                     },
                 ),
                 Preference.PreferenceItem.MultiSelectListPreference(
-                    pref = libraryUpdateDeviceRestrictionPref,
+                    pref = libraryPreferences.libraryUpdateDeviceRestriction(),
                     enabled = libraryUpdateInterval > 0,
                     title = stringResource(R.string.pref_library_update_restriction),
                     subtitle = stringResource(R.string.restrictions),
@@ -241,7 +206,7 @@ object SettingsLibraryScreen : SearchableSettings {
                     subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
                 ),
                 Preference.PreferenceItem.MultiSelectListPreference(
-                    pref = libraryUpdateMangaRestrictionPref,
+                    pref = libraryPreferences.libraryUpdateMangaRestriction(),
                     title = stringResource(R.string.pref_library_update_manga_restriction),
                     entries = mapOf(
                         MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
@@ -250,17 +215,6 @@ object SettingsLibraryScreen : SearchableSettings {
                         MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
                     ),
                 ),
-                Preference.PreferenceItem.TextPreference(
-                    title = stringResource(R.string.pref_update_release_grace_period),
-                    subtitle = listOf(
-                        pluralStringResource(R.plurals.pref_update_release_leading_days, leadRange, leadRange),
-                        pluralStringResource(R.plurals.pref_update_release_following_days, followRange, followRange),
-                    ).joinToString(),
-                    onClick = { showFetchRangesDialog = true },
-                ).takeIf { MANGA_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
-                Preference.PreferenceItem.InfoPreference(
-                    title = stringResource(R.string.pref_update_release_grace_period_info),
-                ).takeIf { MANGA_OUTSIDE_RELEASE_PERIOD in libraryUpdateMangaRestriction },
                 Preference.PreferenceItem.SwitchPreference(
                     pref = libraryPreferences.newShowUpdatesCount(),
                     title = stringResource(R.string.pref_library_update_show_tab_badge),
@@ -299,79 +253,4 @@ object SettingsLibraryScreen : SearchableSettings {
             ),
         )
     }
-    	
-    @Composable
-    private fun LibraryExpectedRangeDialog(
-        initialLead: Int,
-        initialFollow: Int,
-        onDismissRequest: () -> Unit,
-        onValueChanged: (portrait: Int, landscape: Int) -> Unit,
-    ) {
-        var leadValue by rememberSaveable { mutableIntStateOf(initialLead) }
-        var followValue by rememberSaveable { mutableIntStateOf(initialFollow) }
-
-        AlertDialog(
-            onDismissRequest = onDismissRequest,
-            title = { Text(text = stringResource(R.string.pref_update_release_grace_period)) },
-            text = {
-                Column {
-                    Row(
-                        horizontalArrangement = Arrangement.spacedBy(8.dp),
-                    ) {
-                        Text(
-                            modifier = Modifier.weight(1f),
-                            text = pluralStringResource(R.plurals.pref_update_release_leading_days, leadValue, leadValue),
-                            textAlign = TextAlign.Center,
-                            maxLines = 1,
-                            style = MaterialTheme.typography.labelMedium,
-                        )
-                        Text(
-                            modifier = Modifier.weight(1f),
-                            text = pluralStringResource(R.plurals.pref_update_release_following_days, followValue, followValue),
-                            textAlign = TextAlign.Center,
-                            maxLines = 1,
-                            style = MaterialTheme.typography.labelMedium,
-                        )
-                    }
-                }
-                BoxWithConstraints(
-                    modifier = Modifier.fillMaxWidth(),
-                    contentAlignment = Alignment.Center,
-                ) {
-                    val size = DpSize(width = maxWidth / 2, height = 128.dp)
-                    val items = (0..MAX_GRACE_PERIOD).map(Int::toString)
-                    Row(
-                        horizontalArrangement = Arrangement.spacedBy(8.dp),
-                    ) {
-                        WheelTextPicker(
-                            size = size,
-                            items = items,
-                            startIndex = leadValue,
-                            onSelectionChanged = {
-                                leadValue = it
-                            },
-                        )
-                        WheelTextPicker(
-                            size = size,
-                            items = items,
-                            startIndex = followValue,
-                            onSelectionChanged = {
-                                followValue = it
-                            },
-                        )
-                    }
-                }
-            },
-            dismissButton = {
-                TextButton(onClick = onDismissRequest) {
-                    Text(text = stringResource(android.R.string.cancel))
-                }
-            },
-            confirmButton = {
-                TextButton(onClick = { onValueChanged(leadValue, followValue) }) {
-                    Text(text = stringResource(R.string.action_ok))
-                }
-            },
-        )
-    }
 }

+ 5 - 6
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt

@@ -33,8 +33,8 @@ class BackupRestorer(
     private val chapterRepository: ChapterRepository = Injekt.get()
     private val setFetchInterval: SetFetchInterval = Injekt.get()
 
-    private var zonedDateTime = ZonedDateTime.now()
-    private var currentFetchInterval = setFetchInterval.getCurrent(zonedDateTime)
+    private var now = ZonedDateTime.now()
+    private var currentFetchWindow = setFetchInterval.getWindow(now)
 
     private var backupManager = BackupManager(context)
 
@@ -102,8 +102,8 @@ class BackupRestorer(
         // Store source mapping for error messages
         val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
         sourceMapping = backupMaps.associate { it.sourceId to it.name }
-        zonedDateTime = ZonedDateTime.now()
-        currentFetchInterval = setFetchInterval.getCurrent(zonedDateTime)
+        now = ZonedDateTime.now()
+        currentFetchWindow = setFetchInterval.getWindow(now)
 
         return coroutineScope {
             // Restore individual manga
@@ -146,8 +146,7 @@ class BackupRestorer(
                 // Fetch rest of manga information
                 restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
             }
-            val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id)
-            updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentFetchInterval)
+            updateManga.awaitUpdateFetchInterval(restoredManga, now, currentFetchWindow)
         } catch (e: Exception) {
             val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
             errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")

+ 15 - 10
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt

@@ -231,9 +231,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
         val hasDownloads = AtomicBoolean(false)
         val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
 
-        val now = ZonedDateTime.now()
-        val fetchInterval = setFetchInterval.getCurrent(now)
-        val higherLimit = fetchInterval.second
+        val fetchWindow by lazy { setFetchInterval.getWindow(ZonedDateTime.now()) }
 
         coroutineScope {
             mangaToUpdate.groupBy { it.manga.source }.values
@@ -255,8 +253,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
                                     manga,
                                 ) {
                                     when {
-                                        MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit ->
-                                            skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
+                                        manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
+                                            skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
 
                                         MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
                                             skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
@@ -267,12 +265,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
                                         MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
                                             skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
 
-                                        manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
-                                            skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
+                                        MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate !in fetchWindow.first.rangeTo(fetchWindow.second) ->
+                                            skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
 
                                         else -> {
                                             try {
-                                                val newChapters = updateManga(manga, now, fetchInterval)
+                                                val newChapters = updateManga(manga, fetchWindow)
                                                     .sortedByDescending { it.sourceOrder }
 
                                                 if (newChapters.isNotEmpty()) {
@@ -328,6 +326,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
             )
         }
         if (skippedUpdates.isNotEmpty()) {
+            // TODO: surface skipped reasons to user
+            logcat {
+                skippedUpdates
+                    .groupBy { it.second }
+                    .map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
+                    .joinToString()
+            }
             notifier.showUpdateSkippedNotification(skippedUpdates.size)
         }
     }
@@ -344,7 +349,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
      * @param manga the manga to update.
      * @return a pair of the inserted and removed chapters.
      */
-    private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
+    private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> {
         val source = sourceManager.getOrStub(manga.source)
 
         // Update manga metadata if needed
@@ -359,7 +364,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
         // to get latest data so it doesn't get overwritten later on
         val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
 
-        return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
+        return syncChaptersWithSource.await(chapters, dbManga, source, false, fetchWindow)
     }
 
     private suspend fun updateCovers() {

+ 2 - 9
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt

@@ -83,13 +83,6 @@ class MangaScreen(
 
         val successState = state as MangaScreenModel.State.Success
         val isHttpSource = remember { successState.source is HttpSource }
-        val fetchInterval = remember(successState.manga.fetchInterval) {
-            FetchInterval(
-                interval = successState.manga.fetchInterval,
-                leadDays = screenModel.leadDay,
-                followDays = screenModel.followDay,
-            )
-        }
 
         LaunchedEffect(successState.manga, screenModel.source) {
             if (isHttpSource) {
@@ -107,7 +100,7 @@ class MangaScreen(
             state = successState,
             snackbarHostState = screenModel.snackbarHostState,
             dateFormat = screenModel.dateFormat,
-            fetchInterval = fetchInterval,
+            fetchInterval = successState.manga.fetchInterval,
             isTabletUi = isTabletUi(),
             chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
             chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
@@ -218,7 +211,7 @@ class MangaScreen(
             }
             is MangaScreenModel.Dialog.SetFetchInterval -> {
                 SetIntervalDialog(
-                    interval = if (dialog.manga.fetchInterval < 0) -dialog.manga.fetchInterval else 0,
+                    interval = dialog.manga.fetchInterval,
                     onDismissRequest = onDismissRequest,
                     onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
                 )

+ 5 - 20
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreenModel.kt

@@ -129,8 +129,6 @@ class MangaScreenModel(
     private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
 
     val isUpdateIntervalEnabled = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateMangaRestriction().get()
-    val leadDay = libraryPreferences.leadingExpectedDays().get()
-    val followDay = libraryPreferences.followingExpectedDays().get()
 
     private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
     private val selectedChapterIds: HashSet<Long> = HashSet()
@@ -361,20 +359,14 @@ class MangaScreenModel(
         }
     }
 
-    fun setFetchInterval(manga: Manga, newInterval: Int) {
-        val interval = when (newInterval) {
-            // reset interval 0 default to trigger recalculation
-            // only reset if interval is custom, which is negative
-            0 -> if (manga.fetchInterval < 0) 0 else manga.fetchInterval
-            else -> -newInterval
-        }
+    fun setFetchInterval(manga: Manga, interval: Int) {
         coroutineScope.launchIO {
             updateManga.awaitUpdateFetchInterval(
-                manga.copy(fetchInterval = interval),
-                successState?.chapters?.map { it.chapter }.orEmpty(),
+                // Custom intervals are negative
+                manga.copy(fetchInterval = -interval),
             )
-            val newManga = mangaRepository.getMangaById(mangaId)
-            updateSuccessState { it.copy(manga = newManga) }
+            val updatedManga = mangaRepository.getMangaById(manga.id)
+            updateSuccessState { it.copy(manga = updatedManga) }
         }
     }
 
@@ -1055,10 +1047,3 @@ data class ChapterItem(
 ) {
     val isDownloaded = downloadState == Download.State.DOWNLOADED
 }
-
-@Immutable
-data class FetchInterval(
-    val interval: Int,
-    val leadDays: Int,
-    val followDays: Int,
-)

+ 0 - 3
domain/src/main/java/tachiyomi/domain/library/service/LibraryPreferences.kt

@@ -38,9 +38,6 @@ class LibraryPreferences(
         ),
     )
 
-    fun leadingExpectedDays() = preferenceStore.getInt("pref_library_before_expect_key", 1)
-    fun followingExpectedDays() = preferenceStore.getInt("pref_library_after_expect_key", 1)
-
     fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false)
 
     fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)

+ 37 - 41
domain/src/main/java/tachiyomi/domain/manga/interactor/SetFetchInterval.kt

@@ -1,35 +1,34 @@
 package tachiyomi.domain.manga.interactor
 
+import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
 import tachiyomi.domain.chapter.model.Chapter
-import tachiyomi.domain.library.service.LibraryPreferences
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.domain.manga.model.MangaUpdate
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
 import java.time.Instant
 import java.time.ZonedDateTime
 import java.time.temporal.ChronoUnit
 import kotlin.math.absoluteValue
 
-const val MAX_GRACE_PERIOD = 28
+const val MAX_FETCH_INTERVAL = 28
+private const val FETCH_INTERVAL_GRACE_PERIOD = 1
 
 class SetFetchInterval(
-    private val libraryPreferences: LibraryPreferences = Injekt.get(),
+    private val getChapterByMangaId: GetChapterByMangaId,
 ) {
 
-    fun update(
+    suspend fun toMangaUpdateOrNull(
         manga: Manga,
-        chapters: List<Chapter>,
-        zonedDateTime: ZonedDateTime,
-        fetchRange: Pair<Long, Long>,
+        dateTime: ZonedDateTime,
+        window: Pair<Long, Long>,
     ): MangaUpdate? {
-        val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) {
-            getCurrent(ZonedDateTime.now())
+        val currentWindow = if (window.first == 0L && window.second == 0L) {
+            getWindow(ZonedDateTime.now())
         } else {
-            fetchRange
+            window
         }
-        val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
-        val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentInterval)
+        val chapters = getChapterByMangaId.await(manga.id)
+        val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime)
+        val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
 
         return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
             null
@@ -38,20 +37,11 @@ class SetFetchInterval(
         }
     }
 
-    fun getCurrent(timeToCal: ZonedDateTime): Pair<Long, Long> {
-        // lead range and the following range depend on if updateOnlyExpectedPeriod set.
-        var followRange = 0
-        var leadRange = 0
-        if (LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateMangaRestriction().get()) {
-            followRange = libraryPreferences.followingExpectedDays().get()
-            leadRange = libraryPreferences.leadingExpectedDays().get()
-        }
-        val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
-        // revert math of (next_update + follow < now) become (next_update < now - follow)
-        // so (now - follow) become lower limit
-        val lowerRange = startToday.minusDays(followRange.toLong())
-        val higherRange = startToday.plusDays(leadRange.toLong())
-        return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
+    fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
+        val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
+        val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
+        val upperBound = lowerBound.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
+        return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
     }
 
     internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
@@ -91,35 +81,41 @@ class SetFetchInterval(
             // Default to 7 days
             else -> 7
         }
-        // Min 1, max 28 days
-        return interval.coerceIn(1, MAX_GRACE_PERIOD)
+
+        return interval.coerceIn(1, MAX_FETCH_INTERVAL)
     }
 
     private fun calculateNextUpdate(
         manga: Manga,
         interval: Int,
-        zonedDateTime: ZonedDateTime,
-        fetchRange: Pair<Long, Long>,
+        dateTime: ZonedDateTime,
+        window: Pair<Long, Long>,
     ): Long {
         return if (
-            manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
+            manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
             manga.fetchInterval == 0
         ) {
-            val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
-            val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
-            val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
-            latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
+            val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
+                .toLocalDate()
+                .atStartOfDay()
+            val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
+            val cycle = timeSinceLatest.floorDiv(
+                interval.absoluteValue.takeIf { interval < 0 }
+                    ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
+            )
+            latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
         } else {
             manga.nextUpdate
         }
     }
 
-    private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
-        if (delta >= maxValue) return maxValue
-        val cycle = timeSinceLatest.floorDiv(delta) + 1
+    private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
+        if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
+
         // double delta again if missed more than 9 check in new delta
+        val cycle = timeSinceLatest.floorDiv(delta) + 1
         return if (cycle > doubleWhenOver) {
-            doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
+            doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
         } else {
             delta
         }

+ 10 - 13
domain/src/test/java/tachiyomi/domain/manga/interactor/SetFetchIntervalTest.kt

@@ -11,6 +11,7 @@ import java.time.ZonedDateTime
 
 @Execution(ExecutionMode.CONCURRENT)
 class SetFetchIntervalTest {
+
     private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
     private var chapter = Chapter.create().copy(
         dateFetch = testTime.toEpochSecond() * 1000,
@@ -19,14 +20,8 @@ class SetFetchIntervalTest {
 
     private val setFetchInterval = SetFetchInterval(mockk())
 
-    private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
-        val newTime = testTime.plus(duration).toEpochSecond() * 1000
-        return chapter.copy(dateFetch = newTime, dateUpload = newTime)
-    }
-
-    // default 7 when less than 3 distinct day
     @Test
-    fun `calculateInterval returns 7 when 1 chapters in 1 day`() {
+    fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
         val chapters = mutableListOf<Chapter>()
         (1..1).forEach {
             val duration = Duration.ofHours(10)
@@ -63,9 +58,8 @@ class SetFetchIntervalTest {
         setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
     }
 
-    // Default 1 if interval less than 1
     @Test
-    fun `calculateInterval returns 1 when 5 chapters in 75 hours, 3 days`() {
+    fun `calculateInterval returns default of 1 day when interval less than 1`() {
         val chapters = mutableListOf<Chapter>()
         (1..5).forEach {
             val duration = Duration.ofHours(15L * it)
@@ -98,9 +92,8 @@ class SetFetchIntervalTest {
         setFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
     }
 
-    // If interval is decimal, floor to closest integer
     @Test
-    fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days`() {
+    fun `calculateInterval returns floored value when interval is decimal`() {
         val chapters = mutableListOf<Chapter>()
         (1..5).forEach {
             val duration = Duration.ofHours(25L * it)
@@ -121,9 +114,8 @@ class SetFetchIntervalTest {
         setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
     }
 
-    // Use fetch time if upload time not available
     @Test
-    fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days of dateFetch`() {
+    fun `calculateInterval returns interval based on fetch time if upload time not available`() {
         val chapters = mutableListOf<Chapter>()
         (1..5).forEach {
             val duration = Duration.ofHours(25L * it)
@@ -132,4 +124,9 @@ class SetFetchIntervalTest {
         }
         setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
     }
+
+    private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
+        val newTime = testTime.plus(duration).toEpochSecond() * 1000
+        return chapter.copy(dateFetch = newTime, dateUpload = newTime)
+    }
 }

+ 0 - 15
i18n/src/main/res/values/strings.xml

@@ -259,17 +259,6 @@
     <string name="pref_library_update_show_tab_badge">Show unread count on Updates icon</string>
     <string name="pref_update_only_in_release_period">Outside expected release period</string>
 
-    <string name="pref_update_release_grace_period">Expected release grace period</string>
-    <plurals name="pref_update_release_leading_days">
-        <item quantity="one">%d day before</item>
-        <item quantity="other">%d days before</item>
-    </plurals>
-    <plurals name="pref_update_release_following_days">
-        <item quantity="one">%d day after</item>
-        <item quantity="other">%d days after</item>
-    </plurals>
-    <string name="pref_update_release_grace_period_info">A low grace period is recommended to minimize stress on sources. The more checks for an entry that are missed, the longer the interval in between checks will be with a maximum of 28 days.</string>
-
     <string name="pref_library_update_refresh_metadata">Automatically refresh metadata</string>
     <string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
     <string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
@@ -637,10 +626,6 @@
         <item quantity="one">1 day</item>
         <item quantity="other">%d days</item>
     </plurals>
-    <plurals name="range_interval_day">
-        <item quantity="one">%1$d - %2$d day</item>
-        <item quantity="other">%1$d - %2$d days</item>
-    </plurals>
 
     <!-- Manga info -->
     <plurals name="missing_chapters">