diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt index 7a6d8d50b..444af9aec 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreen.kt @@ -78,12 +78,13 @@ import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.isScrolledToEnd import tachiyomi.presentation.core.util.isScrollingUp import tachiyomi.source.local.isLocal +import java.time.Instant @Composable fun MangaScreen( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, - fetchInterval: Int?, + nextUpdate: Instant?, isTabletUi: Boolean, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, @@ -138,7 +139,7 @@ fun MangaScreen( MangaScreenSmallImpl( state = state, snackbarHostState = snackbarHostState, - fetchInterval = fetchInterval, + nextUpdate = nextUpdate, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, onBackClicked = onBackClicked, @@ -175,7 +176,7 @@ fun MangaScreen( snackbarHostState = snackbarHostState, chapterSwipeStartAction = chapterSwipeStartAction, chapterSwipeEndAction = chapterSwipeEndAction, - fetchInterval = fetchInterval, + nextUpdate = nextUpdate, onBackClicked = onBackClicked, onChapterClicked = onChapterClicked, onDownloadChapter = onDownloadChapter, @@ -211,7 +212,7 @@ fun MangaScreen( private fun MangaScreenSmallImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, - fetchInterval: Int?, + nextUpdate: Instant?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, @@ -402,7 +403,7 @@ private fun MangaScreenSmallImpl( MangaActionRow( favorite = state.manga.favorite, trackingCount = state.trackingCount, - fetchInterval = fetchInterval, + nextUpdate = nextUpdate, isUserIntervalMode = state.manga.fetchInterval < 0, onAddToLibraryClicked = onAddToLibraryClicked, onWebViewClicked = onWebViewClicked, @@ -462,7 +463,7 @@ private fun MangaScreenSmallImpl( fun MangaScreenLargeImpl( state: MangaScreenModel.State.Success, snackbarHostState: SnackbarHostState, - fetchInterval: Int?, + nextUpdate: Instant?, chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction, chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction, onBackClicked: () -> Unit, @@ -641,7 +642,7 @@ fun MangaScreenLargeImpl( MangaActionRow( favorite = state.manga.favorite, trackingCount = state.trackingCount, - fetchInterval = fetchInterval, + nextUpdate = nextUpdate, isUserIntervalMode = state.manga.fetchInterval < 0, onAddToLibraryClicked = onAddToLibraryClicked, onWebViewClicked = onWebViewClicked, diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt index f59b4574a..ace822cd1 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaDialogs.kt @@ -2,8 +2,11 @@ package eu.kanade.presentation.manga.components import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height 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 @@ -20,6 +23,7 @@ import kotlinx.collections.immutable.toImmutableList import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.WheelTextPicker +import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource import java.time.Instant @@ -59,57 +63,71 @@ fun DeleteChaptersDialog( @Composable fun SetIntervalDialog( interval: Int, - nextUpdate: Long, + nextUpdate: Instant?, onDismissRequest: () -> Unit, - onValueChanged: (Int) -> Unit, + onValueChanged: ((Int) -> Unit)? = null, ) { var selectedInterval by rememberSaveable { mutableIntStateOf(if (interval < 0) -interval else 0) } val nextUpdateDays = remember(nextUpdate) { - val now = Instant.now() - val nextUpdateInstant = Instant.ofEpochMilli(nextUpdate) - - now.until(nextUpdateInstant, ChronoUnit.DAYS) + return@remember if (nextUpdate != null) { + val now = Instant.now() + now.until(nextUpdate, ChronoUnit.DAYS).toInt() + } else { + null + } } + // TODO: selecting "1" then doesn't allow for future changes unless defaulting first? AlertDialog( onDismissRequest = onDismissRequest, - title = { Text(stringResource(MR.strings.manga_modify_calculated_interval_title)) }, + title = { Text(stringResource(MR.strings.pref_library_update_smart_update)) }, text = { Column { - if (nextUpdateDays >= 0) { + if (nextUpdateDays != null && nextUpdateDays >= 0) { Text( stringResource( MR.strings.manga_interval_expected_update, pluralStringResource( MR.plurals.day, - count = nextUpdateDays.toInt(), + count = nextUpdateDays, nextUpdateDays, ), + pluralStringResource( + MR.plurals.day, + count = interval, + interval, + ), ), ) + + Spacer(Modifier.height(MaterialTheme.padding.small)) } - BoxWithConstraints( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - val size = DpSize(width = maxWidth / 2, height = 128.dp) - val items = (0..FetchInterval.MAX_INTERVAL) - .map { - if (it == 0) { - stringResource(MR.strings.label_default) - } else { - it.toString() + if (onValueChanged != null) { + Text(stringResource(MR.strings.manga_interval_custom_amount)) + + BoxWithConstraints( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + val size = DpSize(width = maxWidth / 2, height = 128.dp) + val items = (0..FetchInterval.MAX_INTERVAL) + .map { + if (it == 0) { + stringResource(MR.strings.label_default) + } else { + it.toString() + } } - } - .toImmutableList() - WheelTextPicker( - items = items, - size = size, - startIndex = selectedInterval, - onSelectionChanged = { selectedInterval = it }, - ) + .toImmutableList() + WheelTextPicker( + items = items, + size = size, + startIndex = selectedInterval, + onSelectionChanged = { selectedInterval = it }, + ) + } } } }, @@ -120,7 +138,7 @@ fun SetIntervalDialog( }, confirmButton = { TextButton(onClick = { - onValueChanged(selectedInterval) + onValueChanged?.invoke(selectedInterval) onDismissRequest() }) { Text(text = stringResource(MR.strings.action_ok)) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 7b5de2467..283f71bbb 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -86,7 +86,8 @@ import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.secondaryItemAlpha -import kotlin.math.absoluteValue +import java.time.Instant +import java.time.temporal.ChronoUnit import kotlin.math.roundToInt private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE)) @@ -165,7 +166,7 @@ fun MangaInfoBox( fun MangaActionRow( favorite: Boolean, trackingCount: Int, - fetchInterval: Int?, + nextUpdate: Instant?, isUserIntervalMode: Boolean, onAddToLibraryClicked: () -> Unit, onWebViewClicked: (() -> Unit)?, @@ -177,6 +178,16 @@ fun MangaActionRow( ) { val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f) + // TODO: show something better when using custom interval + val nextUpdateDays = remember(nextUpdate) { + return@remember if (nextUpdate != null) { + val now = Instant.now() + now.until(nextUpdate, ChronoUnit.DAYS).toInt() + } else { + null + } + } + Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) { MangaActionButton( title = if (favorite) { @@ -189,18 +200,20 @@ fun MangaActionRow( onClick = onAddToLibraryClicked, onLongClick = onEditCategory, ) - if (onEditIntervalClicked != null && fetchInterval != null) { - MangaActionButton( - title = pluralStringResource( + MangaActionButton( + title = if (nextUpdateDays != null) { + pluralStringResource( MR.plurals.day, - count = fetchInterval.absoluteValue, - fetchInterval.absoluteValue, - ), - icon = Icons.Default.HourglassEmpty, - color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, - onClick = onEditIntervalClicked, - ) - } + count = nextUpdateDays, + nextUpdateDays, + ) + } else { + stringResource(MR.strings.not_applicable) + }, + icon = Icons.Default.HourglassEmpty, + color = if (isUserIntervalMode) MaterialTheme.colorScheme.primary else defaultActionButtonColor, + onClick = { onEditIntervalClicked?.invoke() }, + ) MangaActionButton( title = if (trackingCount == 0) { stringResource(MR.strings.manga_tracking_tab) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt index 1ad7410be..346a60b86 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsLibraryScreen.kt @@ -198,7 +198,7 @@ object SettingsLibraryScreen : SearchableSettings { ), Preference.PreferenceItem.MultiSelectListPreference( pref = libraryPreferences.autoUpdateMangaRestrictions(), - title = stringResource(MR.strings.pref_library_update_manga_restriction), + title = stringResource(MR.strings.pref_library_update_smart_update), entries = persistentMapOf( MANGA_HAS_UNREAD to stringResource(MR.strings.pref_update_only_completely_read), MANGA_NON_READ to stringResource(MR.strings.pref_update_only_started), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt index f4c7c48b6..b73b4bcc3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaScreen.kt @@ -104,7 +104,7 @@ class MangaScreen( MangaScreen( state = successState, snackbarHostState = screenModel.snackbarHostState, - fetchInterval = successState.manga.fetchInterval, + nextUpdate = successState.manga.expectedNextUpdate, isTabletUi = isTabletUi(), chapterSwipeStartAction = screenModel.chapterSwipeStartAction, chapterSwipeEndAction = screenModel.chapterSwipeEndAction, @@ -146,7 +146,7 @@ class MangaScreen( onDownloadActionClicked = screenModel::runDownloadAction.takeIf { !successState.source.isLocalOrStub() }, onEditCategoryClicked = screenModel::showChangeCategoryDialog.takeIf { successState.manga.favorite }, onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf { - screenModel.isUpdateIntervalEnabled && successState.manga.favorite + successState.manga.favorite }, onMigrateClicked = { navigator.push(MigrateSearchScreen(successState.manga.id)) @@ -243,9 +243,10 @@ class MangaScreen( is MangaScreenModel.Dialog.SetFetchInterval -> { SetIntervalDialog( interval = dialog.manga.fetchInterval, - nextUpdate = dialog.manga.nextUpdate, + nextUpdate = dialog.manga.expectedNextUpdate, onDismissRequest = onDismissRequest, - onValueChanged = { screenModel.setFetchInterval(dialog.manga, it) }, + onValueChanged = { interval: Int -> screenModel.setFetchInterval(dialog.manga, interval) } + .takeIf { screenModel.isUpdateIntervalEnabled }, ) } } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index f694355a4..2b99c29bf 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -1,8 +1,10 @@ package tachiyomi.domain.manga.model +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.UpdateStrategy import tachiyomi.core.preference.TriState import java.io.Serializable +import java.time.Instant data class Manga( val id: Long, @@ -29,6 +31,11 @@ data class Manga( val favoriteModifiedAt: Long?, ) : Serializable { + val expectedNextUpdate: Instant? + get() = nextUpdate + .takeIf { status != SManga.COMPLETED.toLong() } + ?.let { Instant.ofEpochMilli(it) } + val sorting: Long get() = chapterFlags and CHAPTER_SORTING_MASK diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index e130180c1..df8d826a6 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -275,12 +275,12 @@ When charging Restrictions: %s - Skip updating entries - With unread chapter(s) - With \"Completed\" status - That haven\'t been started + Smart update + Skip entries with unread chapter(s) + Skip entries with \"Completed\" status + Skip unstarted entries + Predict next release time Show unread count on Updates icon - Outside expected release period Automatically refresh metadata Check for new cover and details when updating library @@ -668,9 +668,10 @@ Chapter %1$s Estimate every Set to update every + Next update - Next update expected in around %s - Customize interval + Next update expected in around %1$s, checking around every %2$s + Custom update frequency: Downloading (%1$d/%2$d) Error Paused