Add setting and calculate for update interval (#9399)

* Add Grace Period value and settings

* Add functions to calculate nextUpdate

* update per review

* Move more into SetMangaUpdateInterval, keep wrapper
This commit is contained in:
Quang Kieu 2023-05-27 23:01:36 -04:00 committed by GitHub
parent a458bd9fdb
commit c90f344910
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 302 additions and 5 deletions

View file

@ -3,12 +3,16 @@ 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.getCurrentFetchRange
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
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 java.time.ZonedDateTime
import java.util.Date import java.util.Date
class UpdateManga( class UpdateManga(
@ -73,6 +77,21 @@ class UpdateManga(
) )
} }
suspend fun awaitUpdateIntervalMeta(
manga: Manga,
chapters: List<Chapter>,
zonedDateTime: ZonedDateTime = ZonedDateTime.now(),
setCurrentFetchRange: Pair<Long, Long> = getCurrentFetchRange(zonedDateTime),
): Boolean {
val newMeta = updateIntervalMeta(manga, chapters, zonedDateTime, setCurrentFetchRange)
return if (newMeta != null) {
mangaRepository.update(newMeta)
} else {
true
}
}
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean { suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time)) return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
} }

View file

@ -1,6 +1,13 @@
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.BoxWithConstraints
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
@ -10,9 +17,14 @@ 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
@ -39,6 +51,9 @@ 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.presentation.core.components.WheelPickerDefaults
import tachiyomi.presentation.core.components.WheelTextPicker
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -132,8 +147,8 @@ object SettingsLibraryScreen : SearchableSettings {
val included by libraryUpdateCategoriesPref.collectAsState() val included by libraryUpdateCategoriesPref.collectAsState()
val excluded by libraryUpdateCategoriesExcludePref.collectAsState() val excluded by libraryUpdateCategoriesExcludePref.collectAsState()
var showDialog by rememberSaveable { mutableStateOf(false) } var showCategoriesDialog by rememberSaveable { mutableStateOf(false) }
if (showDialog) { if (showCategoriesDialog) {
TriStateListDialog( TriStateListDialog(
title = stringResource(R.string.categories), title = stringResource(R.string.categories),
message = stringResource(R.string.pref_library_update_categories_details), message = stringResource(R.string.pref_library_update_categories_details),
@ -141,11 +156,27 @@ object SettingsLibraryScreen : SearchableSettings {
initialChecked = included.mapNotNull { id -> allCategories.find { it.id.toString() == id } }, initialChecked = included.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } }, initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
itemLabel = { it.visualName }, itemLabel = { it.visualName },
onDismissRequest = { showDialog = false }, onDismissRequest = { showCategoriesDialog = false },
onValueChanged = { newIncluded, newExcluded -> onValueChanged = { newIncluded, newExcluded ->
libraryUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet()) libraryUpdateCategoriesPref.set(newIncluded.map { it.id.toString() }.toSet())
libraryUpdateCategoriesExcludePref.set(newExcluded.map { it.id.toString() }.toSet()) libraryUpdateCategoriesExcludePref.set(newExcluded.map { it.id.toString() }.toSet())
showDialog = false showCategoriesDialog = false
},
)
}
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
}, },
) )
} }
@ -192,8 +223,27 @@ object SettingsLibraryScreen : SearchableSettings {
MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read), MANGA_HAS_UNREAD to stringResource(R.string.pref_update_only_completely_read),
MANGA_NON_READ to stringResource(R.string.pref_update_only_started), MANGA_NON_READ to stringResource(R.string.pref_update_only_started),
MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed), MANGA_NON_COMPLETED to stringResource(R.string.pref_update_only_non_completed),
MANGA_OUTSIDE_RELEASE_PERIOD to stringResource(R.string.pref_update_only_in_release_period),
), ),
), ),
Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_library_update_manga_restriction),
subtitle = setOf(
stringResource(R.string.pref_update_release_leading_days, leadRange),
stringResource(R.string.pref_update_release_following_days, followRange),
)
.joinToString(";"),
onClick = { showFetchRangesDialog = true },
),
Preference.PreferenceItem.InfoPreference(
title = stringResource(R.string.pref_update_release_grace_period_info1),
),
Preference.PreferenceItem.InfoPreference(
title = stringResource(R.string.pref_update_release_grace_period_info2),
),
Preference.PreferenceItem.InfoPreference(
title = stringResource(R.string.pref_update_release_grace_period_info3),
),
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.categories), title = stringResource(R.string.categories),
subtitle = getCategoriesLabel( subtitle = getCategoriesLabel(
@ -201,7 +251,7 @@ object SettingsLibraryScreen : SearchableSettings {
included = included, included = included,
excluded = excluded, excluded = excluded,
), ),
onClick = { showDialog = true }, onClick = { showCategoriesDialog = true },
), ),
Preference.PreferenceItem.SwitchPreference( Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.autoUpdateMetadata(), pref = libraryPreferences.autoUpdateMetadata(),
@ -248,4 +298,82 @@ object SettingsLibraryScreen : SearchableSettings {
), ),
) )
} }
@Composable
private fun LibraryExpectedRangeDialog(
initialLead: Int,
initialFollow: Int,
onDismissRequest: () -> Unit,
onValueChanged: (portrait: Int, landscape: Int) -> Unit,
) {
val context = LocalContext.current
var leadValue by rememberSaveable { mutableStateOf(initialLead) }
var followValue by rememberSaveable { mutableStateOf(initialFollow) }
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(R.string.pref_update_release_grace_period)) },
text = {
Row {
Text(
modifier = Modifier.weight(1f),
text = stringResource(R.string.pref_update_release_leading_days, "x"),
textAlign = TextAlign.Center,
maxLines = 1,
style = MaterialTheme.typography.labelMedium,
)
Text(
modifier = Modifier.weight(1f),
text = stringResource(R.string.pref_update_release_following_days, "x"),
textAlign = TextAlign.Center,
maxLines = 1,
style = MaterialTheme.typography.labelMedium,
)
}
BoxWithConstraints(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
WheelPickerDefaults.Background(size = DpSize(maxWidth, maxHeight))
val size = DpSize(width = maxWidth / 2, height = 128.dp)
val items = (0..28).map {
if (it == 0) {
stringResource(R.string.label_default)
} else {
it.toString()
}
}
Row {
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(android.R.string.ok))
}
},
)
}
} }

