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:
parent
a458bd9fdb
commit
c90f344910
@ -3,12 +3,16 @@ package eu.kanade.domain.manga.interactor
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.interactor.getCurrentFetchRange
|
||||
import tachiyomi.domain.manga.interactor.updateIntervalMeta
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.source.local.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
|
||||
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 {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
|
||||
}
|
||||
|
@ -1,6 +1,13 @@
|
||||
package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
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.ReadOnlyComposable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@ -10,9 +17,14 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import androidx.core.content.ContextCompat
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
@ -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_NON_COMPLETED
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
|
||||
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
|
||||
import tachiyomi.presentation.core.components.WheelPickerDefaults
|
||||
import tachiyomi.presentation.core.components.WheelTextPicker
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@ -132,8 +147,8 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
|
||||
val included by libraryUpdateCategoriesPref.collectAsState()
|
||||
val excluded by libraryUpdateCategoriesExcludePref.collectAsState()
|
||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showDialog) {
|
||||
var showCategoriesDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (showCategoriesDialog) {
|
||||
TriStateListDialog(
|
||||
title = stringResource(R.string.categories),
|
||||
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 } },
|
||||
initialInversed = excluded.mapNotNull { id -> allCategories.find { it.id.toString() == id } },
|
||||
itemLabel = { it.visualName },
|
||||
onDismissRequest = { showDialog = false },
|
||||
onDismissRequest = { showCategoriesDialog = false },
|
||||
onValueChanged = { newIncluded, newExcluded ->
|
||||
libraryUpdateCategoriesPref.set(newIncluded.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_NON_READ to stringResource(R.string.pref_update_only_started),
|
||||
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(
|
||||
title = stringResource(R.string.categories),
|
||||
subtitle = getCategoriesLabel(
|
||||
@ -201,7 +251,7 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||
included = included,
|
||||
excluded = excluded,
|
||||
),
|
||||
onClick = { showDialog = true },
|
||||
onClick = { showCategoriesDialog = true },
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -34,9 +34,13 @@ class LibraryPreferences(
|
||||
MANGA_HAS_UNREAD,
|
||||
MANGA_NON_COMPLETED,
|
||||
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 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 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)
|
||||
|
||||
// endregion
|
||||
@ -142,5 +156,6 @@ class LibraryPreferences(
|
||||
const val MANGA_NON_COMPLETED = "manga_ongoing"
|
||||
const val MANGA_HAS_UNREAD = "manga_fully_read"
|
||||
const val MANGA_NON_READ = "manga_started"
|
||||
const val MANGA_OUTSIDE_RELEASE_PERIOD = "manga_outside_release_period"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -44,9 +44,15 @@
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_menu">Menu</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_tracked">Tracked</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 -->
|
||||
<string name="action_filter_empty">Remove filter</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_manga_update">Last update check</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_chapter_fetch_date">Chapter fetch date</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_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_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_summary">Check for new cover and details when updating library</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="manga_from_library">From library</string>
|
||||
<string name="downloaded_chapters">Downloaded chapters</string>
|
||||
<string name="intervals_header">Intervals</string>
|
||||
<!-- For badges/buttons on library covers. -->
|
||||
<string name="overlay_header">Overlay</string>
|
||||
<string name="tabs_header">Tabs</string>
|
||||
@ -618,6 +636,10 @@
|
||||
<string name="local_invalid_format">Invalid chapter format</string>
|
||||
<string name="local_filter_order_by">Order by</string>
|
||||
<string name="date">Date</string>
|
||||
<plurals name="day">
|
||||
<item quantity="one">1 day</item>
|
||||
<item quantity="other">%d days</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Manga info -->
|
||||
<plurals name="missing_chapters">
|
||||
@ -657,6 +679,10 @@
|
||||
|
||||
<!-- Manga chapters -->
|
||||
<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_error">Error</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_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_in_release_period">Skipped because no release was expected today</string>
|
||||
|
||||
<!-- File Picker Titles -->
|
||||
<string name="file_select_cover">Select cover image</string>
|
||||
|
Loading…
Reference in New Issue
Block a user