Add handling for previously unhandled preferences (delegated MD) (#1524)

* Include romanized titles of the original language in description

* Implement handling for `finalChapterInDesc` preference.

* Handle `preferExtensionLangTitle` preference when fetching manga details.

* Address some warnings, clean up unused code and spotless apply.
This commit is contained in:
NGB-Was-Taken
2025-12-12 00:43:56 +05:45
committed by GitHub
parent 5566db160b
commit 582d0ef121
6 changed files with 203 additions and 144 deletions
@@ -168,17 +168,17 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
trackPreferences.trackToken(this).delete()
}
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata? {
override suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException()
val manga = mdex.getMangaMetadata(track.toDbTrack())
TrackMangaMetadata(
remoteId = 0,
title = manga?.title,
thumbnailUrl = manga?.thumbnail_url, // Doesn't load the actual cover because of Refer header
description = manga?.description,
authors = manga?.author,
artists = manga?.artist,
title = manga.title,
thumbnailUrl = manga.thumbnail_url, // Doesn't load the actual cover because of Refer header
description = manga.description,
authors = manga.author,
artists = manga.artist,
)
}
}
@@ -90,6 +90,8 @@ class MangaDex(delegate: HttpSource, val context: Context) :
private fun coverQuality() = sourcePreferences.getString(getCoverQualityPrefKey(mdLang.lang), "").orEmpty()
private fun tryUsingFirstVolumeCover() = sourcePreferences.getBoolean(getTryUsingFirstVolumeCoverKey(mdLang.lang), false)
private fun altTitlesInDesc() = sourcePreferences.getBoolean(getAltTitlesInDescKey(mdLang.lang), false)
private fun finalChapterInDesc() = sourcePreferences.getBoolean(getFinalChapterInDescPrefKey(mdLang.lang), false)
private fun preferExtensionLangTitle() = sourcePreferences.getBoolean(getPreferExtensionLangTitlePrefKey(mdLang.extLang), true)
private val mangadexService by lazy {
MangaDexService(client)
@@ -107,7 +109,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
FollowsHandler(mdLang.lang, mangadexAuthService)
}
private val mangaHandler by lazy {
MangaHandler(mdLang.lang, mangadexService, apiMangaParser, followsHandler)
MangaHandler(mdLang.lang, mangadexService, apiMangaParser)
}
private val similarHandler by lazy {
SimilarHandler(mdLang.lang, mangadexService, similarService)
@@ -192,11 +194,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return mangaHandler.fetchMangaDetailsObservable(manga, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
return mangaHandler.fetchMangaDetailsObservable(
manga,
id,
coverQuality(),
tryUsingFirstVolumeCover(),
altTitlesInDesc(),
finalChapterInDesc(),
preferExtensionLangTitle(),
)
}
override suspend fun getMangaDetails(manga: SManga): SManga {
return mangaHandler.getMangaDetails(manga, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
return mangaHandler.getMangaDetails(
manga,
id,
coverQuality(),
tryUsingFirstVolumeCover(),
altTitlesInDesc(),
finalChapterInDesc(),
preferExtensionLangTitle(),
)
}
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
@@ -241,8 +259,21 @@ class MangaDex(delegate: HttpSource, val context: Context) :
override fun newMetaInstance() = MangaDexSearchMetadata()
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Triple<MangaDto, List<String>, StatisticsMangaDto>) {
apiMangaParser.parseIntoMetadata(metadata, input.first, input.second, input.third, null, coverQuality(), altTitlesInDesc())
override suspend fun parseIntoMetadata(
metadata: MangaDexSearchMetadata,
input: Triple<MangaDto, List<String>, StatisticsMangaDto>,
) {
apiMangaParser.parseIntoMetadata(
metadata,
input.first,
input.second,
input.third,
null,
coverQuality(),
altTitlesInDesc(),
finalChapterInDesc(),
preferExtensionLangTitle(),
)
}
// LoginSource methods
@@ -296,10 +327,6 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return followsHandler.updateRating(track)
}
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata?> {
return mangaHandler.getTrackingInfo(track)
}
// RandomMangaSource method
override suspend fun fetchRandomMangaUrl(): String {
return mangaHandler.fetchRandomMangaId()
@@ -313,51 +340,62 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return similarHandler.getRelated(manga)
}
suspend fun getMangaMetadata(track: Track): SManga? {
return mangaHandler.getMangaMetadata(track, id, coverQuality(), tryUsingFirstVolumeCover(), altTitlesInDesc())
suspend fun getMangaMetadata(track: Track): SManga {
return mangaHandler.getMangaMetadata(
track,
id,
coverQuality(),
tryUsingFirstVolumeCover(),
altTitlesInDesc(),
finalChapterInDesc(),
preferExtensionLangTitle(),
)
}
companion object {
private const val dataSaverPref = "dataSaverV5"
fun getDataSaverPreferenceKey(dexLang: String): String {
return "${dataSaverPref}_$dexLang"
}
private const val standardHttpsPortPref = "usePort443"
fun getStandardHttpsPreferenceKey(dexLang: String): String {
return "${standardHttpsPortPref}_$dexLang"
}
private const val blockedGroupsPref = "blockedGroups"
fun getBlockedGroupsPrefKey(dexLang: String): String {
return "${blockedGroupsPref}_$dexLang"
}
private const val blockedUploaderPref = "blockedUploader"
fun getBlockedUploaderPrefKey(dexLang: String): String {
return "${blockedUploaderPref}_$dexLang"
}
private const val coverQualityPref = "thumbnailQuality"
fun getCoverQualityPrefKey(dexLang: String): String {
return "${coverQualityPref}_$dexLang"
}
private const val tryUsingFirstVolumeCover = "tryUsingFirstVolumeCover"
private const val tryUsingFirstVolumeCoverPref = "tryUsingFirstVolumeCover"
fun getTryUsingFirstVolumeCoverKey(dexLang: String): String {
return "${tryUsingFirstVolumeCover}_$dexLang"
return "${tryUsingFirstVolumeCoverPref}_$dexLang"
}
private const val altTitlesInDesc = "altTitlesInDesc"
private const val altTitlesInDescPref = "altTitlesInDesc"
fun getAltTitlesInDescKey(dexLang: String): String {
return "${altTitlesInDesc}_$dexLang"
return "${altTitlesInDescPref}_$dexLang"
}
private const val finalChapterInDescPref = "finalChapterInDesc"
fun getFinalChapterInDescPrefKey(dexLang: String): String {
return "${finalChapterInDescPref}_$dexLang"
}
private const val preferExtensionLangTitlePref = "preferExtensionLangTitle"
fun getPreferExtensionLangTitlePrefKey(dexLang: String): String {
return "${preferExtensionLangTitlePref}_$dexLang"
}
}
}
@@ -44,6 +44,8 @@ class ApiMangaParser(
coverFileName: String?,
coverQuality: String,
altTitlesInDesc: Boolean,
finalChapterInDesc: Boolean,
preferExtensionLangTitle: Boolean,
): SManga {
val mangaId = getManga.await(manga.url, sourceId)?.id
val metadata = if (mangaId != null) {
@@ -53,7 +55,17 @@ class ApiMangaParser(
newMetaInstance()
}
parseIntoMetadata(metadata, input, simpleChapters, statistics, coverFileName, coverQuality, altTitlesInDesc)
parseIntoMetadata(
metadata,
input,
simpleChapters,
statistics,
coverFileName,
coverQuality,
altTitlesInDesc,
finalChapterInDesc,
preferExtensionLangTitle,
)
if (mangaId != null) {
metadata.mangaId = mangaId
insertFlatMetadata.await(metadata.flatten())
@@ -70,13 +82,17 @@ class ApiMangaParser(
coverFileName: String?,
coverQuality: String,
altTitlesInDesc: Boolean,
finalChapterInDesc: Boolean,
preferExtensionLangTitle: Boolean,
) {
with(metadata) {
try {
val mangaAttributesDto = mangaDto.data.attributes
mdUuid = mangaDto.data.id
title = MdUtil.getTitleFromManga(mangaAttributesDto, lang)
altTitles = mangaAttributesDto.altTitles.mapNotNull { it[lang] }.nullIfEmpty()
title = MdUtil.getTitleFromManga(mangaAttributesDto, lang, preferExtensionLangTitle)
altTitles = mangaAttributesDto.altTitles
.filter { it.containsKey(lang) || it.containsKey("${mangaAttributesDto.originalLanguage}-ro") }
.mapNotNull { it.values.singleOrNull() }.nullIfEmpty()
val mangaRelationshipsDto = mangaDto.data.relationships
cover = if (!coverFileName.isNullOrEmpty()) {
@@ -96,9 +112,19 @@ class ApiMangaParser(
originalLanguage = mangaAttributesDto.originalLanguage,
).orEmpty()
val cleanDesc = MdUtil.cleanDescription(rawDesc)
description = if (altTitlesInDesc) MdUtil.addAltTitleToDesc(cleanDesc, altTitles) else cleanDesc
description = MdUtil.cleanDescription(rawDesc)
.let { if (altTitlesInDesc) MdUtil.addAltTitleToDesc(it, altTitles) else it }
.let {
if (finalChapterInDesc) {
MdUtil.addFinalChapterToDesc(
it,
mangaAttributesDto.lastVolume,
mangaAttributesDto.lastChapter,
)
} else {
it
}
}
authors = mangaRelationshipsDto.filter { relationshipDto ->
relationshipDto.type.equals(MdConstants.Types.author, true)
@@ -148,7 +174,11 @@ class ApiMangaParser(
mangaAttributesDto.contentRating
?.takeUnless { it == "safe" }
?.let {
RaisedTag("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT)
RaisedTag(
"Content Rating",
it.capitalize(Locale.US),
MangaDexSearchMetadata.TAG_TYPE_DEFAULT,
)
},
)
@@ -8,7 +8,6 @@ import exh.md.service.MangaDexService
import exh.md.utils.MdConstants
import exh.md.utils.MdUtil
import exh.md.utils.mdListCall
import exh.metadata.metadata.MangaDexSearchMetadata
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -21,7 +20,6 @@ class MangaHandler(
private val lang: String,
private val service: MangaDexService,
private val apiMangaParser: ApiMangaParser,
private val followsHandler: FollowsHandler,
) {
suspend fun getMangaDetails(
manga: SManga,
@@ -29,6 +27,8 @@ class MangaHandler(
coverQuality: String,
tryUsingFirstVolumeCover: Boolean,
altTitlesInDesc: Boolean,
finalChapterInDesc: Boolean,
preferExtensionLangTitle: Boolean,
): SManga {
return coroutineScope {
val mangaId = MdUtil.getMangaId(manga.url)
@@ -55,13 +55,31 @@ class MangaHandler(
coverFileName?.await(),
coverQuality,
altTitlesInDesc,
finalChapterInDesc,
preferExtensionLangTitle,
)
}
}
fun fetchMangaDetailsObservable(manga: SManga, sourceId: Long, coverQuality: String, tryUsingFirstVolumeCover: Boolean, altTitlesInDesc: Boolean): Observable<SManga> {
fun fetchMangaDetailsObservable(
manga: SManga,
sourceId: Long,
coverQuality: String,
tryUsingFirstVolumeCover: Boolean,
altTitlesInDesc: Boolean,
finalChapterInDesc: Boolean,
preferExtensionLangTitle: Boolean,
): Observable<SManga> {
return runAsObservable {
getMangaDetails(manga, sourceId, coverQuality, tryUsingFirstVolumeCover, altTitlesInDesc)
getMangaDetails(
manga,
sourceId,
coverQuality,
tryUsingFirstVolumeCover,
altTitlesInDesc,
finalChapterInDesc,
preferExtensionLangTitle,
)
}
}
@@ -92,11 +110,10 @@ class MangaHandler(
}
private fun getGroupMap(results: List<ChapterDataDto>): Map<String, String> {
return results.map { chapter -> chapter.relationships }
.flatten()
return results
.flatMap { it.relationships }
.filter { it.type == MdConstants.Types.scanlator }
.map { it.id to it.attributes!!.name!! }
.toMap()
.associate { it.id to it.attributes!!.name!! }
}
suspend fun fetchRandomMangaId(): String {
@@ -105,23 +122,6 @@ class MangaHandler(
}
}
suspend fun getTrackingInfo(track: Track): Pair<Track, MangaDexSearchMetadata?> {
return withIOContext {
/*val metadata = async {
val mangaUrl = MdUtil.buildMangaUrl(MdUtil.getMangaId(track.tracking_url))
val manga = MangaInfo(mangaUrl, track.title)
val response = client.newCall(mangaRequest(manga)).await()
val metadata = MangaDexSearchMetadata()
apiMangaParser.parseIntoMetadata(metadata, response, emptyList())
metadata
}*/
val remoteTrack = async {
followsHandler.fetchTrackingInfo(track.tracking_url)
}
remoteTrack.await() to null
}
}
suspend fun getMangaFromChapterId(chapterId: String): String? {
return withIOContext {
apiMangaParser.chapterParseForMangaId(service.viewChapter(chapterId))
@@ -134,7 +134,9 @@ class MangaHandler(
coverQuality: String,
tryUsingFirstVolumeCover: Boolean,
altTitlesInDesc: Boolean,
): SManga? {
finalChapterInDesc: Boolean,
preferExtensionLangTitle: Boolean,
): SManga {
return withIOContext {
val mangaId = MdUtil.getMangaId(track.tracking_url)
val response = service.viewManga(mangaId)
@@ -154,6 +156,8 @@ class MangaHandler(
coverFileName,
coverQuality,
altTitlesInDesc,
finalChapterInDesc,
preferExtensionLangTitle,
)
}
}
+68 -82
View File
@@ -3,19 +3,15 @@ package exh.md.utils
import android.app.Application
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALOAuth
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.util.PkceUtil
import exh.md.dto.MangaAttributesDto
import exh.md.dto.MangaDataDto
import exh.source.getMainSource
import exh.util.dropBlank
import exh.util.floor
import exh.util.nullIfZero
import kotlinx.serialization.json.Json
import okhttp3.FormBody
@@ -25,7 +21,9 @@ import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.parser.Parser
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.sy.SYMR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
@@ -39,21 +37,10 @@ class MdUtil {
const val baseUrl = "https://mangadex.org"
const val chapterSuffix = "/chapter/"
const val similarCacheMapping = "https://api.similarmanga.com/mapping/mdex2search.csv"
const val similarCacheMangas = "https://api.similarmanga.com/manga/"
const val similarBaseApi = "https://api.similarmanga.com/similar/"
const val groupSearchUrl = "$baseUrl/groups/0/1/"
const val reportUrl = "https://api.mangadex.network/report"
const val mdAtHomeTokenLifespan = 10 * 60 * 1000
const val mangaLimit = 20
/**
* Get the manga offset pages are 1 based, so subtract 1
*/
fun getMangaListOffset(page: Int): String = (mangaLimit * (page - 1)).toString()
val jsonParser =
Json {
isLenient = true
@@ -65,15 +52,8 @@ class MdUtil {
private const val scanlatorSeparator = " & "
const val contentRatingSafe = "safe"
const val contentRatingSuggestive = "suggestive"
const val contentRatingErotica = "erotica"
const val contentRatingPornographic = "pornographic"
val validOneShotFinalChapters = listOf("0", "1")
val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
val markdownLinksRegex = "\\[([^]]+)]\\(([^)]+)\\)".toRegex()
val markdownItalicBoldRegex = "\\*+\\s*([^*]*)\\s*\\*+".toRegex()
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
fun buildMangaUrl(mangaUuid: String): String {
@@ -94,47 +74,10 @@ class MdUtil {
.trim()
}
fun getImageUrl(attr: String): String {
// Some images are hosted elsewhere
if (attr.startsWith("http")) {
return attr
}
return baseUrl + attr
}
fun getScanlators(scanlators: String?): Set<String> {
return scanlators?.split(scanlatorSeparator)?.dropBlank()?.toSet().orEmpty()
}
fun getScanlatorString(scanlators: Set<String>): String {
return scanlators.sorted().joinToString(scanlatorSeparator)
}
fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? {
if (mangaStatus == SManga.COMPLETED) return null
val remove0ChaptersFromCount = chapters.distinctBy {
/*if (it.chapter_txt.isNotEmpty()) {
it.vol + it.chapter_txt
} else {*/
it.name
/*}*/
}.sortedByDescending { it.chapter_number }
remove0ChaptersFromCount.firstOrNull()?.let { chapter ->
val chpNumber = chapter.chapter_number.floor()
val allChapters = (1..chpNumber).toMutableSet()
remove0ChaptersFromCount.forEach {
allChapters.remove(it.chapter_number.floor())
}
if (allChapters.isEmpty()) return null
return allChapters.size.toString()
}
return null
}
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
@@ -144,7 +87,7 @@ class MdUtil {
fun createMangaEntry(json: MangaDataDto, lang: String): SManga {
return SManga(
url = buildMangaUrl(json.id),
title = getTitleFromManga(json.attributes, lang),
title = getTitleFromManga(json.attributes, lang, true),
thumbnail_url = json.relationships
.firstOrNull { relationshipDto -> relationshipDto.type == MdConstants.Types.coverArt }
?.attributes
@@ -155,12 +98,30 @@ class MdUtil {
)
}
fun getTitleFromManga(json: MangaAttributesDto, lang: String): String {
return getFromLangMap(json.title.asMdMap(), lang, json.originalLanguage)
?: getAltTitle(json.altTitles, lang, json.originalLanguage)
?: json.title.asMdMap<String>()[json.originalLanguage]
?: json.altTitles.firstNotNullOfOrNull { it[json.originalLanguage] }
.orEmpty()
fun getTitleFromManga(json: MangaAttributesDto, lang: String, preferExtensionLangTitle: Boolean): String {
val titleMap = json.title.asMdMap<String>()
val altTitles = json.altTitles
val originalLang = json.originalLanguage
titleMap[lang]?.let { return it }
val mainTitle = titleMap.values.firstOrNull()
val langTitle = findTitleInMaps(lang, titleMap, altTitles)
val enTitle = findTitleInMaps("en", titleMap, altTitles)
val originalLangTitle = findTitleInMaps("$originalLang-ro", titleMap, altTitles) ?: findTitleInMaps(
originalLang,
titleMap,
altTitles,
)
val ordered = if (preferExtensionLangTitle) {
listOf(langTitle, mainTitle, enTitle, originalLangTitle)
} else {
listOf(mainTitle, langTitle, enTitle, originalLangTitle)
}
return ordered.firstOrNull { it != null }
?: ""
}
fun getFromLangMap(langMap: Map<String, String>, currentLang: String, originalLanguage: String): String? {
@@ -174,15 +135,12 @@ class MdUtil {
}
}
fun getAltTitle(langMaps: List<Map<String, String>>, currentLang: String, originalLanguage: String): String? {
return langMaps.firstNotNullOfOrNull { it[currentLang] }
?: langMaps.firstNotNullOfOrNull { it["en"] }
?: if (originalLanguage == "ja") {
langMaps.firstNotNullOfOrNull { it["ja-ro"] }
?: langMaps.firstNotNullOfOrNull { it["jp-ro"] }
} else {
null
}
fun findTitleInMaps(
lang: String,
titleMap: Map<String, String>,
altTitleMaps: List<Map<String, String>>,
): String? {
return titleMap[lang] ?: altTitleMaps.firstNotNullOfOrNull { it[lang] }
}
fun cdnCoverUrl(dexId: String, fileName: String): String {
@@ -200,7 +158,7 @@ class MdUtil {
fun loadOAuth(preferences: TrackPreferences, mdList: MdList): MALOAuth? {
return try {
jsonParser.decodeFromString<MALOAuth>(preferences.trackToken(mdList).get())
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
@@ -230,7 +188,10 @@ class MdUtil {
return codeVerifier ?: PkceUtil.generateCodeVerifier().also { codeVerifier = it }
}
fun getEnabledMangaDex(sourcePreferences: SourcePreferences = Injekt.get(), sourceManager: SourceManager = Injekt.get()): MangaDex? {
fun getEnabledMangaDex(
sourcePreferences: SourcePreferences = Injekt.get(),
sourceManager: SourceManager = Injekt.get(),
): MangaDex? {
return getEnabledMangaDexs(sourcePreferences, sourceManager).let { mangadexs ->
sourcePreferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()
?.let { preferredMangaDexId ->
@@ -240,7 +201,10 @@ class MdUtil {
}
}
fun getEnabledMangaDexs(preferences: SourcePreferences, sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
fun getEnabledMangaDexs(
preferences: SourcePreferences,
sourceManager: SourceManager = Injekt.get(),
): List<MangaDex> {
val languages = preferences.enabledLanguages().get()
val disabledSourceIds = preferences.disabledSources().get()
@@ -262,8 +226,30 @@ class MdUtil {
description
} else {
val altTitlesDesc = altTitles
.joinToString("\n", "${Injekt.get<Application>().getString(R.string.alt_titles)}:\n") { "$it" }
description + (if (description.isBlank()) "" else "\n\n") + Parser.unescapeEntities(altTitlesDesc, false)
.joinToString(
"\n",
"${Injekt.get<Application>().stringResource(SYMR.strings.alt_titles)}:\n",
) { "$it" }
description + (if (description.isBlank()) "" else "\n\n") + Parser.unescapeEntities(
altTitlesDesc,
false,
)
}
}
fun addFinalChapterToDesc(description: String, lastVolume: String?, lastChapter: String?): String {
val parts = listOfNotNull(
lastVolume?.takeIf { it.isNotEmpty() }?.let { "Vol.$it" },
lastChapter?.takeIf { it.isNotEmpty() }?.let { "Ch.$it" },
)
return if (parts.isEmpty()) {
description
} else {
description + (if (description.isBlank()) "" else "\n\n") + parts.joinToString(
" ",
"${Injekt.get<Application>().stringResource(SYMR.strings.final_chapter)}:\n",
)
}
}
}
@@ -708,7 +708,8 @@
<string name="mangadex_push_favorites_to_mangadex_summary">Syncs any non MdList tracked entries to MangaDex as reading.</string>
<string name="community_recommendations">Community recommendations</string>
<string name="similar_titles">Similar titles</string>
<string name="alt_titles">Alternative Titles</string>
<string name="alt_titles">Alternative titles</string>
<string name="final_chapter">Final chapter</string>
<!-- Scanlator filters -->
<string name="select_scanlators">Scanlator groups to show</string>