View file

@ -34,9 +34,13 @@ class LibraryPreferences(
MANGA_HAS_UNREAD, MANGA_HAS_UNREAD,
MANGA_NON_COMPLETED, MANGA_NON_COMPLETED,
MANGA_NON_READ, MANGA_NON_READ,
MANGA_OUTSIDE_RELEASE_PERIOD,
), ),
) )
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)
@ -55,6 +59,16 @@ class LibraryPreferences(
fun filterCompleted() = preferenceStore.getEnum("pref_filter_library_completed_v2", TriStateFilter.DISABLED) fun filterCompleted() = preferenceStore.getEnum("pref_filter_library_completed_v2", TriStateFilter.DISABLED)
fun filterIntervalCustom() = preferenceStore.getEnum("pref_filter_library_interval_custom", TriStateFilter.DISABLED)
fun filterIntervalLong() = preferenceStore.getEnum("pref_filter_library_interval_long", TriStateFilter.DISABLED)
fun filterIntervalLate() = preferenceStore.getEnum("pref_filter_library_interval_late", TriStateFilter.DISABLED)
fun filterIntervalDropped() = preferenceStore.getEnum("pref_filter_library_interval_dropped", TriStateFilter.DISABLED)
fun filterIntervalPassed() = preferenceStore.getEnum("pref_filter_library_interval_passed", TriStateFilter.DISABLED)
fun filterTracking(id: Int) = preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriStateFilter.DISABLED) fun filterTracking(id: Int) = preferenceStore.getEnum("pref_filter_library_tracked_${id}_v2", TriStateFilter.DISABLED)
// endregion // endregion
@ -142,5 +156,6 @@ class LibraryPreferences(
const val MANGA_NON_COMPLETED = "manga_ongoing" const val MANGA_NON_COMPLETED = "manga_ongoing"
const val MANGA_HAS_UNREAD = "manga_fully_read" const val MANGA_HAS_UNREAD = "manga_fully_read"
const val MANGA_NON_READ = "manga_started" const val MANGA_NON_READ = "manga_started"
const val MANGA_OUTSIDE_RELEASE_PERIOD = "manga_outside_release_period"
} }
} }

