Update Manga in Expected Period (#5734)
* Add Predict Interval Test * Get mangas next update and interval in library update * Get next update and interval in backup restore * Display and set intervals, nextUpdate in Manga Info * Move logic function to MangeScreen and InfoHeader Update per suggestion --------- Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
parent
6d69caf59e
commit
cb639f4e90
14 changed files with 460 additions and 131 deletions
|
@ -57,6 +57,7 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
||||||
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
|
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
import tachiyomi.domain.release.service.ReleaseService
|
import tachiyomi.domain.release.service.ReleaseService
|
||||||
|
@ -100,10 +101,11 @@ class DomainModule : InjektModule {
|
||||||
addFactory { GetNextChapters(get(), get(), get()) }
|
addFactory { GetNextChapters(get(), get(), get()) }
|
||||||
addFactory { ResetViewerFlags(get()) }
|
addFactory { ResetViewerFlags(get()) }
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
addFactory { SetMangaChapterFlags(get()) }
|
||||||
|
addFactory { SetMangaUpdateInterval(get()) }
|
||||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||||
addFactory { SetMangaViewerFlags(get()) }
|
addFactory { SetMangaViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
addFactory { NetworkToLocalManga(get()) }
|
||||||
addFactory { UpdateManga(get()) }
|
addFactory { UpdateManga(get(), get()) }
|
||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
|
|
||||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||||
|
|
|
@ -23,6 +23,7 @@ import tachiyomi.source.local.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
|
@ -48,6 +49,9 @@ class SyncChaptersWithSource(
|
||||||
rawSourceChapters: List<SChapter>,
|
rawSourceChapters: List<SChapter>,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: Source,
|
source: Source,
|
||||||
|
manualFetch: Boolean = false,
|
||||||
|
zoneDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
|
fetchRange: Pair<Long, Long> = Pair(0, 0),
|
||||||
): List<Chapter> {
|
): List<Chapter> {
|
||||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||||
throw NoChaptersException()
|
throw NoChaptersException()
|
||||||
|
@ -134,6 +138,14 @@ class SyncChaptersWithSource(
|
||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||||
|
if (manualFetch || manga.calculateInterval == 0 || manga.nextUpdate < fetchRange.first) {
|
||||||
|
updateManga.awaitUpdateFetchInterval(
|
||||||
|
manga,
|
||||||
|
dbChapters,
|
||||||
|
zoneDateTime,
|
||||||
|
fetchRange,
|
||||||
|
)
|
||||||
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +200,8 @@ class SyncChaptersWithSource(
|
||||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
}
|
}
|
||||||
|
val newChapters = chapterRepository.getChapterByMangaId(manga.id)
|
||||||
|
updateManga.awaitUpdateFetchInterval(manga, newChapters, zoneDateTime, fetchRange)
|
||||||
|
|
||||||
// Set this manga as updated since chapters were changed
|
// Set this manga as updated since chapters were changed
|
||||||
// Note that last_update actually represents last time the chapter list changed at all
|
// Note that last_update actually represents last time the chapter list changed at all
|
||||||
|
|
|
@ -4,8 +4,7 @@ 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.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.interactor.getCurrentFetchRange
|
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||||
import tachiyomi.domain.manga.interactor.updateIntervalMeta
|
|
||||||
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 tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
|
@ -17,6 +16,7 @@ import java.util.Date
|
||||||
|
|
||||||
class UpdateManga(
|
class UpdateManga(
|
||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
|
private val setMangaUpdateInterval: SetMangaUpdateInterval,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||||
|
@ -77,16 +77,15 @@ class UpdateManga(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateIntervalMeta(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
fetchRange: Pair<Long, Long> = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val newMeta = updateIntervalMeta(manga, chapters, zonedDateTime, setCurrentFetchRange)
|
val updatedManga = setMangaUpdateInterval.updateInterval(manga, chapters, zonedDateTime, fetchRange)
|
||||||
|
return if (updatedManga != null) {
|
||||||
return if (newMeta != null) {
|
mangaRepository.update(updatedManga)
|
||||||
mangaRepository.update(newMeta)
|
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,7 @@ fun MangaScreen(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
dateRelativeTime: Int,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -112,6 +113,7 @@ fun MangaScreen(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
|
@ -141,6 +143,7 @@ fun MangaScreen(
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
dateRelativeTime = dateRelativeTime,
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
|
@ -160,6 +163,7 @@ fun MangaScreen(
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
onDownloadActionClicked = onDownloadActionClicked,
|
onDownloadActionClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
|
@ -178,6 +182,7 @@ fun MangaScreen(
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
onDownloadChapter = onDownloadChapter,
|
onDownloadChapter = onDownloadChapter,
|
||||||
|
@ -195,6 +200,7 @@ fun MangaScreen(
|
||||||
onShareClicked = onShareClicked,
|
onShareClicked = onShareClicked,
|
||||||
onDownloadActionClicked = onDownloadActionClicked,
|
onDownloadActionClicked = onDownloadActionClicked,
|
||||||
onEditCategoryClicked = onEditCategoryClicked,
|
onEditCategoryClicked = onEditCategoryClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onMigrateClicked = onMigrateClicked,
|
onMigrateClicked = onMigrateClicked,
|
||||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||||
|
@ -214,6 +220,7 @@ private fun MangaScreenSmallImpl(
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
dateRelativeTime: Int,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
@ -240,6 +247,7 @@ private fun MangaScreenSmallImpl(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
|
@ -383,10 +391,13 @@ private fun MangaScreenSmallImpl(
|
||||||
MangaActionRow(
|
MangaActionRow(
|
||||||
favorite = state.manga.favorite,
|
favorite = state.manga.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
|
isUserIntervalMode = state.manga.calculateInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
onWebViewLongClicked = onWebViewLongClicked,
|
onWebViewLongClicked = onWebViewLongClicked,
|
||||||
onTrackingClicked = onTrackingClicked,
|
onTrackingClicked = onTrackingClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onEditCategory = onEditCategoryClicked,
|
onEditCategory = onEditCategoryClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -440,6 +451,7 @@ fun MangaScreenLargeImpl(
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
dateRelativeTime: Int,
|
dateRelativeTime: Int,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
@ -466,6 +478,7 @@ fun MangaScreenLargeImpl(
|
||||||
onShareClicked: (() -> Unit)?,
|
onShareClicked: (() -> Unit)?,
|
||||||
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
onDownloadActionClicked: ((DownloadAction) -> Unit)?,
|
||||||
onEditCategoryClicked: (() -> Unit)?,
|
onEditCategoryClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onMigrateClicked: (() -> Unit)?,
|
onMigrateClicked: (() -> Unit)?,
|
||||||
|
|
||||||
// For bottom action menu
|
// For bottom action menu
|
||||||
|
@ -596,10 +609,13 @@ fun MangaScreenLargeImpl(
|
||||||
MangaActionRow(
|
MangaActionRow(
|
||||||
favorite = state.manga.favorite,
|
favorite = state.manga.favorite,
|
||||||
trackingCount = state.trackingCount,
|
trackingCount = state.trackingCount,
|
||||||
|
intervalDisplay = intervalDisplay,
|
||||||
|
isUserIntervalMode = state.manga.calculateInterval < 0,
|
||||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||||
onWebViewClicked = onWebViewClicked,
|
onWebViewClicked = onWebViewClicked,
|
||||||
onWebViewLongClicked = onWebViewLongClicked,
|
onWebViewLongClicked = onWebViewLongClicked,
|
||||||
onTrackingClicked = onTrackingClicked,
|
onTrackingClicked = onTrackingClicked,
|
||||||
|
onEditIntervalClicked = onEditIntervalClicked,
|
||||||
onEditCategory = onEditCategoryClicked,
|
onEditCategory = onEditCategoryClicked,
|
||||||
)
|
)
|
||||||
ExpandableMangaDescription(
|
ExpandableMangaDescription(
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
package eu.kanade.presentation.manga.components
|
package eu.kanade.presentation.manga.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import tachiyomi.domain.manga.interactor.MAX_GRACE_PERIOD
|
||||||
|
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeleteChaptersDialog(
|
fun DeleteChaptersDialog(
|
||||||
|
@ -37,3 +49,51 @@ fun DeleteChaptersDialog(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SetIntervalDialog(
|
||||||
|
interval: Int,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onValueChanged: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
var intervalValue by rememberSaveable { mutableIntStateOf(interval) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = { Text(text = stringResource(R.string.manga_modify_calculated_interval_title)) },
|
||||||
|
text = {
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
val size = DpSize(width = maxWidth / 2, height = 128.dp)
|
||||||
|
val items = (0..MAX_GRACE_PERIOD).map {
|
||||||
|
if (it == 0) {
|
||||||
|
stringResource(R.string.label_default)
|
||||||
|
} else {
|
||||||
|
it.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WheelTextPicker(
|
||||||
|
size = size,
|
||||||
|
items = items,
|
||||||
|
startIndex = intervalValue,
|
||||||
|
onSelectionChanged = { intervalValue = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onValueChanged(intervalValue)
|
||||||
|
onDismissRequest()
|
||||||
|
},) {
|
||||||
|
Text(text = stringResource(R.string.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Favorite
|
import androidx.compose.material.icons.filled.Favorite
|
||||||
|
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||||
import androidx.compose.material.icons.filled.Warning
|
import androidx.compose.material.icons.filled.Warning
|
||||||
import androidx.compose.material.icons.outlined.AttachMoney
|
import androidx.compose.material.icons.outlined.AttachMoney
|
||||||
import androidx.compose.material.icons.outlined.Block
|
import androidx.compose.material.icons.outlined.Block
|
||||||
|
@ -164,14 +165,19 @@ fun MangaActionRow(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
trackingCount: Int,
|
trackingCount: Int,
|
||||||
|
intervalDisplay: () -> Pair<Int, Int>?,
|
||||||
|
isUserIntervalMode: Boolean,
|
||||||
onAddToLibraryClicked: () -> Unit,
|
onAddToLibraryClicked: () -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
onWebViewLongClicked: (() -> Unit)?,
|
onWebViewLongClicked: (() -> Unit)?,
|
||||||
onTrackingClicked: (() -> Unit)?,
|
onTrackingClicked: (() -> Unit)?,
|
||||||
|
onEditIntervalClicked: (() -> Unit)?,
|
||||||
onEditCategory: (() -> Unit)?,
|
onEditCategory: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
|
val interval: Pair<Int, Int>? = intervalDisplay()
|
||||||
|
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||||
|
|
||||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (favorite) {
|
title = if (favorite) {
|
||||||
stringResource(R.string.in_library)
|
stringResource(R.string.in_library)
|
||||||
|
@ -183,6 +189,19 @@ fun MangaActionRow(
|
||||||
onClick = onAddToLibraryClicked,
|
onClick = onAddToLibraryClicked,
|
||||||
onLongClick = onEditCategory,
|
onLongClick = onEditCategory,
|
||||||
)
|
)
|
||||||
|
if (onEditIntervalClicked != null && interval != null) {
|
||||||
|
MangaActionButton(
|
||||||
|
title =
|
||||||
|
if (interval.first == interval.second) {
|
||||||
|
pluralStringResource(id = R.plurals.day, count = interval.second, interval.second)
|
||||||
|
} else {
|
||||||
|
pluralStringResource(id = R.plurals.range_interval_day, count = interval.second, interval.first, interval.second)
|
||||||
|
},
|
||||||
|
icon = Icons.Default.HourglassEmpty,
|
||||||
|
color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||||
|
onClick = onEditIntervalClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
if (onTrackingClicked != null) {
|
if (onTrackingClicked != null) {
|
||||||
MangaActionButton(
|
MangaActionButton(
|
||||||
title = if (trackingCount == 0) {
|
title = if (trackingCount == 0) {
|
||||||
|
|
|
@ -241,18 +241,15 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
title = stringResource(R.string.pref_library_update_refresh_trackers),
|
title = stringResource(R.string.pref_library_update_refresh_trackers),
|
||||||
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
subtitle = stringResource(R.string.pref_library_update_refresh_trackers_summary),
|
||||||
),
|
),
|
||||||
// TODO: remove isDevFlavor checks once functionality is available
|
|
||||||
Preference.PreferenceItem.MultiSelectListPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
pref = libraryUpdateMangaRestrictionPref,
|
pref = libraryUpdateMangaRestrictionPref,
|
||||||
title = stringResource(R.string.pref_library_update_manga_restriction),
|
title = stringResource(R.string.pref_library_update_manga_restriction),
|
||||||
entries = buildMap {
|
entries = mapOf(
|
||||||
put(MANGA_HAS_UNREAD, stringResource(R.string.pref_update_only_completely_read))
|
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
|
||||||
put(MANGA_NON_READ, stringResource(R.string.pref_update_only_started))
|
MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
|
||||||
put(MANGA_NON_COMPLETED, stringResource(R.string.pref_update_only_non_completed))
|
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
|
||||||
if (isDevFlavor) {
|
MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
|
||||||
put(MANGA_OUTSIDE_RELEASE_PERIOD, stringResource(R.string.pref_update_only_in_release_period))
|
),
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(R.string.pref_update_release_grace_period),
|
title = stringResource(R.string.pref_update_release_grace_period),
|
||||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
|
@ -12,10 +13,15 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
|
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -23,6 +29,12 @@ class BackupRestorer(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val notifier: BackupNotifier,
|
private val notifier: BackupNotifier,
|
||||||
) {
|
) {
|
||||||
|
private val updateManga: UpdateManga = Injekt.get()
|
||||||
|
private val chapterRepository: ChapterRepository = Injekt.get()
|
||||||
|
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
|
||||||
|
|
||||||
|
private var zonedDateTime = ZonedDateTime.now()
|
||||||
|
private var currentRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||||
|
|
||||||
private var backupManager = BackupManager(context)
|
private var backupManager = BackupManager(context)
|
||||||
|
|
||||||
|
@ -90,6 +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()
|
||||||
|
currentRange = setMangaUpdateInterval.getCurrentFetchRange(zonedDateTime)
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
|
@ -122,7 +136,7 @@ class BackupRestorer(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
||||||
if (dbManga == null) {
|
val restoredManga = if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,6 +146,8 @@ 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, updatedChapters, zonedDateTime, currentRange)
|
||||||
} 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}")
|
||||||
|
@ -159,10 +175,11 @@ class BackupRestorer(
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
): Manga {
|
||||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
val fetchedManga = backupManager.restoreNewManga(manga)
|
||||||
backupManager.restoreChapters(fetchedManga, chapters)
|
backupManager.restoreChapters(fetchedManga, chapters)
|
||||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||||
|
return fetchedManga
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreNewManga(
|
private suspend fun restoreNewManga(
|
||||||
|
@ -172,9 +189,10 @@ class BackupRestorer(
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
) {
|
): Manga {
|
||||||
backupManager.restoreChapters(backupManga, chapters)
|
backupManager.restoreChapters(backupManga, chapters)
|
||||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||||
|
return backupManga
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||||
|
|
|
@ -66,8 +66,10 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.DEVICE_ONLY
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_UNREAD
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_HAS_UNREAD
|
||||||
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.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
import tachiyomi.domain.manga.interactor.GetManga
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.manga.interactor.SetMangaUpdateInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.toMangaUpdate
|
import tachiyomi.domain.manga.model.toMangaUpdate
|
||||||
import tachiyomi.domain.source.model.SourceNotInstalledException
|
import tachiyomi.domain.source.model.SourceNotInstalledException
|
||||||
|
@ -77,6 +79,7 @@ import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -101,6 +104,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
private val getTracks: GetTracks = Injekt.get()
|
private val getTracks: GetTracks = Injekt.get()
|
||||||
private val insertTrack: InsertTrack = Injekt.get()
|
private val insertTrack: InsertTrack = Injekt.get()
|
||||||
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
|
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get()
|
||||||
|
private val setMangaUpdateInterval: SetMangaUpdateInterval = Injekt.get()
|
||||||
|
|
||||||
private val notifier = LibraryUpdateNotifier(context)
|
private val notifier = LibraryUpdateNotifier(context)
|
||||||
|
|
||||||
|
@ -227,6 +231,10 @@ 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 fetchRange = setMangaUpdateInterval.getCurrentFetchRange(now)
|
||||||
|
val higherLimit = fetchRange.second
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
mangaToUpdate.groupBy { it.manga.source }.values
|
mangaToUpdate.groupBy { it.manga.source }.values
|
||||||
.map { mangaInSource ->
|
.map { mangaInSource ->
|
||||||
|
@ -247,6 +255,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
manga,
|
manga,
|
||||||
) {
|
) {
|
||||||
when {
|
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_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))
|
||||||
|
|
||||||
|
@ -261,7 +272,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
try {
|
try {
|
||||||
val newChapters = updateManga(manga)
|
val newChapters = updateManga(manga, now, fetchRange)
|
||||||
.sortedByDescending { it.sourceOrder }
|
.sortedByDescending { it.sourceOrder }
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
if (newChapters.isNotEmpty()) {
|
||||||
|
@ -333,7 +344,7 @@ class 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): List<Chapter> {
|
private suspend fun updateManga(manga: Manga, zoneDateTime: ZonedDateTime, fetchRange: Pair<Long, Long>): List<Chapter> {
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
val source = sourceManager.getOrStub(manga.source)
|
||||||
|
|
||||||
// Update manga metadata if needed
|
// Update manga metadata if needed
|
||||||
|
@ -348,7 +359,7 @@ class 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)
|
return syncChaptersWithSource.await(chapters, dbManga, source, false, zoneDateTime, fetchRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers() {
|
private suspend fun updateCovers() {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import eu.kanade.presentation.manga.EditCoverAction
|
||||||
import eu.kanade.presentation.manga.MangaScreen
|
import eu.kanade.presentation.manga.MangaScreen
|
||||||
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
|
import eu.kanade.presentation.manga.components.DeleteChaptersDialog
|
||||||
import eu.kanade.presentation.manga.components.MangaCoverDialog
|
import eu.kanade.presentation.manga.components.MangaCoverDialog
|
||||||
|
import eu.kanade.presentation.manga.components.SetIntervalDialog
|
||||||
import eu.kanade.presentation.util.AssistContentScreen
|
import eu.kanade.presentation.util.AssistContentScreen
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.presentation.util.isTabletUi
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
|
@ -53,6 +54,7 @@ import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
|
@ -100,6 +102,7 @@ class MangaScreen(
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
dateRelativeTime = screenModel.relativeTime,
|
dateRelativeTime = screenModel.relativeTime,
|
||||||
dateFormat = screenModel.dateFormat,
|
dateFormat = screenModel.dateFormat,
|
||||||
|
intervalDisplay = screenModel::intervalDisplay,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
chapterSwipeStartAction = screenModel.chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
chapterSwipeEndAction = screenModel.chapterSwipeEndAction,
|
||||||
|
@ -121,7 +124,8 @@ class MangaScreen(
|
||||||
onCoverClicked = screenModel::showCoverDialog,
|
onCoverClicked = screenModel::showCoverDialog,
|
||||||
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
onShareClicked = { shareManga(context, screenModel.manga, screenModel.source) }.takeIf { isHttpSource },
|
||||||
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() },
|
||||||
onEditCategoryClicked = screenModel::promptChangeCategories.takeIf { successState.manga.favorite },
|
onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite },
|
||||||
|
onEditIntervalClicked = screenModel::showSetMangaIntervalDialog.takeIf { MANGA_OUTSIDE_RELEASE_PERIOD in screenModel.libraryPreferences.libraryUpdateMangaRestriction().get() && successState.manga.favorite },
|
||||||
onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
|
onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) }.takeIf { successState.manga.favorite },
|
||||||
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
onMultiBookmarkClicked = screenModel::bookmarkChapters,
|
||||||
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
|
onMultiMarkAsReadClicked = screenModel::markChaptersRead,
|
||||||
|
@ -207,6 +211,13 @@ class MangaScreen(
|
||||||
LoadingScreen(Modifier.systemBarsPadding())
|
LoadingScreen(Modifier.systemBarsPadding())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is MangaScreenModel.Dialog.SetMangaInterval -> {
|
||||||
|
SetIntervalDialog(
|
||||||
|
interval = if (dialog.manga.calculateInterval < 0) -dialog.manga.calculateInterval else 0,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onValueChanged = { screenModel.setFetchRangeInterval(dialog.manga, it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,20 +69,22 @@ import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
||||||
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.applyFilter
|
import tachiyomi.domain.manga.model.applyFilter
|
||||||
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
class MangaScreenModel(
|
class MangaScreenModel(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val mangaId: Long,
|
val mangaId: Long,
|
||||||
private val isFromSource: Boolean,
|
private val isFromSource: Boolean,
|
||||||
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
readerPreferences: ReaderPreferences = Injekt.get(),
|
val readerPreferences: ReaderPreferences = Injekt.get(),
|
||||||
uiPreferences: UiPreferences = Injekt.get(),
|
val uiPreferences: UiPreferences = Injekt.get(),
|
||||||
private val trackManager: TrackManager = Injekt.get(),
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
private val downloadCache: DownloadCache = Injekt.get(),
|
private val downloadCache: DownloadCache = Injekt.get(),
|
||||||
|
@ -97,6 +99,7 @@ class MangaScreenModel(
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
private val getTracks: GetTracks = Injekt.get(),
|
private val getTracks: GetTracks = Injekt.get(),
|
||||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||||
|
private val mangaRepository: MangaRepository = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
|
) : StateScreenModel<MangaScreenModel.State>(State.Loading) {
|
||||||
|
|
||||||
|
@ -307,7 +310,7 @@ class MangaScreenModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose a category
|
// Choose a category
|
||||||
else -> promptChangeCategories()
|
else -> showChangeCategoryDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally match with enhanced tracking when available
|
// Finally match with enhanced tracking when available
|
||||||
|
@ -333,7 +336,7 @@ class MangaScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun promptChangeCategories() {
|
fun showChangeCategoryDialog() {
|
||||||
val manga = successState?.manga ?: return
|
val manga = successState?.manga ?: return
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val categories = getCategories()
|
val categories = getCategories()
|
||||||
|
@ -349,6 +352,39 @@ class MangaScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showSetMangaIntervalDialog() {
|
||||||
|
val manga = successState?.manga ?: return
|
||||||
|
updateSuccessState {
|
||||||
|
it.copy(dialog = Dialog.SetMangaInterval(manga))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this should be in the state/composables
|
||||||
|
fun intervalDisplay(): Pair<Int, Int>? {
|
||||||
|
val state = successState ?: return null
|
||||||
|
val leadDay = libraryPreferences.leadingExpectedDays().get()
|
||||||
|
val followDay = libraryPreferences.followingExpectedDays().get()
|
||||||
|
val effInterval = state.manga.calculateInterval
|
||||||
|
return 1.coerceAtLeast(effInterval.absoluteValue - leadDay) to (effInterval.absoluteValue + followDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFetchRangeInterval(manga: Manga, newInterval: Int) {
|
||||||
|
val interval = when (newInterval) {
|
||||||
|
// reset interval 0 default to trigger recalculation
|
||||||
|
// only reset if interval is custom, which is negative
|
||||||
|
0 -> if (manga.calculateInterval < 0) 0 else manga.calculateInterval
|
||||||
|
else -> -newInterval
|
||||||
|
}
|
||||||
|
coroutineScope.launchIO {
|
||||||
|
updateManga.awaitUpdateFetchInterval(
|
||||||
|
manga.copy(calculateInterval = interval),
|
||||||
|
successState?.chapters?.map { it.chapter }.orEmpty(),
|
||||||
|
)
|
||||||
|
val newManga = mangaRepository.getMangaById(mangaId)
|
||||||
|
updateSuccessState { it.copy(manga = newManga) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the manga has any downloads.
|
* Returns true if the manga has any downloads.
|
||||||
*/
|
*/
|
||||||
|
@ -502,6 +538,7 @@ class MangaScreenModel(
|
||||||
chapters,
|
chapters,
|
||||||
state.manga,
|
state.manga,
|
||||||
state.source,
|
state.source,
|
||||||
|
manualFetch,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (manualFetch) {
|
if (manualFetch) {
|
||||||
|
@ -519,6 +556,8 @@ class MangaScreenModel(
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
snackbarHostState.showSnackbar(message = message)
|
snackbarHostState.showSnackbar(message = message)
|
||||||
}
|
}
|
||||||
|
val newManga = mangaRepository.getMangaById(mangaId)
|
||||||
|
updateSuccessState { it.copy(manga = newManga, isRefreshingData = false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -943,6 +982,7 @@ class MangaScreenModel(
|
||||||
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
|
||||||
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
|
||||||
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
|
||||||
|
data class SetMangaInterval(val manga: Manga) : Dialog
|
||||||
data object SettingsSheet : Dialog
|
data object SettingsSheet : Dialog
|
||||||
data object TrackSheet : Dialog
|
data object TrackSheet : Dialog
|
||||||
data object FullCover : Dialog
|
data object FullCover : Dialog
|
||||||
|
|
|
@ -13,111 +13,115 @@ import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_GRACE_PERIOD = 28
|
const val MAX_GRACE_PERIOD = 28
|
||||||
|
|
||||||
fun updateIntervalMeta(
|
class SetMangaUpdateInterval(
|
||||||
manga: Manga,
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
chapters: List<Chapter>,
|
) {
|
||||||
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
|
|
||||||
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
|
|
||||||
): MangaUpdate? {
|
|
||||||
val currentFetchRange = if (setCurrentFetchRange.first == 0L && setCurrentFetchRange.second == 0L) {
|
|
||||||
getCurrentFetchRange(ZonedDateTime.now())
|
|
||||||
} else {
|
|
||||||
setCurrentFetchRange
|
|
||||||
}
|
|
||||||
val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
|
||||||
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
|
|
||||||
|
|
||||||
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
|
fun updateInterval(
|
||||||
null
|
manga: Manga,
|
||||||
} else { MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval) }
|
chapters: List<Chapter>,
|
||||||
}
|
zonedDateTime: ZonedDateTime,
|
||||||
|
fetchRange: Pair<Long, Long>,
|
||||||
fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
): MangaUpdate? {
|
||||||
val sortedChapters = chapters
|
val currentFetchRange = if (fetchRange.first == 0L && fetchRange.second == 0L) {
|
||||||
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
getCurrentFetchRange(ZonedDateTime.now())
|
||||||
.take(50)
|
} else {
|
||||||
|
fetchRange
|
||||||
val uploadDates = sortedChapters
|
|
||||||
.filter { it.dateUpload > 0L }
|
|
||||||
.map {
|
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
|
||||||
.toLocalDate()
|
|
||||||
.atStartOfDay()
|
|
||||||
}
|
}
|
||||||
.distinct()
|
val interval = manga.calculateInterval.takeIf { it < 0 } ?: calculateInterval(chapters, zonedDateTime)
|
||||||
val fetchDates = sortedChapters
|
val nextUpdate = calculateNextUpdate(manga, interval, zonedDateTime, currentFetchRange)
|
||||||
.map {
|
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
return if (manga.nextUpdate == nextUpdate && manga.calculateInterval == interval) {
|
||||||
.toLocalDate()
|
null
|
||||||
.atStartOfDay()
|
} else {
|
||||||
|
MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval)
|
||||||
}
|
}
|
||||||
.distinct()
|
}
|
||||||
|
|
||||||
val newInterval = when {
|
fun getCurrentFetchRange(timeToCal: ZonedDateTime): Pair<Long, Long> {
|
||||||
// Enough upload date from source
|
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
||||||
uploadDates.size >= 3 -> {
|
var followRange = 0
|
||||||
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
var leadRange = 0
|
||||||
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
if (LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in libraryPreferences.libraryUpdateMangaRestriction().get()) {
|
||||||
uploadDelta.floorDiv(uploadPeriod).toInt()
|
followRange = libraryPreferences.followingExpectedDays().get()
|
||||||
|
leadRange = libraryPreferences.leadingExpectedDays().get()
|
||||||
}
|
}
|
||||||
// Enough fetch date from client
|
val startToday = timeToCal.toLocalDate().atStartOfDay(timeToCal.zone)
|
||||||
fetchDates.size >= 3 -> {
|
// revert math of (next_update + follow < now) become (next_update < now - follow)
|
||||||
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
// so (now - follow) become lower limit
|
||||||
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
val lowerRange = startToday.minusDays(followRange.toLong())
|
||||||
fetchDelta.floorDiv(uploadPeriod).toInt()
|
val higherRange = startToday.plusDays(leadRange.toLong())
|
||||||
|
return Pair(lowerRange.toEpochSecond() * 1000, higherRange.toEpochSecond() * 1000 - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||||
|
val sortedChapters = chapters
|
||||||
|
.sortedWith(compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch })
|
||||||
|
.take(50)
|
||||||
|
|
||||||
|
val uploadDates = sortedChapters
|
||||||
|
.filter { it.dateUpload > 0L }
|
||||||
|
.map {
|
||||||
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||||
|
.toLocalDate()
|
||||||
|
.atStartOfDay()
|
||||||
|
}
|
||||||
|
.distinct()
|
||||||
|
val fetchDates = sortedChapters
|
||||||
|
.map {
|
||||||
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||||
|
.toLocalDate()
|
||||||
|
.atStartOfDay()
|
||||||
|
}
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
val interval = when {
|
||||||
|
// Enough upload date from source
|
||||||
|
uploadDates.size >= 3 -> {
|
||||||
|
val uploadDelta = uploadDates.last().until(uploadDates.first(), ChronoUnit.DAYS)
|
||||||
|
val uploadPeriod = uploadDates.indexOf(uploadDates.last())
|
||||||
|
uploadDelta.floorDiv(uploadPeriod).toInt()
|
||||||
|
}
|
||||||
|
// Enough fetch date from client
|
||||||
|
fetchDates.size >= 3 -> {
|
||||||
|
val fetchDelta = fetchDates.last().until(fetchDates.first(), ChronoUnit.DAYS)
|
||||||
|
val uploadPeriod = fetchDates.indexOf(fetchDates.last())
|
||||||
|
fetchDelta.floorDiv(uploadPeriod).toInt()
|
||||||
|
}
|
||||||
|
// Default to 7 days
|
||||||
|
else -> 7
|
||||||
}
|
}
|
||||||
// Default to 7 days
|
// Min 1, max 28 days
|
||||||
else -> 7
|
return interval.coerceIn(1, MAX_GRACE_PERIOD)
|
||||||
}
|
}
|
||||||
// Min 1, max 28 days
|
|
||||||
return newInterval.coerceIn(1, MAX_GRACE_PERIOD)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
interval: Int,
|
interval: Int,
|
||||||
zonedDateTime: ZonedDateTime,
|
zonedDateTime: ZonedDateTime,
|
||||||
currentFetchRange: Pair<Long, Long>,
|
fetchRange: Pair<Long, Long>,
|
||||||
): Long {
|
): Long {
|
||||||
return if (manga.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.second + 1) ||
|
return if (
|
||||||
manga.calculateInterval == 0
|
manga.nextUpdate !in fetchRange.first.rangeTo(fetchRange.second + 1) ||
|
||||||
) {
|
manga.calculateInterval == 0
|
||||||
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
) {
|
||||||
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
val latestDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(manga.lastUpdate), zonedDateTime.zone).toLocalDate().atStartOfDay()
|
||||||
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
val timeSinceLatest = ChronoUnit.DAYS.between(latestDate, zonedDateTime).toInt()
|
||||||
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
val cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
|
||||||
} else {
|
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
|
||||||
manga.nextUpdate
|
} else {
|
||||||
|
manga.nextUpdate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
||||||
|
if (delta >= maxValue) return maxValue
|
||||||
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
|
// double delta again if missed more than 9 check in new delta
|
||||||
|
return if (cycle > doubleWhenOver) {
|
||||||
|
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
||||||
|
} else {
|
||||||
|
delta
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int, maxValue: Int): Int {
|
|
||||||
if (delta >= maxValue) return maxValue
|
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
|
||||||
// double delta again if missed more than 9 check in new delta
|
|
||||||
return if (cycle > doubleWhenOver) {
|
|
||||||
doubleInterval(delta * 2, timeSinceLatest, doubleWhenOver, maxValue)
|
|
||||||
} else {
|
|
||||||
delta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentFetchRange(
|
|
||||||
timeToCal: ZonedDateTime,
|
|
||||||
): Pair<Long, Long> {
|
|
||||||
val preferences: LibraryPreferences = Injekt.get()
|
|
||||||
|
|
||||||
// lead range and the following range depend on if updateOnlyExpectedPeriod set.
|
|
||||||
var followRange = 0
|
|
||||||
var leadRange = 0
|
|
||||||
if (LibraryPreferences.MANGA_OUTSIDE_RELEASE_PERIOD in preferences.libraryUpdateMangaRestriction().get()) {
|
|
||||||
followRange = preferences.followingExpectedDays().get()
|
|
||||||
leadRange = preferences.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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
package tachiyomi.domain.manga.interactor
|
||||||
|
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.parallel.Execution
|
||||||
|
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||||
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
|
@Execution(ExecutionMode.CONCURRENT)
|
||||||
|
class SetMangaUpdateIntervalTest {
|
||||||
|
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||||
|
private var chapter = Chapter.create().copy(
|
||||||
|
dateFetch = testTime.toEpochSecond() * 1000,
|
||||||
|
dateUpload = testTime.toEpochSecond() * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val setMangaUpdateInterval = SetMangaUpdateInterval(mockk())
|
||||||
|
|
||||||
|
private fun chapterAddTime(chapter: Chapter, duration: Duration): Chapter {
|
||||||
|
val newTime = testTime.plus(duration).toEpochSecond() * 1000
|
||||||
|
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default 7 when less than 3 distinct day
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 1 chapters in 1 day`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..1).forEach {
|
||||||
|
val duration = Duration.ofHours(10)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(10)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..2).forEach {
|
||||||
|
val duration = Duration.ofHours(24L)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(48L)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default 1 if interval less than 1
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 75 hours, 3 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(15L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal interval calculation
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(24L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(48L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// If interval is decimal, floor to closest integer
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(43L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use fetch time if upload time not available
|
||||||
|
@Test
|
||||||
|
fun `calculateInterval returns 1 when 5 chapters in 125 hours, 5 days of dateFetch`() {
|
||||||
|
val chapters = mutableListOf<Chapter>()
|
||||||
|
(1..5).forEach {
|
||||||
|
val duration = Duration.ofHours(25L * it)
|
||||||
|
val newChapter = chapterAddTime(chapter, duration).copy(dateUpload = 0L)
|
||||||
|
chapters.add(newChapter)
|
||||||
|
}
|
||||||
|
setMangaUpdateInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -641,6 +641,10 @@
|
||||||
<item quantity="one">1 day</item>
|
<item quantity="one">1 day</item>
|
||||||
<item quantity="other">%d days</item>
|
<item quantity="other">%d days</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="range_interval_day">
|
||||||
|
<item quantity="one">%1$d - %2$d day</item>
|
||||||
|
<item quantity="other">%1$d - %2$d days</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<!-- Manga info -->
|
<!-- Manga info -->
|
||||||
<plurals name="missing_chapters">
|
<plurals name="missing_chapters">
|
||||||
|
@ -682,8 +686,7 @@
|
||||||
<string name="display_mode_chapter">Chapter %1$s</string>
|
<string name="display_mode_chapter">Chapter %1$s</string>
|
||||||
<string name="manga_display_interval_title">Estimate every</string>
|
<string name="manga_display_interval_title">Estimate every</string>
|
||||||
<string name="manga_display_modified_interval_title">Set to update every</string>
|
<string name="manga_display_modified_interval_title">Set to update every</string>
|
||||||
<string name="manga_modify_interval_title">Modify interval</string>
|
<string name="manga_modify_calculated_interval_title">Customize interval</string>
|
||||||
<string name="manga_modify_calculated_interval_title">Customize Interval</string>
|
|
||||||
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
|
||||||
<string name="chapter_error">Error</string>
|
<string name="chapter_error">Error</string>
|
||||||
<string name="chapter_paused">Paused</string>
|
<string name="chapter_paused">Paused</string>
|
||||||
|
|
Reference in a new issue