More refactoring of expected next update logic
This commit is contained in:
parent
c9a1bd86b5
commit
81cd765543
14 changed files with 101 additions and 278 deletions
|
@ -50,13 +50,14 @@ class SyncChaptersWithSource(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: Source,
|
source: Source,
|
||||||
manualFetch: Boolean = false,
|
manualFetch: Boolean = false,
|
||||||
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
||||||
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
|
||||||
): List<Chapter> {
|
): List<Chapter> {
|
||||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||||
throw NoChaptersException()
|
throw NoChaptersException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val now = ZonedDateTime.now()
|
||||||
|
|
||||||
val sourceChapters = rawSourceChapters
|
val sourceChapters = rawSourceChapters
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }
|
||||||
.mapIndexed { i, sChapter ->
|
.mapIndexed { i, sChapter ->
|
||||||
|
@ -138,12 +139,11 @@ class SyncChaptersWithSource(
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchRange.first) {
|
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||||
updateManga.awaitUpdateFetchInterval(
|
updateManga.awaitUpdateFetchInterval(
|
||||||
manga,
|
manga,
|
||||||
dbChapters,
|
now,
|
||||||
zoneDateTime,
|
fetchWindow,
|
||||||
fetchRange,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
|
@ -200,8 +200,7 @@ class SyncChaptersWithSource(
|
||||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
}
|
}
|
||||||
val newChapters = chapterRepository.getChapterByMangaId(manga.id)
|
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||||
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
|
|
||||||
|
|
||||||
// Set this manga as updated since chapters were changed
|
// Set this manga as updated since chapters were changed
|
||||||
// Note that last_update actually represents last time the chapter list changed at all
|
// Note that last_update actually represents last time the chapter list changed at all
|
||||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.domain.manga.interactor
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
import eu.kanade.domain.manga.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
|
||||||
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
import tachiyomi.domain.manga.interactor.SetFetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
|
@ -79,16 +78,12 @@ class UpdateManga(
|
||||||
|
|
||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
window: Pair<Long, Long> = setFetchInterval.getWindow(dateTime),
|
||||||
fetchRange: Pair<Long, Long> = setFetchInterval.getCurrent(zonedDateTime),
|
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val updatedManga = setFetchInterval.update(manga, chapters, zonedDateTime, fetchRange)
|
return setFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||||
return if (updatedManga != null) {
|
?.let { mangaRepository.update(it) }
|
||||||
mangaRepository.update(updatedManga)
|
?: false
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||||
|
|
|
@ -62,7 +62,6 @@ import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
import eu.kanade.tachiyomi.source.getNameForMangaInfo
|
||||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
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.ui.manga.MangaScreenModel
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
|
@ -85,7 +84,7 @@ import java.util.Date
|
||||||
fun MangaScreen(
|
fun MangaScreen(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: FetchInterval?,
|
fetchInterval: Int?,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -217,7 +216,7 @@ private fun MangaScreenSmallImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: FetchInterval?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
@ -448,7 +447,7 @@ fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: FetchInterval?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
|
|
@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.domain.manga.interactor.MAX_GRACE_PERIOD
|
import tachiyomi.domain.manga.interactor.MAX_FETCH_INTERVAL
|
||||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -56,7 +56,7 @@ fun SetIntervalDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onValueChanged: (Int) -> Unit,
|
onValueChanged: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
|
var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
@ -67,7 +67,7 @@ fun SetIntervalDialog(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||||
val items = (0..MAX_GRACE_PERIOD).map {
|
val items = (0..MAX_FETCH_INTERVAL).map {
|
||||||
if (it == 0) {
|
if (it == 0) {
|
||||||
stringResource(R.string.label_default)
|
stringResource(R.string.label_default)
|
||||||
} else {
|
} else {
|
||||||
|
@ -77,8 +77,8 @@ fun SetIntervalDialog(
|
||||||
WheelTextPicker(
|
WheelTextPicker(
|
||||||
size = size,
|
size = size,
|
||||||
items = items,
|
items = items,
|
||||||
startIndex = intervalValue,
|
startIndex = selectedInterval,
|
||||||
onSelectionChanged = { intervalValue = it },
|
onSelectionChanged = { selectedInterval = it },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -89,7 +89,7 @@ fun SetIntervalDialog(
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onValueChanged(intervalValue)
|
onValueChanged(selectedInterval)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
},) {
|
||||||
Text(text = stringResource(R.string.action_ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
|
|
|
@ -78,13 +78,13 @@ import coil.compose.AsyncImage
|
||||||
import eu.kanade.presentation.components.DropdownMenu
|
import eu.kanade.presentation.components.DropdownMenu
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.manga.FetchInterval
|
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.presentation.core.components.material.TextButton
|
import tachiyomi.presentation.core.components.material.TextButton
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||||
|
@ -166,7 +166,7 @@ fun MangaActionRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
fetchInterval: FetchInterval?,
|
fetchInterval: Int?,
|
||||||
isUserIntervalMode: Boolean,
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
|
@ -190,14 +190,8 @@ fun MangaActionRow(
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
if (onEditIntervalClicked != null && fetchInterval != null) {
|
if (onEditIntervalClicked != null && fetchInterval != null) {
|
||||||
val intervalPair = 1.coerceAtLeast(fetchInterval.interval - fetchInterval.leadDays) to (fetchInterval.interval + fetchInterval.followDays)
|
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title =
|
title = pluralStringResource(id = R.plurals.day, count = fetchInterval.absoluteValue, fetchInterval.absoluteValue),
|
||||||
if (intervalPair.first == intervalPair.second) {
|
|
||||||
pluralStringResource(id = R.plurals.day, count = intervalPair.second, intervalPair.second)
|
|
||||||
} else {
|
|
||||||
pluralStringResource(id = R.plurals.range_interval_day, count = intervalPair.second, intervalPair.first, intervalPair.second)
|
|
||||||
},
|
|
||||||
icon = Icons.Default.HourglassEmpty,
|
icon = Icons.Default.HourglassEmpty,
|
||||||
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
onClick = onEditIntervalClicked,
|
onClick = onEditIntervalClicked,
|
||||||
|
|
|
@ -1,33 +1,18 @@
|
||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.util.fastMap
|
import androidx.compose.ui.util.fastMap
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
@ -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_COMPLETED
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
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 tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -141,13 +124,10 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
|
val libraryUpdateIntervalPref = libraryPreferences.libraryUpdateInterval()
|
||||||
val libraryUpdateDeviceRestrictionPref = libraryPreferences.libraryUpdateDeviceRestriction()
|
|
||||||
val libraryUpdateMangaRestrictionPref = libraryPreferences.libraryUpdateMangaRestriction()
|
|
||||||
val libraryUpdateCategoriesPref = libraryPreferences.libraryUpdateCategories()
|
val libraryUpdateCategoriesPref = libraryPreferences.libraryUpdateCategories()
|
||||||
val libraryUpdateCategoriesExcludePref = libraryPreferences.libraryUpdateCategoriesExclude()
|
val libraryUpdateCategoriesExcludePref = libraryPreferences.libraryUpdateCategoriesExclude()
|
||||||
|
|
||||||
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
|
val libraryUpdateInterval by libraryUpdateIntervalPref.collectAsState()
|
||||||
val libraryUpdateMangaRestriction by libraryUpdateMangaRestrictionPref.collectAsState()
|
|
||||||
|
|
||||||
val included by libraryUpdateCategoriesPref.collectAsState()
|
val included by libraryUpdateCategoriesPref.collectAsState()
|
||||||
val excluded by libraryUpdateCategoriesExcludePref.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(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(R.string.pref_category_library_update),
|
title = stringResource(R.string.pref_category_library_update),
|
||||||
preferenceItems = listOfNotNull(
|
preferenceItems = listOf(
|
||||||
Preference.PreferenceItem.ListPreference(
|
Preference.PreferenceItem.ListPreference(
|
||||||
pref = libraryUpdateIntervalPref,
|
pref = libraryUpdateIntervalPref,
|
||||||
title = stringResource(R.string.pref_library_update_interval),
|
title = stringResource(R.string.pref_library_update_interval),
|
||||||
|
@ -204,7 +169,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryUpdateDeviceRestrictionPref,
|
pref = libraryPreferences.libraryUpdateDeviceRestriction(),
|
||||||
enabled = libraryUpdateInterval > 0,
|
enabled = libraryUpdateInterval > 0,
|
||||||
title = stringResource(R.string.pref_library_update_restriction),
|
title = stringResource(R.string.pref_library_update_restriction),
|
||||||
subtitle = stringResource(R.string.restrictions),
|
subtitle = stringResource(R.string.restrictions),
|
||||||
|
@ -241,7 +206,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryUpdateMangaRestrictionPref,
|
pref = libraryPreferences.libraryUpdateMangaRestriction(),
|
||||||
title = stringResource(R.string.pref_library_update_manga_restriction),
|
title = stringResource(R.string.pref_library_update_manga_restriction),
|
||||||
entries = mapOf(
|
entries = mapOf(
|
||||||
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
|
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),
|
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(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
pref = libraryPreferences.newShowUpdatesCount(),
|
pref = libraryPreferences.newShowUpdatesCount(),
|
||||||
title = stringResource(R.string.pref_library_update_show_tab_badge),
|
title = stringResource(R.string.pref_library_update_show_tab_badge),
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,8 @@ class BackupRestorer(
|
||||||
private val chapterRepository: ChapterRepository = Injekt.get()
|
private val chapterRepository: ChapterRepository = Injekt.get()
|
||||||
private val setFetchInterval: SetFetchInterval = Injekt.get()
|
private val setFetchInterval: SetFetchInterval = Injekt.get()
|
||||||
|
|
||||||
private var zonedDateTime = ZonedDateTime.now()
|
private var now = ZonedDateTime.now()
|
||||||
private var currentFetchInterval = setFetchInterval.getCurrent(zonedDateTime)
|
private var currentFetchWindow = setFetchInterval.getWindow(now)
|
||||||
|
|
||||||
private var backupManager = BackupManager(context)
|
private var backupManager = BackupManager(context)
|
||||||
|
|
||||||
|
@ -102,8 +102,8 @@ class BackupRestorer(
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||||
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
sourceMapping = backupMaps.associate { it.sourceId to it.name }
|
||||||
zonedDateTime = ZonedDateTime.now()
|
now = ZonedDateTime.now()
|
||||||
currentFetchInterval = setFetchInterval.getCurrent(zonedDateTime)
|
currentFetchWindow = setFetchInterval.getWindow(now)
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
|
@ -146,8 +146,7 @@ class BackupRestorer(
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
|
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
val updatedChapters = chapterRepository.getChapterByMangaId(restoredManga.id)
|
updateManga.awaitUpdateFetchInterval(restoredManga, now, currentFetchWindow)
|
||||||
updateManga.awaitUpdateFetchInterval(restoredManga, updatedChapters, zonedDateTime, currentFetchInterval)
|
|
||||||
} 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}")
|
||||||
|
|
|
@ -231,9 +231,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
|
val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get()
|
||||||
|
|
||||||
val now = ZonedDateTime.now()
|
val fetchWindow by lazy { setFetchInterval.getWindow(ZonedDateTime.now()) }
|
||||||
val fetchInterval = setFetchInterval.getCurrent(now)
|
|
||||||
val higherLimit = fetchInterval.second
|
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
mangaToUpdate.groupBy { it.manga.source }.values
|
mangaToUpdate.groupBy { it.manga.source }.values
|
||||||
|
@ -255,8 +253,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
manga,
|
manga,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
MANGA_OUTSIDE_RELEASE_PERIOD in restrictions && manga.nextUpdate > higherLimit ->
|
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_always_update))
|
||||||
|
|
||||||
MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
MANGA_NON_COMPLETED in restrictions && manga.status.toInt() == SManga.COMPLETED ->
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_completed))
|
||||||
|
@ -267,12 +265,12 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
|
MANGA_NON_READ in restrictions && libraryManga.totalChapters > 0L && !libraryManga.hasStarted ->
|
||||||
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_started))
|
||||||
|
|
||||||
manga.updateStrategy != UpdateStrategy.ALWAYS_UPDATE ->
|
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_always_update))
|
skippedUpdates.add(manga to context.getString(R.string.skipped_reason_not_in_release_period))
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
try {
|
try {
|
||||||
val newChapters = updateManga(manga, now, fetchInterval)
|
val newChapters = updateManga(manga, fetchWindow)
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
|
@ -328,6 +326,13 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (skippedUpdates.isNotEmpty()) {
|
if (skippedUpdates.isNotEmpty()) {
|
||||||
|
// TODO: surface skipped reasons to user
|
||||||
|
logcat {
|
||||||
|
skippedUpdates
|
||||||
|
.groupBy { it.second }
|
||||||
|
.map { (reason, entries) -> "$reason: [${entries.map { it.first.title }.sorted().joinToString()}]" }
|
||||||
|
.joinToString()
|
||||||
|
}
|
||||||
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
notifier.showUpdateSkippedNotification(skippedUpdates.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,7 +349,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return a pair of the inserted and removed chapters.
|
* @return a pair of the inserted and removed chapters.
|
||||||
*/
|
*/
|
||||||
private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
|
private suspend fun updateManga(manga: Manga, fetchWindow: Pair<Long, Long>): List<Chapter> {
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
// Update manga metadata if needed
|
// Update manga metadata if needed
|
||||||
|
@ -359,7 +364,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
// to get latest data so it doesn't get overwritten later on
|
// to get latest data so it doesn't get overwritten later on
|
||||||
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
|
val dbManga = getManga.await(manga.id)?.takeIf { it.favorite } ?: return emptyList()
|
||||||
|
|
||||||
return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
|
return syncChaptersWithSource.await(chapters, dbManga, source, false, fetchWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
|
|
|
@ -83,13 +83,6 @@ class MangaScreen(
|
||||||
|
|
||||||
val successState = state as MangaScreenModel.State.Success
|
val successState = state as MangaScreenModel.State.Success
|
||||||
val isHttpSource = remember { successState.source is HttpSource }
|
val isHttpSource = remember { successState.source is HttpSource }
|
||||||
val fetchInterval = remember(successState.manga.fetchInterval) {
|
|
||||||
FetchInterval(
|
|
||||||
interval = successState.manga.fetchInterval,
|
|
||||||
leadDays = screenModel.leadDay,
|
|
||||||
followDays = screenModel.followDay,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(successState.manga, screenModel.source) {
|
LaunchedEffect(successState.manga, screenModel.source) {
|
||||||
if (isHttpSource) {
|
if (isHttpSource) {
|
||||||
|
@ -107,7 +100,7 @@ class MangaScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
dateFormat = screenModel.dateFormat,
|
dateFormat = screenModel.dateFormat,
|
||||||
fetchInterval = fetchInterval,
|
fetchInterval = successState.manga.fetchInterval,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||||
|
@ -218,7 +211,7 @@ class MangaScreen(
|
||||||
}
|
}
|
||||||
is MangaScreenModel.Dialog.SetFetchInterval -> {
|
is MangaScreenModel.Dialog.SetFetchInterval -> {
|
||||||
SetIntervalDialog(
|
SetIntervalDialog(
|
||||||
interval = if (dialog.manga.fetchInterval < 0) -dialog.manga.fetchInterval else 0,
|
interval = dialog.manga.fetchInterval,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
|
onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) },
|
||||||
)
|
)
|
||||||
|
|
|
@ -129,8 +129,6 @@ class MangaScreenModel(
|
||||||
private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
private val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
||||||
|
|
||||||
val isUpdateIntervalEnabled = LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateMangaRestriction().get()
|
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 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()
|
||||||
|
@ -361,20 +359,14 @@ class MangaScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFetchInterval(manga: Manga, newInterval: Int) {
|
fun setFetchInterval(manga: Manga, interval: Int) {
|
||||||
val interval = when (newInterval) {
|
|
||||||
// reset interval 0 default to trigger recalculation
|
|
||||||
// only reset if interval is custom, which is negative
|
|
||||||
0 -> if (manga.fetchInterval < 0) 0 else manga.fetchInterval
|
|
||||||
else -> -newInterval
|
|
||||||
}
|
|
||||||
coroutineScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
updateManga.awaitUpdateFetchInterval(
|
updateManga.awaitUpdateFetchInterval(
|
||||||
manga.copy(fetchInterval = interval),
|
// Custom intervals are negative
|
||||||
successState?.chapters?.map { it.chapter }.orEmpty(),
|
manga.copy(fetchInterval = -interval),
|
||||||
)
|
)
|
||||||
val newManga = mangaRepository.getMangaById(mangaId)
|
val updatedManga = mangaRepository.getMangaById(manga.id)
|
||||||
updateSuccessState { it.copy(manga = newManga) }
|
updateSuccessState { it.copy(manga = updatedManga) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1055,10 +1047,3 @@ data class ChapterItem(
|
||||||
) {
|
) {
|
||||||
val isDownloaded = downloadState == Download.State.DOWNLOADED
|
val isDownloaded = downloadState == Download.State.DOWNLOADED
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class FetchInterval(
|
|
||||||
val interval: Int,
|
|
||||||
val leadDays: Int,
|
|
||||||
val followDays: Int,
|
|
||||||
)
|
|
||||||
|
|
|
@ -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 autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false)
|
||||||
|
|
||||||
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
||||||
|
|
|
@ -1,35 +1,34 @@
|
||||||
package tachiyomi.domain.manga.interactor
|
package tachiyomi.domain.manga.interactor
|
||||||
|
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_GRACE_PERIOD = 28
|
const val MAX_FETCH_INTERVAL = 28
|
||||||
|
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
||||||
|
|
||||||
class SetFetchInterval(
|
class SetFetchInterval(
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val getChapterByMangaId: GetChapterByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun update(
|
suspend fun toMangaUpdateOrNull(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
dateTime: ZonedDateTime,
|
||||||
zonedDateTime: ZonedDateTime,
|
window: Pair<Long, Long>,
|
||||||
fetchRange: Pair<Long, Long>,
|
|
||||||
): MangaUpdate? {
|
): MangaUpdate? {
|
||||||
val currentInterval = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
val currentWindow = if (window.first == 0L && window.second == 0L) {
|
||||||
getCurrent(ZonedDateTime.now())
|
getWindow(ZonedDateTime.now())
|
||||||
} else {
|
} else {
|
||||||
fetchRange
|
window
|
||||||
}
|
}
|
||||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
val chapters = getChapterByMangaId.await(manga.id)
|
||||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentInterval)
|
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(chapters, dateTime)
|
||||||
|
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||||
|
|
||||||
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
return if (manga.nextUpdate == nextUpdate && manga.fetchInterval == interval) {
|
||||||
null
|
null
|
||||||
|
@ -38,20 +37,11 @@ class SetFetchInterval(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrent(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||||
var followRange = 0
|
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||||
var leadRange = 0
|
val upperBound = lowerBound.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||||
if (LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateMangaRestriction().get()) {
|
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||||
|
@ -91,35 +81,41 @@ class SetFetchInterval(
|
||||||
// Default to 7 days
|
// Default to 7 days
|
||||||
else -> 7
|
else -> 7
|
||||||
}
|
}
|
||||||
// Min 1, max 28 days
|
|
||||||
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
interval: Int,
|
interval: Int,
|
||||||
zonedDateTime: ZonedDateTime,
|
dateTime: ZonedDateTime,
|
||||||
fetchRange: Pair<Long, Long>,
|
window: Pair<Long, Long>,
|
||||||
): Long {
|
): Long {
|
||||||
return if (
|
return if (
|
||||||
manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
manga.nextUpdate !in window.first.rangeTo(window.second + 1) ||
|
||||||
manga.fetchInterval == 0
|
manga.fetchInterval == 0
|
||||||
) {
|
) {
|
||||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), dateTime.zone)
|
||||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
.toLocalDate()
|
||||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
.atStartOfDay()
|
||||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, dateTime).toInt()
|
||||||
|
val cycle = timeSinceLatest.floorDiv(
|
||||||
|
interval.absoluteValue.takeIf { interval < 0 }
|
||||||
|
?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10),
|
||||||
|
)
|
||||||
|
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(dateTime.offset) * 1000
|
||||||
} else {
|
} else {
|
||||||
manga.nextUpdate
|
manga.nextUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||||
if (delta >= maxValue) return maxValue
|
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
|
||||||
// double delta again if missed more than 9 check in new delta
|
// double delta again if missed more than 9 check in new delta
|
||||||
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
return if (cycle > doubleWhenOver) {
|
return if (cycle > doubleWhenOver) {
|
||||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver)
|
||||||
} else {
|
} else {
|
||||||
delta
|
delta
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.time.ZonedDateTime
|
||||||
|
|
||||||
@Execution(ExecutionMode.CONCURRENT)
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
class SetFetchIntervalTest {
|
class SetFetchIntervalTest {
|
||||||
|
|
||||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||||
private var chapter = Chapter.create().copy(
|
private var chapter = Chapter.create().copy(
|
||||||
dateFetch = testTime.toEpochSecond() * 1000,
|
dateFetch = testTime.toEpochSecond() * 1000,
|
||||||
|
@ -19,14 +20,8 @@ class SetFetchIntervalTest {
|
||||||
|
|
||||||
private val setFetchInterval = SetFetchInterval(mockk())
|
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
|
@Test
|
||||||
fun `calculateInterval returns 7 when 1 chapters in 1 day`() {
|
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
(1..1).forEach {
|
(1..1).forEach {
|
||||||
val duration = Duration.ofHours(10)
|
val duration = Duration.ofHours(10)
|
||||||
|
@ -63,9 +58,8 @@ class SetFetchIntervalTest {
|
||||||
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default 1 if interval less than 1
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 chapters in 75 hours, 3 days`() {
|
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(15L * it)
|
val duration = Duration.ofHours(15L * it)
|
||||||
|
@ -98,9 +92,8 @@ class SetFetchIntervalTest {
|
||||||
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// If interval is decimal, floor to closest integer
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days`() {
|
fun `calculateInterval returns floored value when interval is decimal`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(25L * it)
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
@ -121,9 +114,8 @@ class SetFetchIntervalTest {
|
||||||
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use fetch time if upload time not available
|
|
||||||
@Test
|
@Test
|
||||||
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days of dateFetch`() {
|
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
||||||
val chapters = mutableListOf<Chapter>()
|
val chapters = mutableListOf<Chapter>()
|
||||||
(1..5).forEach {
|
(1..5).forEach {
|
||||||
val duration = Duration.ofHours(25L * it)
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
@ -132,4 +124,9 @@ class SetFetchIntervalTest {
|
||||||
}
|
}
|
||||||
setFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,17 +259,6 @@
|
||||||
<string name="pref_library_update_show_tab_badge">Show unread count on Updates icon</string>
|
<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_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">Automatically refresh metadata</string>
|
||||||
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
|
<string name="pref_library_update_refresh_metadata_summary">Check for new cover and details when updating library</string>
|
||||||
<string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
|
<string name="pref_library_update_refresh_trackers">Automatically refresh trackers</string>
|
||||||
|
@ -637,10 +626,6 @@
|
||||||
<item quantity="one">1 day</item>
|
<item quantity="one">1 day</item>
|
||||||
<item quantity="other">%d days</item>
|
<item quantity="other">%d days</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="range_interval_day">
|
|
||||||
<item quantity="one">%1$d - %2$d day</item>
|
|
||||||
<item quantity="other">%1$d - %2$d days</item>
|
|
||||||
</plurals>
|
|
||||||
|
|
||||||
<!-- Manga info -->
|
<!-- Manga info -->
|
||||||
<plurals name="missing_chapters">
|
<plurals name="missing_chapters">
|
||||||
|
|
Reference in a new issue