View file

@ -0,0 +1,108 @@
package tachiyomi.domain.manga.interactor
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.Instant
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import kotlin.math.absoluteValue
fun updateIntervalMeta(
manga: Manga,
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) {
null
} else { MangaUpdate(id = manga.id, nextUpdate = nextUpdate, calculateInterval = interval) }
}
fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
val sortChapters =
chapters.sortedWith(compareBy<Chapter> { it.dateUpload }.thenBy { it.dateFetch })
.reversed().take(50)
val uploadDates = sortChapters.filter { it.dateUpload != 0L }.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone).toLocalDate()
.atStartOfDay()
}
val uploadDateDistinct = uploadDates.distinctBy { it }
val fetchDates = sortChapters.map {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone).toLocalDate()
.atStartOfDay()
}
val fetchDatesDistinct = fetchDates.distinctBy { it }
val newInterval = when {
// enough upload date from source
(uploadDateDistinct.size >= 3) -> {
val uploadDelta = uploadDateDistinct.last().until(uploadDateDistinct.first(), ChronoUnit.DAYS)
val uploadPeriod = uploadDates.indexOf(uploadDateDistinct.last())
(uploadDelta).floorDiv(uploadPeriod).toInt()
}
// enough fetch date from client
(fetchDatesDistinct.size >= 3) -> {
val fetchDelta = fetchDatesDistinct.last().until(fetchDatesDistinct.first(), ChronoUnit.DAYS)
val uploadPeriod = fetchDates.indexOf(fetchDatesDistinct.last())
(fetchDelta).floorDiv(uploadPeriod).toInt()
}
// default 7 days
else -> 7
}
// min 1, max 28 days
return newInterval.coerceIn(1, 28)
}
private fun calculateNextUpdate(manga: Manga, interval: Int, zonedDateTime: ZonedDateTime, currentFetchRange: Pair<Long, Long>): Long {
return if (manga.nextUpdate !in currentFetchRange.first.rangeTo(currentFetchRange.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 cycle = timeSinceLatest.floorDiv(interval.absoluteValue.takeIf { interval < 0 } ?: doubleInterval(interval, timeSinceLatest, doubleWhenOver = 10, maxValue = 28))
latestDate.plusDays((cycle + 1) * interval.toLong()).toEpochSecond(zonedDateTime.offset) * 1000
} 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
}
}
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)
}

View file

@ -44,9 +44,15 @@
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="action_menu">Menu</string> <string name="action_menu">Menu</string>
<string name="action_filter">Filter</string> <string name="action_filter">Filter</string>
<string name="action_set_interval">Set interval</string>
<string name="action_filter_bookmarked">Bookmarked</string> <string name="action_filter_bookmarked">Bookmarked</string>
<string name="action_filter_tracked">Tracked</string> <string name="action_filter_tracked">Tracked</string>
<string name="action_filter_unread">Unread</string> <string name="action_filter_unread">Unread</string>
<string name="action_filter_interval_custom">Customized fetch interval</string>
<string name="action_filter_interval_long">Fetch monthly (28 days)</string>
<string name="action_filter_interval_late">Late 10+ check</string>
<string name="action_filter_interval_dropped">Dropped? Late 20+ and 2 months</string>
<string name="action_filter_interval_passed">Passed check period</string>
<!-- reserved for #4048 --> <!-- reserved for #4048 -->
<string name="action_filter_empty">Remove filter</string> <string name="action_filter_empty">Remove filter</string>
<string name="action_sort_alpha">Alphabetically</string> <string name="action_sort_alpha">Alphabetically</string>
@ -55,6 +61,7 @@
<string name="action_sort_last_read">Last read</string> <string name="action_sort_last_read">Last read</string>
<string name="action_sort_last_manga_update">Last update check</string> <string name="action_sort_last_manga_update">Last update check</string>
<string name="action_sort_unread_count">Unread count</string> <string name="action_sort_unread_count">Unread count</string>
<string name="action_sort_next_updated">Next expected update</string>
<string name="action_sort_latest_chapter">Latest chapter</string> <string name="action_sort_latest_chapter">Latest chapter</string>
<string name="action_sort_chapter_fetch_date">Chapter fetch date</string> <string name="action_sort_chapter_fetch_date">Chapter fetch date</string>
<string name="action_sort_date_added">Date added</string> <string name="action_sort_date_added">Date added</string>
@ -255,6 +262,16 @@
<string name="pref_update_only_non_completed">With \"Completed\" status</string> <string name="pref_update_only_non_completed">With \"Completed\" status</string>
<string name="pref_update_only_started">That haven\'t been started</string> <string name="pref_update_only_started">That haven\'t been started</string>
<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 release period</string>
<string name="pref_update_release_grace_period">Grace release period:</string>
<string name="pref_update_release_leading_days">Check %s day(s) before</string>
<string name="pref_update_release_following_days">Check %s day(s) after</string>
<string name="pref_update_release_grace_period_info1">It is recommended to keep small grace period to minimize stress on servers.</string>
<string name="pref_update_release_grace_period_info2">The more checks comic missed, the longer extend check interval (max at 28 day).</string>
<string name="pref_update_release_grace_period_info3">It is recommend to remove or migrate source if comic in Dropped status filter.</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>
@ -593,6 +610,7 @@
<string name="updating_category">Updating category</string> <string name="updating_category">Updating category</string>
<string name="manga_from_library">From library</string> <string name="manga_from_library">From library</string>
<string name="downloaded_chapters">Downloaded chapters</string> <string name="downloaded_chapters">Downloaded chapters</string>
<string name="intervals_header">Intervals</string>
<!-- For badges/buttons on library covers. --> <!-- For badges/buttons on library covers. -->
<string name="overlay_header">Overlay</string> <string name="overlay_header">Overlay</string>
<string name="tabs_header">Tabs</string> <string name="tabs_header">Tabs</string>
@ -618,6 +636,10 @@
<string name="local_invalid_format">Invalid chapter format</string> <string name="local_invalid_format">Invalid chapter format</string>
<string name="local_filter_order_by">Order by</string> <string name="local_filter_order_by">Order by</string>
<string name="date">Date</string> <string name="date">Date</string>
<plurals name="day">
<item quantity="one">1 day</item>
<item quantity="other">%d days</item>
</plurals>
<!-- Manga info --> <!-- Manga info -->
<plurals name="missing_chapters"> <plurals name="missing_chapters">
@ -657,6 +679,10 @@
<!-- Manga chapters --> <!-- Manga chapters -->
<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_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="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>
@ -855,6 +881,7 @@
<string name="skipped_reason_not_caught_up">Skipped because there are unread chapters</string> <string name="skipped_reason_not_caught_up">Skipped because there are unread chapters</string>
<string name="skipped_reason_not_started">Skipped because no chapters are read</string> <string name="skipped_reason_not_started">Skipped because no chapters are read</string>
<string name="skipped_reason_not_always_update">Skipped because series does not require updates</string> <string name="skipped_reason_not_always_update">Skipped because series does not require updates</string>
<string name="skipped_reason_not_in_release_period">Skipped because no release was expected today</string>
<!-- File Picker Titles --> <!-- File Picker Titles -->
<string name="file_select_cover">Select cover image</string> <string name="file_select_cover">Select cover image</string>