13 Commits

Author SHA1 Message Date
javara999 6391c9376c Improve text reader streaming and theming
Build & Test / Build & Test App (push) Waiting to run
2026-09-19 19:39:48 +09:00
javara999 dcec339477 fix: avoid ANR during live text style updates
Build & Test / Build & Test App (push) Has been cancelled
2026-09-19 13:42:50 +09:00
javara999 1b01c894ad feat: add Kavita document fallback and live text settings
Build & Test / Build & Test App (push) Has been cancelled
2026-09-19 12:58:13 +09:00
javara999 4900efbca5 feat: add BookOasis document reader and text settings
Update website / update_website (release) Waiting to run
Build & Test / Build & Test App (push) Has been cancelled
2026-09-19 10:58:14 +09:00
Mend Renovate 5848ffb4c4 Update metro to v1.4.4 (#3973) 2026-09-17 13:37:56 +06:00
hosted-weblate[bot] 51efde975b Translations update from Hosted Weblate (#3950)
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/am/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/bg/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ceb/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/cv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/da/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/eu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/fi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/gl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/hu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/jv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ka/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/kk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/km/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/kn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/lt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/lv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ml/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/mr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/nn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/pl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/pt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sah/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sc/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sdh/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sq/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/sr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/te/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/th/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon-plurals/uz/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/am/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ar/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/as/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/az/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/be/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bg/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/bn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ca/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ceb/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cs/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/cv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/da/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/de/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/el/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eo/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/es/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/eu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fil/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/fr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/gl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/he/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/hu/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/id/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/it/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ja/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/jv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ka/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/km/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kmr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/kn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ko/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/lv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ml/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/mr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ms/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/my/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ne/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/nn/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pl/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ro/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ru/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sa/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sah/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sc/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sdh/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sq/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/sv/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/ta/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/te/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/th/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tr/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/tt/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/uk/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/uz/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/vi/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/mihon/mihon/zh_Hant/
Translation: Mihon/Mihon
Translation: Mihon/Mihon Plurals

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: AntsyLich <antsylich@gmail.com>
Co-authored-by: Arash <ara.khoram95@gmail.com>
Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: David Katrinka <davidkatrinka1995@gmail.com>
Co-authored-by: Dexroneum <Rozhenkov69@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Giuseppe Ventrone <ventrone524@gmail.com>
Co-authored-by: Horace Johnson <horacejohnson99@gmail.com>
Co-authored-by: Infy Milliaria <ced.paltep10@gmail.com>
Co-authored-by: KING APPS <kiperking1@gmail.com>
Co-authored-by: KraXen <dpelech1@gmail.com>
Co-authored-by: Lucas Correia <anicetoclucas@gmail.com>
Co-authored-by: Lyfja <45209212+lyfja@users.noreply.github.com>
Co-authored-by: Szafranek13 <jakubfabijan@tuta.io>
Co-authored-by: Throw Away <throwawayacc4gulshan@gmail.com>
Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Co-authored-by: icarus suraci <mhmtylmz112000@gmail.com>
Co-authored-by: nadevko <me+oss@nadevko.cc>
Co-authored-by: on9686 <on9686@users.noreply.hosted.weblate.org>
2026-09-16 23:59:22 +06:00
MajorTanya 1e142c4255 Fix padded year & 31-clamped month in local source (#3970) 2026-09-16 23:56:27 +06:00
MajorTanya a7179805ad Use & write Year, Month, Day info in ComicInfo (#3967)
* Use & write Year, Month, Day info in ComicInfo

Currently, users can only modify the date Mihon displays in the
chapter list by manipulating the _last modified date_ of the file.
This is obviously inconvenient, especially since the ComicInfo spec
includes Year, Month, and Day fields for this exact purpose.

If present, this reads the date at start of day in the current default
system time zone (which matches how we use Chapter.dateUpload with
relativeDateText, for example).

If Year is not specified, the entire date process is skipped.
If Month is not specified, it is set to January.
If Day is not specified, it is set to the first.

The parsed date (if any) is set as the Chapter's `date_upload` which
is used for display in the app.

Additionally, Mihon now includes this info in the written
ComicInfo.xml file for downloaded chapters. This will be convenient
for many more users, not least of which those who move
source-downloaded chapters to Local Source to read from there.

---

I used kotlinx.datetime because it's what we use for date parsing in
non-i18n places already.

* Changelog
2026-09-16 08:23:25 +06:00
AntsyLich 1c43addeca Resolve Injekt dependencies directly from the Metro graph (#3965)
The previous setup had a circular dependency between Injekt
registration and extension loading.

In regard to #3931, I've moved the culprit ExtensionManager out of
Injekt altogether, alongside CoverCache, PreferenceStore and
TrackPreferences, none of which a source needs.
2026-09-15 15:57:48 +06:00
MajorTanya b2cda03e3a Use modern OpenDocument for backup file selection (#3948)
This might also fix the underlying problem of #3947.

I don't have any nonstandard devices available, but this works fine
in an A17 Pixel emulator & on my A14 Xiaomi device.

Looks like the GetContent approach was merely carried over from older
code without being updated to the more modern OpenDocument contract.
2026-09-15 02:20:09 +06:00
AntsyLich fdc2d40a1d Reload extensions when relevant settings are updated (#3954) 2026-09-14 22:41:53 +06:00
AntsyLich e6d8efe6f8 Expose installed extensions that failed to load (#3953) 2026-09-14 21:34:55 +06:00
AntsyLich 0e0bc6972a Allow user to load installed extensions regardless of content warning (#3952) 2026-09-14 21:22:32 +06:00
132 changed files with 2922 additions and 817 deletions
+1
View File
@@ -15,6 +15,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Add `id:` prefix search to remaining trackers (AniList, Bangumi, Kitsu, MangaUpdates, Shikimori, and Hikka) ([@MajorTanya](https://github.com/MajorTanya)) ([#3776](https://github.com/mihonapp/mihon/pull/3776))
- Allow `id:` to search for slugs on Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792))
- Add support for using the user's chosen rating system for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3818](https://github.com/mihonapp/mihon/pull/3818))
- Add support for the Year, Month, and Day fields in ComicInfo.xml files for chapter dating ([@MajorTanya](https://github.com/MajorTanya)) ([#3967](https://github.com/mihonapp/mihon/pull/3967))
### Improved
- Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
+1
View File
@@ -216,6 +216,7 @@ dependencies {
implementation(projects.coreMetadata)
implementation(projects.sourceApi)
implementation(projects.sourceLocal)
implementation(libs.jsoup)
implementation(projects.data)
implementation(projects.domain)
implementation(projects.presentationCore)
@@ -12,7 +12,7 @@ class GetExtensionSources(
private val preferences: SourcePreferences,
) {
fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
fun subscribe(extension: Extension.Loaded): Flow<List<ExtensionSourceItem>> {
val isMultiSource = extension.sources.size > 1
val isMultiLangSingleSource =
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
@@ -19,24 +19,24 @@ class GetExtensionsByType(
return combine(
preferences.enabledLanguages.changes(),
extensionManager.installedExtensionsFlow,
extensionManager.untrustedExtensionsFlow,
extensionManager.loadedExtensionsFlow,
extensionManager.notLoadedExtensionsFlow,
extensionManager.availableExtensionsFlow,
) { enabledLanguages, _installed, _untrusted, _available ->
val (updates, installed) = _installed
) { enabledLanguages, _loaded, _notLoaded, _available ->
val (updates, loaded) = _loaded
.sortedWith(
compareBy<Extension.Installed> { !it.isObsolete }
compareBy<Extension.Loaded> { !it.isObsolete }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
)
.partition { it.hasUpdate }
val untrusted = _untrusted
val notLoaded = _notLoaded
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
val available = _available
.filter { extension ->
_installed.none { it.pkgName == extension.pkgName } &&
_untrusted.none { it.pkgName == extension.pkgName } &&
_loaded.none { it.pkgName == extension.pkgName } &&
_notLoaded.none { it.pkgName == extension.pkgName } &&
extension.contentWarning in enabledContentWarnings
}
.flatMap { ext ->
@@ -52,7 +52,7 @@ class GetExtensionsByType(
}
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
Extensions(updates, installed, available, untrusted)
Extensions(updates, loaded, available, notLoaded)
}
}
}
@@ -4,6 +4,11 @@ import android.content.pm.PackageInfo
import androidx.core.content.pm.PackageInfoCompat
import dev.zacsweers.metro.Inject
import eu.kanade.domain.source.service.SourcePreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import mihon.domain.extension.repository.ExtensionStoreRepository
import tachiyomi.core.common.preference.getAndSet
@@ -31,4 +36,23 @@ class TrustExtension(
fun revokeAll() {
preferences.trustedExtensions.delete()
}
/**
* Emits whenever what counts as trusted changes, either because a store was added or removed or
* because an extension was trusted or had its trust revoked. Both sources replay their current
* value, which is dropped.
*/
fun changes(): Flow<Unit> {
return merge(
// Stores are rewritten whenever their index is refreshed, so only their keys matter here
repository.getAllAsFlow()
.map { stores -> stores.mapTo(HashSet()) { it.signingKey } }
.distinctUntilChanged()
.drop(1),
preferences.trustedExtensions.changes()
.distinctUntilChanged()
.drop(1),
)
.map {}
}
}
@@ -3,8 +3,8 @@ package eu.kanade.domain.extension.model
import eu.kanade.tachiyomi.extension.model.Extension
data class Extensions(
val updates: List<Extension.Installed>,
val installed: List<Extension.Installed>,
val updates: List<Extension.Loaded>,
val loaded: List<Extension.Loaded>,
val available: List<Extension.Available>,
val untrusted: List<Extension.Untrusted>,
val notLoaded: List<Extension.NotLoaded>,
)
@@ -5,6 +5,9 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
import kotlinx.datetime.toLocalDateTime
import mihon.app.di.appGraph
import tachiyomi.core.common.preference.TriState
import tachiyomi.core.metadata.comicinfo.ComicInfo
@@ -13,6 +16,7 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Instant
// TODO: move these into the domain model
val Manga.readingMode: Long
@@ -72,7 +76,7 @@ fun Manga.copyFrom(other: SManga): Manga {
)
}
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get<Context>().appGraph.coverCache): Boolean {
return coverCache.getCustomCoverFile(id).exists()
}
@@ -85,30 +89,41 @@ fun getComicInfo(
urls: List<String>,
categories: List<String>?,
sourceName: String,
) = ComicInfo(
title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title),
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
if ((it.rem(1) == 0.0)) {
ComicInfo.Number(it.toInt().toString())
} else {
ComicInfo.Number(it.toString())
): ComicInfo {
val date = chapter.dateUpload
.takeIf { it != 0L }
?.let {
Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.currentSystemDefault())
}
},
web = ComicInfo.Web(urls.joinToString(" ")),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
source = ComicInfo.SourceMihon(sourceName),
inker = null,
colorist = null,
letterer = null,
coverArtist = null,
tags = null,
)
return ComicInfo(
title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title),
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
if ((it.rem(1) == 0.0)) {
ComicInfo.Number(it.toInt().toString())
} else {
ComicInfo.Number(it.toString())
}
},
web = ComicInfo.Web(urls.joinToString(" ")),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
source = ComicInfo.SourceMihon(sourceName),
inker = null,
colorist = null,
letterer = null,
coverArtist = null,
tags = null,
year = date?.year?.let { ComicInfo.Year(it) },
month = date?.month?.number?.let { ComicInfo.Month(it) },
day = date?.day?.let { ComicInfo.Day(it) },
)
}
@@ -48,6 +48,11 @@ class SourcePreferences(
setOf(ContentWarning.SAFE, ContentWarning.MIXED, ContentWarning.NSFW),
)
val applyContentWarningsToInstalled: Preference<Boolean> = preferenceStore.getBoolean(
"apply_content_warnings_to_installed",
true,
)
val migrationSortingMode: Preference<SetMigrateSorting.Mode> = preferenceStore.getEnum(
"pref_migration_sorting",
SetMigrateSorting.Mode.ALPHABETICAL,
@@ -145,7 +145,7 @@ fun ExtensionDetailsScreen(
@Composable
private fun ExtensionDetails(
contentPadding: PaddingValues,
extension: Extension.Installed,
extension: Extension.Loaded,
sources: List<ExtensionSourceItem>,
incognitoMode: Boolean,
onClickSourcePreferences: (sourceId: Long) -> Unit,
@@ -239,7 +239,7 @@ private fun DetailsHeader(
""".trimIndent(),
)
if (extension is Extension.Installed) {
if (extension is Extension.Loaded) {
append("\n\n")
appendLine(
"""
@@ -31,6 +31,7 @@ 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.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.LocalNavigator
@@ -48,14 +49,17 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import mihon.icons.materialsymbols.MaterialSymbols
import mihon.icons.materialsymbols.rounded.Close
import mihon.icons.materialsymbols.rounded.Download
import mihon.icons.materialsymbols.rounded.Info
import mihon.icons.materialsymbols.rounded.Public
import mihon.icons.materialsymbols.rounded.Refresh
import mihon.icons.materialsymbols.rounded.Settings
import mihon.icons.materialsymbols.rounded.VerifiedUser
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.FastScrollLazyColumn
import tachiyomi.presentation.core.components.material.PullRefresh
@@ -78,10 +82,10 @@ fun ExtensionScreen(
onClickItemCancel: (Extension) -> Unit,
onOpenWebView: (Extension.Available) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit,
onTrustExtension: (Extension.Untrusted) -> Unit,
onOpenExtension: (Extension.Installed) -> Unit,
onUninstallExtension: (Extension.Installed) -> Unit,
onUpdateExtension: (Extension.Loaded) -> Unit,
onTrustExtension: (Extension.NotLoaded) -> Unit,
onOpenExtension: (Extension.Loaded) -> Unit,
onClickUpdateAll: () -> Unit,
onRefresh: () -> Unit,
) {
@@ -139,14 +143,14 @@ private fun ExtensionContent(
onClickItemCancel: (Extension) -> Unit,
onOpenWebView: (Extension.Available) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit,
onTrustExtension: (Extension.Untrusted) -> Unit,
onOpenExtension: (Extension.Installed) -> Unit,
onUninstallExtension: (Extension.Installed) -> Unit,
onUpdateExtension: (Extension.Loaded) -> Unit,
onTrustExtension: (Extension.NotLoaded) -> Unit,
onOpenExtension: (Extension.Loaded) -> Unit,
onClickUpdateAll: () -> Unit,
) {
val context = LocalContext.current
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
var notLoadedState by remember { mutableStateOf<Extension.NotLoaded?>(null) }
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
FastScrollLazyColumn(
@@ -205,8 +209,8 @@ private fun ExtensionContent(
contentType = { "item" },
key = { item ->
when (item.extension) {
is Extension.Untrusted -> "extension-untrusted-${item.hashCode()}"
is Extension.Installed -> "extension-installed-${item.hashCode()}"
is Extension.NotLoaded -> "extension-not-loaded-${item.hashCode()}"
is Extension.Loaded -> "extension-loaded-${item.hashCode()}"
is Extension.Available -> "extension-available-${item.hashCode()}"
}
},
@@ -217,9 +221,9 @@ private fun ExtensionContent(
onClickItem = {
when (it) {
is Extension.Available -> onInstallExtension(it)
is Extension.Installed -> onOpenExtension(it)
is Extension.Untrusted -> {
trustState = it
is Extension.Loaded -> onOpenExtension(it)
is Extension.NotLoaded -> {
notLoadedState = it
}
}
},
@@ -227,7 +231,7 @@ private fun ExtensionContent(
onClickItemSecondaryAction = {
when (it) {
is Extension.Available -> onOpenWebView(it)
is Extension.Installed -> onOpenExtension(it)
is Extension.Loaded -> onOpenExtension(it)
else -> {}
}
},
@@ -235,15 +239,15 @@ private fun ExtensionContent(
onClickItemAction = {
when (it) {
is Extension.Available -> onInstallExtension(it)
is Extension.Installed -> {
is Extension.Loaded -> {
if (it.hasUpdate) {
onUpdateExtension(it)
} else {
onOpenExtension(it)
}
}
is Extension.Untrusted -> {
trustState = it
is Extension.NotLoaded -> {
notLoadedState = it
}
}
},
@@ -251,20 +255,30 @@ private fun ExtensionContent(
}
}
}
if (trustState != null) {
ExtensionTrustDialog(
onClickConfirm = {
onTrustExtension(trustState!!)
trustState = null
},
onClickDismiss = {
onUninstallExtension(trustState!!)
trustState = null
},
onDismissRequest = {
trustState = null
},
)
notLoadedState?.let { extension ->
val dismiss = { notLoadedState = null }
if (extension.reason is Extension.NotLoaded.Reason.Untrusted) {
ExtensionTrustDialog(
onClickConfirm = {
onTrustExtension(extension)
dismiss()
},
onClickDismiss = {
onUninstallExtension(extension)
dismiss()
},
onDismissRequest = dismiss,
)
} else {
ExtensionNotLoadedDialog(
reason = extension.reason,
onClickUninstall = {
onUninstallExtension(extension)
dismiss()
},
onDismissRequest = dismiss,
)
}
}
}
@@ -353,7 +367,7 @@ private fun ExtensionItemContent(
) {
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
if (extension is Extension.Loaded && extension.lang.isNotEmpty()) {
hasAlreadyShownAnElement = true
Text(
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
@@ -370,9 +384,9 @@ private fun ExtensionItemContent(
val warnings = listOfNotNull(
when {
extension is Extension.Untrusted ->
MR.strings.ext_untrusted to MaterialTheme.colorScheme.error
extension is Extension.Installed && extension.isObsolete ->
extension is Extension.NotLoaded ->
extension.reason.labelRes?.let { it to MaterialTheme.colorScheme.error }
extension is Extension.Loaded && extension.isObsolete ->
MR.strings.ext_obsolete to MaterialTheme.colorScheme.error
else -> null
},
@@ -389,7 +403,7 @@ private fun ExtensionItemContent(
)
}
if (extension is Extension.Installed && !extension.isShared) {
if (extension is Extension.Loaded && !extension.isShared) {
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
Text(
text = stringResource(MR.strings.ext_installer_private),
@@ -446,7 +460,7 @@ private fun ExtensionItemActions(
}
installStep == InstallStep.Idle -> {
when (extension) {
is Extension.Installed -> {
is Extension.Loaded -> {
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
Icon(
imageVector = MaterialSymbols.Rounded.Settings,
@@ -463,11 +477,20 @@ private fun ExtensionItemActions(
}
}
}
is Extension.Untrusted -> {
is Extension.NotLoaded -> {
val isUntrusted = extension.reason is Extension.NotLoaded.Reason.Untrusted
IconButton(onClick = { onClickItemAction(extension) }) {
Icon(
imageVector = MaterialSymbols.Rounded.VerifiedUser,
contentDescription = stringResource(MR.strings.ext_trust),
imageVector = if (isUntrusted) {
MaterialSymbols.Rounded.VerifiedUser
} else {
MaterialSymbols.Rounded.Info
},
contentDescription = if (isUntrusted) {
stringResource(MR.strings.ext_trust)
} else {
stringResource(MR.strings.ext_not_loaded)
},
)
}
}
@@ -530,6 +553,83 @@ private fun ExtensionHeader(
}
}
/**
* Only the reasons the user can act on are worth naming in the row; the rest all mean "broken" to
* them and are spelled out in [ExtensionNotLoadedDialog] instead.
*/
private val Extension.NotLoaded.Reason.labelRes: StringResource?
get() = when (this) {
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.ext_untrusted
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered
// The section header already says these aren't loaded; the dialog says why
Extension.NotLoaded.Reason.Unsigned,
Extension.NotLoaded.Reason.UnsupportedLibVersion,
Extension.NotLoaded.Reason.Malformed,
is Extension.NotLoaded.Reason.Failed,
-> null
}
private val Extension.NotLoaded.Reason.descriptionRes: StringResource
get() = when (this) {
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.untrusted_extension_message
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered_message
Extension.NotLoaded.Reason.Unsigned -> MR.strings.ext_unsigned_message
Extension.NotLoaded.Reason.UnsupportedLibVersion -> MR.strings.ext_unsupported_message
Extension.NotLoaded.Reason.Malformed -> MR.strings.ext_malformed_message
is Extension.NotLoaded.Reason.Failed -> MR.strings.ext_load_failed_message
}
@Composable
private fun ExtensionNotLoadedDialog(
reason: Extension.NotLoaded.Reason,
onClickUninstall: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
title = {
Text(text = stringResource(MR.strings.ext_not_loaded_dialog))
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
Text(text = stringResource(reason.descriptionRes))
if (reason is Extension.NotLoaded.Reason.Failed) {
Text(
text = reason.message,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
val context = LocalContext.current
TextButton(
onClick = {
context.copyToClipboard(
label = context.stringResource(MR.strings.ext_copy_stacktrace),
content = reason.stackTrace,
)
},
contentPadding = PaddingValues(0.dp),
) {
Text(text = stringResource(MR.strings.ext_copy_stacktrace))
}
}
}
},
confirmButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_ok))
}
},
dismissButton = {
TextButton(onClick = onClickUninstall) {
Text(text = stringResource(MR.strings.ext_uninstall))
}
},
onDismissRequest = onDismissRequest,
)
}
@Composable
private fun ExtensionTrustDialog(
onClickConfirm: () -> Unit,
@@ -96,7 +96,7 @@ fun ExtensionIcon(
.clip(MaterialTheme.shapes.extraSmall),
)
}
is Extension.Installed -> {
is Extension.Loaded -> {
val icon by extension.getIcon(density)
when (icon) {
Result.Loading -> Box(modifier = modifier)
@@ -112,7 +112,7 @@ fun ExtensionIcon(
)
}
}
is Extension.Untrusted -> Image(
is Extension.NotLoaded -> Image(
imageVector = MaterialSymbols.Rounded.Dangerous,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error),
@@ -400,10 +400,7 @@ object SettingsAdvancedScreen : SearchableSettings {
),
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.ext_revoke_trust),
onClick = {
trustExtension.revokeAll()
context.toast(MR.strings.requires_app_restart)
},
onClick = { trustExtension.revokeAll() },
),
),
)
@@ -81,6 +81,22 @@ object SettingsBrowseScreen : SearchableSettings {
}
},
),
Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.applyContentWarningsToInstalled,
title = stringResource(MR.strings.pref_apply_content_warnings_to_installed),
subtitle = stringResource(MR.strings.pref_apply_content_warnings_to_installed_summary),
onValueChanged = { newValue ->
if (newValue) {
true
} else {
(context as FragmentActivity).authenticate(
title = context.stringResource(
MR.strings.pref_apply_content_warnings_to_installed,
),
)
}
},
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.content_warnings_info)),
),
),
@@ -1,7 +1,6 @@
package eu.kanade.presentation.more.settings.screen
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher
@@ -186,18 +185,22 @@ object SettingsDataScreen : SearchableSettings {
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp.collectAsState()
val chooseBackup = rememberLauncherForActivityResult(
object : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
}
},
ActivityResultContracts.OpenDocument(),
) {
if (it == null) {
context.toast(MR.strings.file_null_uri_error)
return@rememberLauncherForActivityResult
}
try {
context.contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION,
)
} catch (e: SecurityException) {
logcat(LogPriority.ERROR, e)
}
navigator.push(RestoreBackupScreen(it.toString()))
}
@@ -232,9 +235,7 @@ object SettingsDataScreen : SearchableSettings {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(MR.strings.restore_miui_warning)
}
// no need to catch because it's wrapped with a chooser
chooseBackup.launch("*/*")
chooseBackup.launch(arrayOf("*/*"))
} else {
context.toast(MR.strings.restore_in_progress)
}
@@ -40,7 +40,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.presentation.manga.components.MangaCover
import eu.kanade.presentation.theme.TachiyomiTheme
@@ -48,13 +47,10 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import mihon.icons.materialsymbols.MaterialSymbols
import mihon.icons.materialsymbols.roundedfilled.CheckCircle
import tachiyomi.core.common.preference.InMemoryPreferenceStore
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.fullType
@Composable
internal fun AppThemePreferenceWidget(
@@ -261,7 +257,6 @@ fun AppThemePreviewItem(
@Composable
private fun AppThemesListPreview() {
var appTheme by remember { mutableStateOf(AppTheme.DEFAULT) }
Injekt.addSingleton(fullType<UiPreferences>(), UiPreferences(InMemoryPreferenceStore()))
TachiyomiTheme(appTheme = appTheme) {
Surface {
AppThemesList(
@@ -51,7 +51,10 @@ internal fun ColumnScope.GeneralPage(viewModel: ReaderSettingsViewModel) {
themes.map { (labelRes, value) ->
FilterChip(
selected = readerTheme == value,
onClick = { viewModel.preferences.readerTheme.set(value) },
onClick = {
viewModel.preferences.readerTheme.set(value)
viewModel.requestTextSettingsReload()
},
label = { Text(stringResource(labelRes)) },
)
}
@@ -29,13 +29,14 @@ fun ReaderSettingsDialog(
stringResource(MR.strings.pref_category_reading_mode),
stringResource(MR.strings.pref_category_general),
stringResource(MR.strings.custom_filter),
stringResource(MR.strings.pref_category_text_reader),
)
val pagerState = rememberPagerState { tabTitles.size }
BoxWithConstraints {
TabbedDialog(
modifier = Modifier.heightIn(max = maxHeight * 0.75f),
onDismissRequest = {
viewModel.flushTextSettingsReload()
onDismissRequest()
onShowMenus()
},
@@ -45,7 +46,7 @@ fun ReaderSettingsDialog(
val window = (LocalView.current.parent as? DialogWindowProvider)?.window
LaunchedEffect(pagerState.currentPage) {
if (pagerState.currentPage == 2) {
if (pagerState.currentPage == 2 || pagerState.currentPage == 3) {
window?.setDimAmount(0f)
onHideMenus()
} else {
@@ -63,6 +64,7 @@ fun ReaderSettingsDialog(
0 -> ReadingModePage(viewModel)
1 -> GeneralPage(viewModel)
2 -> ColorFilterPage(viewModel)
3 -> TextSettingsPage(viewModel)
}
}
}
@@ -0,0 +1,95 @@
package eu.kanade.presentation.reader.settings
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsViewModel
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.SettingsChipRow
import tachiyomi.presentation.core.components.SliderItem
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
private val fontFamilies = listOf(
MR.strings.pref_text_reader_font_system to ReaderPreferences.TextFontFamily.SYSTEM,
MR.strings.pref_text_reader_font_sans to ReaderPreferences.TextFontFamily.SANS_SERIF,
MR.strings.pref_text_reader_font_serif to ReaderPreferences.TextFontFamily.SERIF,
MR.strings.pref_text_reader_font_mono to ReaderPreferences.TextFontFamily.MONOSPACE,
)
@Composable
internal fun ColumnScope.TextSettingsPage(viewModel: ReaderSettingsViewModel) {
val fontSizePref = viewModel.preferences.textFontSize
val fontSize by fontSizePref.collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_text_reader_font_size),
value = fontSize,
valueRange = 10..30,
steps = 19,
valueString = "$fontSize sp",
onChange = { value ->
fontSizePref.set(value)
viewModel.requestTextSettingsReload()
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
val lineSpacingPref = viewModel.preferences.textLineSpacing
val lineSpacing by lineSpacingPref.collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_text_reader_line_spacing),
value = lineSpacing,
valueRange = 90..180,
steps = 17,
valueString = String.format("%.2fx", lineSpacing / 100f),
onChange = { value ->
lineSpacingPref.set(value)
viewModel.requestTextSettingsReload()
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
val paragraphSpacingPref = viewModel.preferences.textParagraphSpacing
val paragraphSpacing by paragraphSpacingPref.collectAsState()
SliderItem(
label = stringResource(MR.strings.pref_text_reader_paragraph_spacing),
value = paragraphSpacing,
valueRange = 0..16,
steps = 15,
valueString = "$paragraphSpacing dp",
onChange = { value ->
paragraphSpacingPref.set(value)
viewModel.requestTextSettingsReload()
},
pillColor = MaterialTheme.colorScheme.surfaceContainerHighest,
)
val fontFamilyPref = viewModel.preferences.textFontFamily
val fontFamily by fontFamilyPref.collectAsState()
SettingsChipRow(MR.strings.pref_text_reader_font_family) {
fontFamilies.forEach { (label, value) ->
FilterChip(
selected = fontFamily == value,
onClick = {
fontFamilyPref.set(value)
viewModel.requestTextSettingsReload()
},
label = { Text(stringResource(label)) },
)
}
}
Text(
text = stringResource(MR.strings.pref_text_reader_pdf_note),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
)
}
+3 -11
View File
@@ -52,7 +52,7 @@ import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import mihon.app.di.AppGraph
import mihon.app.di.injekt.MetroInteropModule
import mihon.app.di.injekt.MetroInjektRegistrar
import mihon.core.metro.GraphProvider
import mihon.core.migration.Migration
import mihon.core.migration.Migrator
@@ -66,7 +66,7 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.presentation.widget.WidgetManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.InjektScope
import java.security.Security
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory, GraphProvider<AppGraph> {
@@ -93,8 +93,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
@Inject private lateinit var widgetManager: WidgetManager
@Inject private lateinit var injektMetroInteropModule: MetroInteropModule
@Inject private lateinit var migrations: Set<Migration>
private val disableIncognitoReceiver = DisableIncognitoReceiver()
@@ -110,8 +108,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
Injekt = InjektScope(MetroInjektRegistrar(application = this, graphProvider = this))
graph.inject(this)
setupInjekt()
TelemetryConfig.init(applicationContext)
@@ -185,12 +183,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
initializeMigrator()
}
private fun setupInjekt() {
Injekt.addSingleton<Application>(this)
Injekt.addSingleton<Context>(this)
Injekt.importModule(injektMetroInteropModule)
}
private fun initializeMigrator() {
val preference = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0)
logcat {
@@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
@@ -24,8 +23,12 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -60,38 +63,51 @@ class ExtensionManager(
private val iconMap = mutableMapOf<String, Drawable>()
private val installedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Installed>())
val installedExtensionsFlow = installedExtensionMapFlow.mapExtensionsWhenInitialized()
private val loadedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Loaded>())
val loadedExtensionsFlow = loadedExtensionMapFlow.mapExtensionsWhenInitialized()
private val availableExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Available>())
val availableExtensionsFlow = availableExtensionMapFlow.mapExtensions(scope)
private val untrustedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.Untrusted>())
val untrustedExtensionsFlow = untrustedExtensionMapFlow.mapExtensionsWhenInitialized()
private val notLoadedExtensionMapFlow = MutableStateFlow(emptyMap<String, Extension.NotLoaded>())
val notLoadedExtensionsFlow = notLoadedExtensionMapFlow.mapExtensionsWhenInitialized()
init {
scope.launch(Dispatchers.IO) {
initExtensions()
loadExtensions()
ExtensionInstallReceiver(InstallationListener()).register(context)
// Everything the load decision rests on can change while running, so decide again
merge(
trustExtension.changes(),
preferences.enabledContentWarnings.changes().distinctUntilChanged().drop(1).map {},
preferences.applyContentWarningsToInstalled.changes().distinctUntilChanged().drop(1).map {},
)
.collectLatest { loadExtensions() }
}
}
private var subLanguagesEnabledOnFirstRun = preferences.enabledLanguages.isSet()
suspend fun getInstalledExtensions(): List<Extension.Installed> {
suspend fun getLoadedExtensions(): List<Extension.Loaded> {
initialized.await()
return installedExtensionMapFlow.value.values.toList()
return loadedExtensionMapFlow.value.values.toList()
}
suspend fun getNotLoadedExtensions(): List<Extension.NotLoaded> {
initialized.await()
return notLoadedExtensionMapFlow.value.values.toList()
}
suspend fun getExtensionPackage(sourceId: Long): String? {
return getInstalledExtensions().find { extension ->
return getLoadedExtensions().find { extension ->
extension.sources.any { it.id == sourceId }
}
?.pkgName
}
fun getExtensionPackageAsFlow(sourceId: Long): Flow<String?> {
return installedExtensionsFlow.map { extensions ->
return loadedExtensionsFlow.map { extensions ->
extensions.find { extension ->
extension.sources.any { it.id == sourceId }
}
@@ -120,25 +136,29 @@ class ExtensionManager(
fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
/**
* Loads and registers the installed extensions.
* Loads and registers the installed extensions. Safe to call again: every extension is judged
* again, so one can move between loaded and not loaded in either direction, while extensions
* that still pass keep the instances they already had.
*/
private fun initExtensions() {
private suspend fun loadExtensions() {
try {
val extensions = ExtensionLoader.loadExtensions(context)
val extensions = ExtensionLoader.loadExtensions(context, loadedExtensionMapFlow.value)
installedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Success>()
.associate { it.extension.pkgName to it.extension }
loadedExtensionMapFlow.value = extensions
.filterIsInstance<Extension.Loaded>()
.associateBy { it.pkgName }
untrustedExtensionMapFlow.value = extensions
.filterIsInstance<LoadResult.Untrusted>()
.associate { it.extension.pkgName to it.extension }
notLoadedExtensionMapFlow.value = extensions
.filterIsInstance<Extension.NotLoaded>()
.associateBy { it.pkgName }
initialized.complete(Unit)
// Newly loaded extensions have no status derived from the store index yet
updatedInstalledExtensionsStatuses(availableExtensionMapFlow.value.values.toList())
} catch (e: Throwable) {
// Release anything waiting on the extensions before the failure propagates
logcat(LogPriority.ERROR, e) { "Failed to load extensions" }
} finally {
// Release anything waiting on the extensions whether or not the load worked
initialized.complete(Unit)
throw e
}
}
@@ -202,23 +222,23 @@ class ExtensionManager(
return
}
val installedExtensionsMap = installedExtensionMapFlow.value.toMutableMap()
val loadedExtensionsMap = loadedExtensionMapFlow.value.toMutableMap()
var changed = false
for ((pkgName, extension) in installedExtensionsMap) {
for ((pkgName, extension) in loadedExtensionsMap) {
val availableExt = availableExtensions.find { it.pkgName == pkgName }
if (availableExt == null && !extension.isObsolete) {
installedExtensionsMap[pkgName] = extension.copy(isObsolete = true)
loadedExtensionsMap[pkgName] = extension.copy(isObsolete = true)
changed = true
} else if (availableExt != null) {
val hasUpdate = extension.updateExists(availableExt)
if (extension.hasUpdate != hasUpdate) {
installedExtensionsMap[pkgName] = extension.copy(
loadedExtensionsMap[pkgName] = extension.copy(
hasUpdate = hasUpdate,
store = availableExt.store,
)
} else {
installedExtensionsMap[pkgName] = extension.copy(
loadedExtensionsMap[pkgName] = extension.copy(
store = availableExt.store,
)
}
@@ -226,7 +246,7 @@ class ExtensionManager(
}
}
if (changed) {
installedExtensionMapFlow.value = installedExtensionsMap
loadedExtensionMapFlow.value = loadedExtensionsMap
}
updatePendingUpdatesCount()
}
@@ -249,7 +269,7 @@ class ExtensionManager(
*
* @param extension The extension to be updated.
*/
fun updateExtension(extension: Extension.Installed): Flow<InstallStep> {
fun updateExtension(extension: Extension.Loaded): Flow<InstallStep> {
val availableExt = availableExtensionMapFlow.value[extension.pkgName] ?: return emptyFlow()
val isUpdateForPrivatelyInstalled = !extension.isShared
return installer.downloadAndInstall(availableExt.apkUrl, availableExt, isUpdateForPrivatelyInstalled)
@@ -277,7 +297,7 @@ class ExtensionManager(
*
* @param extension The extension to uninstall.
*/
fun uninstallExtension(extension: Extension) {
fun uninstallExtension(extension: Extension.Installed) {
installer.uninstallApk(extension.pkgName)
}
@@ -287,16 +307,12 @@ class ExtensionManager(
*
* @param extension the extension to trust
*/
suspend fun trust(extension: Extension.Untrusted) {
untrustedExtensionMapFlow.value[extension.pkgName] ?: return
fun trust(extension: Extension.NotLoaded) {
val reason = extension.reason as? Extension.NotLoaded.Reason.Untrusted ?: return
notLoadedExtensionMapFlow.value[extension.pkgName] ?: return
trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash)
untrustedExtensionMapFlow.value -= extension.pkgName
ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName)
.let { it as? LoadResult.Success }
?.let { registerNewExtension(it.extension) }
// Loading it again is left to the reload triggered by the trust change
trustExtension.trust(extension.pkgName, extension.versionCode, reason.signatureHash)
}
/**
@@ -304,18 +320,8 @@ class ExtensionManager(
*
* @param extension The extension to be registered.
*/
private fun registerNewExtension(extension: Extension.Installed) {
installedExtensionMapFlow.value += extension
}
/**
* Registers the given updated extension in this and the source managers previously removing
* the outdated ones.
*
* @param extension The extension to be registered.
*/
private fun registerUpdatedExtension(extension: Extension.Installed) {
installedExtensionMapFlow.value += extension
private fun registerExtension(extension: Extension.Loaded) {
loadedExtensionMapFlow.value += extension
}
/**
@@ -325,8 +331,8 @@ class ExtensionManager(
* @param pkgName The package name of the uninstalled application.
*/
private fun unregisterExtension(pkgName: String) {
installedExtensionMapFlow.value -= pkgName
untrustedExtensionMapFlow.value -= pkgName
loadedExtensionMapFlow.value -= pkgName
notLoadedExtensionMapFlow.value -= pkgName
}
/**
@@ -334,19 +340,15 @@ class ExtensionManager(
*/
private inner class InstallationListener : ExtensionInstallReceiver.Listener {
override fun onExtensionInstalled(extension: Extension.Installed) {
registerNewExtension(extension.withUpdateCheck())
override fun onExtensionLoaded(extension: Extension.Loaded) {
registerExtension(extension.withUpdateCheck())
notLoadedExtensionMapFlow.value -= extension.pkgName
updatePendingUpdatesCount()
}
override fun onExtensionUpdated(extension: Extension.Installed) {
registerUpdatedExtension(extension.withUpdateCheck())
updatePendingUpdatesCount()
}
override fun onExtensionUntrusted(extension: Extension.Untrusted) {
installedExtensionMapFlow.value -= extension.pkgName
untrustedExtensionMapFlow.value += extension
override fun onExtensionNotLoaded(extension: Extension.NotLoaded) {
loadedExtensionMapFlow.value -= extension.pkgName
notLoadedExtensionMapFlow.value += extension
updatePendingUpdatesCount()
}
@@ -360,7 +362,7 @@ class ExtensionManager(
/**
* Extension method to set the update field of an installed extension.
*/
private fun Extension.Installed.withUpdateCheck(): Extension.Installed {
private fun Extension.Loaded.withUpdateCheck(): Extension.Loaded {
return if (updateExists()) {
copy(hasUpdate = true)
} else {
@@ -368,7 +370,7 @@ class ExtensionManager(
}
}
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
private fun Extension.Loaded.updateExists(availableExtension: Extension.Available? = null): Boolean {
val availableExt = availableExtension
?: availableExtensionMapFlow.value[pkgName]
?: return false
@@ -377,7 +379,7 @@ class ExtensionManager(
}
private fun updatePendingUpdatesCount() {
val pendingUpdateCount = installedExtensionMapFlow.value.values.count { it.hasUpdate }
val pendingUpdateCount = loadedExtensionMapFlow.value.values.count { it.hasUpdate }
preferences.extensionUpdatesCount.set(pendingUpdateCount)
if (pendingUpdateCount == 0) {
extensionUpdateNotifier.dismiss()
@@ -1,12 +1,9 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import mihon.domain.extension.interactor.UpdateExtensionStores
import mihon.domain.extension.repository.ExtensionStoreRepository
import tachiyomi.core.common.util.lang.withIOContext
@@ -23,17 +20,17 @@ class ExtensionApi(
return withIOContext { repository.fetchExtensions() }
}
suspend fun checkForUpdates(context: Context) {
/**
* @param loadedExtensions Extensions already loaded by [eu.kanade.tachiyomi.extension.ExtensionManager].
* Only their versions are read, so there's nothing to gain from loading them a second time.
*/
suspend fun checkForUpdates(loadedExtensions: List<Extension.Loaded>) {
updateExtensionStores()
val extensions = findExtensions()
val installedExtensions = ExtensionLoader.loadExtensions(context)
.filterIsInstance<LoadResult.Success>()
.map { it.extension }
val extensionsWithUpdate = mutableListOf<Extension.Installed>()
for (installedExt in installedExtensions) {
val extensionsWithUpdate = mutableListOf<Extension.Loaded>()
for (installedExt in loadedExtensions) {
val pkgName = installedExt.pkgName
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
@@ -5,7 +5,7 @@ import android.net.Uri
import androidx.annotation.CallSuper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep
import uy.kohesive.injekt.injectLazy
import mihon.app.di.appGraph
import java.util.Collections
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.concurrent.atomics.AtomicReference
@@ -17,7 +17,7 @@ import kotlin.concurrent.atomics.ExperimentalAtomicApi
@OptIn(ExperimentalAtomicApi::class)
abstract class Installer(private val service: Service) {
private val extensionManager: ExtensionManager by injectLazy()
private val extensionManager: ExtensionManager by lazy { service.appGraph.extensionManager }
private var waitingInstall = AtomicReference<Entry?>(null)
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
@@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.extension.model
sealed interface LoadResult {
data class Success(val extension: Extension.Installed) : LoadResult
data class Untrusted(val extension: Extension.Untrusted) : LoadResult
data object Error : LoadResult
}
@@ -8,7 +8,6 @@ import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@@ -51,19 +50,19 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc
if (isReplacing(intent)) return
scope.launch {
when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
else -> {}
when (val extension = getExtensionFromIntent(context, intent)) {
is Extension.Loaded -> listener.onExtensionLoaded(extension)
is Extension.NotLoaded -> listener.onExtensionNotLoaded(extension)
null -> {}
}
}
}
Intent.ACTION_PACKAGE_REPLACED, ACTION_EXTENSION_REPLACED -> {
scope.launch {
when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
else -> {}
when (val extension = getExtensionFromIntent(context, intent)) {
is Extension.Loaded -> listener.onExtensionLoaded(extension)
is Extension.NotLoaded -> listener.onExtensionNotLoaded(extension)
null -> {}
}
}
}
@@ -93,11 +92,11 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc
* @param context The application context.
* @param intent The intent containing the package name of the extension.
*/
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): Extension.Installed? {
val pkgName = getPackageNameFromIntent(intent)
if (pkgName == null) {
logcat(LogPriority.WARN) { "Package name not found" }
return LoadResult.Error
return null
}
return ExtensionLoader.loadExtensionFromPkgName(context, pkgName)
}
@@ -113,9 +112,8 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : Broadc
* Listener that receives extension installation events.
*/
interface Listener {
fun onExtensionInstalled(extension: Extension.Installed)
fun onExtensionUpdated(extension: Extension.Installed)
fun onExtensionUntrusted(extension: Extension.Untrusted)
fun onExtensionLoaded(extension: Extension.Loaded)
fun onExtensionNotLoaded(extension: Extension.NotLoaded)
fun onPackageUninstalled(pkgName: String)
}
@@ -8,19 +8,17 @@ import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import logcat.LogPriority
import mihon.app.di.appGraph
import mihon.data.dalvik.DelegateLastClassLoaderCompat
import mihon.domain.extension.model.ContentWarning
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import java.io.File
@@ -111,8 +109,19 @@ internal object ExtensionLoader {
* Return a list of all the available extensions initialized concurrently.
*
* @param context The application context.
* @param alreadyLoaded Extensions loaded by an earlier call. Any of these whose apk is unchanged
* and which still passes every check is returned as is, so its sources keep working and its
* update status survives. Pass nothing to load every extension from scratch.
*/
fun loadExtensions(context: Context): List<LoadResult> {
suspend fun loadExtensions(
context: Context,
alreadyLoaded: Map<String, Extension.Loaded> = emptyMap(),
): List<Extension.Installed> {
val trustExtension = context.appGraph.trustExtension
val sourcePreferences = context.appGraph.sourcePreferences
val enabledContentWarnings = sourcePreferences.enabledContentWarnings.get()
val applyContentWarningsToInstalled = sourcePreferences.applyContentWarningsToInstalled.get()
val pkgManager = context.packageManager
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -138,7 +147,7 @@ internal object ExtensionLoader {
val path = it.absolutePath
pkgManager.getPackageArchiveInfo(path, PACKAGE_FLAGS)
?.apply { applicationInfo!!.fixBasePaths(path) }
?.also { pkg -> pkg.applicationInfo?.fixBasePaths(path) }
}
?.filter { isPackageAnExtension(it) }
?.map { ExtensionInfo(packageInfo = it, isShared = false) }
@@ -158,11 +167,21 @@ internal object ExtensionLoader {
if (extPkgs.isEmpty()) return emptyList()
// Load each extension concurrently and wait for completion
return runBlocking(Dispatchers.IO) {
val deferred = extPkgs.map {
async { loadExtension(context, it) }
}
deferred.awaitAll()
return withIOContext {
extPkgs
.map {
async {
loadExtensionCatching(
context = context,
extensionInfo = it,
trustExtension = trustExtension,
enabledContentWarnings = enabledContentWarnings,
applyContentWarningsToInstalled = applyContentWarningsToInstalled,
alreadyLoaded = alreadyLoaded[it.packageInfo.packageName],
)
}
}
.awaitAll()
}
}
@@ -170,13 +189,21 @@ internal object ExtensionLoader {
* Attempts to load an extension from the given package name. It checks if the extension
* contains the required feature flag before trying to load it.
*/
suspend fun loadExtensionFromPkgName(context: Context, pkgName: String): LoadResult {
suspend fun loadExtensionFromPkgName(context: Context, pkgName: String): Extension.Installed? {
val extensionPackage = getExtensionInfoFromPkgName(context, pkgName)
if (extensionPackage == null) {
logcat(LogPriority.ERROR) { "Extension package is not found ($pkgName)" }
return LoadResult.Error
return null
}
return loadExtension(context, extensionPackage)
val sourcePreferences = context.appGraph.sourcePreferences
return loadExtensionCatching(
context = context,
extensionInfo = extensionPackage,
trustExtension = context.appGraph.trustExtension,
enabledContentWarnings = sourcePreferences.enabledContentWarnings.get(),
applyContentWarningsToInstalled = sourcePreferences.applyContentWarningsToInstalled.get(),
)
}
fun getExtensionPackageInfoFromPkgName(context: Context, pkgName: String): PackageInfo? {
@@ -189,7 +216,7 @@ internal object ExtensionLoader {
context.packageManager.getPackageArchiveInfo(privateExtensionFile.absolutePath, PACKAGE_FLAGS)
?.takeIf { isPackageAnExtension(it) }
?.let {
it.applicationInfo!!.fixBasePaths(privateExtensionFile.absolutePath)
it.applicationInfo?.fixBasePaths(privateExtensionFile.absolutePath)
ExtensionInfo(
packageInfo = it,
isShared = false,
@@ -215,33 +242,107 @@ internal object ExtensionLoader {
return selectExtensionPackage(sharedPkg, privatePkg)
}
/**
* [loadExtension] reports the failures it knows how to name, but an apk can be malformed in
* ways it doesn't check for. Keep anything unforeseen to the extension that caused it instead of
* letting it take down the load of every other extension.
*/
private suspend fun loadExtensionCatching(
context: Context,
extensionInfo: ExtensionInfo,
trustExtension: TrustExtension,
enabledContentWarnings: Set<ContentWarning>,
applyContentWarningsToInstalled: Boolean,
alreadyLoaded: Extension.Loaded? = null,
): Extension.Installed {
return try {
loadExtension(
context = context,
extensionInfo = extensionInfo,
trustExtension = trustExtension,
enabledContentWarnings = enabledContentWarnings,
applyContentWarningsToInstalled = applyContentWarningsToInstalled,
alreadyLoaded = alreadyLoaded,
)
} catch (e: Throwable) {
val pkgInfo = extensionInfo.packageInfo
logcat(LogPriority.ERROR, e) { "Extension load error: ${pkgInfo.packageName}" }
Extension.NotLoaded(
name = pkgInfo.packageName,
pkgName = pkgInfo.packageName,
versionName = pkgInfo.versionName.orEmpty(),
versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo),
isShared = extensionInfo.isShared,
contentWarning = ContentWarning.SAFE,
reason = Extension.NotLoaded.Reason.Failed(e.rootMessage, e.stackTraceToString()),
)
}
}
/**
* Loads an extension
*
* @param context The application context.
* @param extensionInfo The extension to load.
*/
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
val trustExtension: TrustExtension = context.appGraph.trustExtension
val enabledContentWarnings = context.appGraph.sourcePreferences.enabledContentWarnings.get()
private suspend fun loadExtension(
context: Context,
extensionInfo: ExtensionInfo,
trustExtension: TrustExtension,
enabledContentWarnings: Set<ContentWarning>,
applyContentWarningsToInstalled: Boolean,
alreadyLoaded: Extension.Loaded? = null,
): Extension.Installed {
val pkgManager = context.packageManager
val pkgInfo = extensionInfo.packageInfo
val appInfo = pkgInfo.applicationInfo!!
val appInfo = pkgInfo.applicationInfo
val metaData = appInfo?.metaData
val pkgName = pkgInfo.packageName
val extName = appInfo.metaData.getString(METADATA_NAME)
?: pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
val extName = metaData?.getString(METADATA_NAME)
?: appInfo?.let { pkgManager.getApplicationLabel(it).toString().substringAfter("Tachiyomi: ") }
?: pkgName
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
val contentWarning = when {
metaData == null -> ContentWarning.SAFE
metaData.containsKey(METADATA_CONTENT_WARNING) -> {
when (metaData.getInt(METADATA_CONTENT_WARNING)) {
1 -> ContentWarning.MIXED
2 -> ContentWarning.NSFW
else -> ContentWarning.SAFE
}
}
metaData.getInt(METADATA_NSFW) == 1 -> ContentWarning.NSFW
else -> ContentWarning.SAFE
}
fun notLoaded(
reason: Extension.NotLoaded.Reason,
libVersion: Double? = null,
) = Extension.NotLoaded(
name = extName,
pkgName = pkgName,
versionName = versionName.orEmpty(),
versionCode = versionCode,
isShared = extensionInfo.isShared,
contentWarning = contentWarning,
libVersion = libVersion,
reason = reason,
)
if (appInfo == null || metaData == null) {
logcat(LogPriority.WARN) { "Missing application info for extension $extName" }
return notLoaded(Extension.NotLoaded.Reason.Malformed)
}
if (versionName.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Missing versionName for extension $extName" }
return LoadResult.Error
return notLoaded(Extension.NotLoaded.Reason.Malformed)
}
// Validate lib version
val libVersion = appInfo.metaData.getFloat(METADATA_EXTENSION_LIB)
val libVersion = metaData.getFloat(METADATA_EXTENSION_LIB)
.takeUnless { it == 0.0f }
?.toString()
?.toDouble()
@@ -250,50 +351,46 @@ internal object ExtensionLoader {
logcat(LogPriority.WARN) {
"Lib version is $libVersion, while only version(s) ${SUPPORTED_LIB_VERSIONS.joinToString()} are supported"
}
return LoadResult.Error
return notLoaded(Extension.NotLoaded.Reason.UnsupportedLibVersion, libVersion)
}
val signatures = getSignatures(pkgInfo)
if (signatures.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error
return notLoaded(Extension.NotLoaded.Reason.Unsigned, libVersion)
} else if (!trustExtension.isTrusted(pkgInfo, signatures)) {
val extension = Extension.Untrusted(
extName,
pkgName,
versionName,
versionCode,
libVersion,
signatures.last(),
)
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
return LoadResult.Untrusted(extension)
return notLoaded(Extension.NotLoaded.Reason.Untrusted(signatures.last()), libVersion)
}
val contentWarning = when {
appInfo.metaData.containsKey(METADATA_CONTENT_WARNING) -> {
when (appInfo.metaData.getInt(METADATA_CONTENT_WARNING)) {
1 -> ContentWarning.MIXED
2 -> ContentWarning.NSFW
else -> ContentWarning.SAFE
}
}
appInfo.metaData.getInt(METADATA_NSFW) == 1 -> ContentWarning.NSFW
else -> ContentWarning.SAFE
}
if (contentWarning !in enabledContentWarnings) {
if (applyContentWarningsToInstalled && contentWarning !in enabledContentWarnings) {
logcat(LogPriority.WARN) { "Extension $pkgName with $contentWarning not allowed" }
return LoadResult.Error
return notLoaded(Extension.NotLoaded.Reason.Filtered, libVersion)
}
// Everything above is cheap to check again, everything below isn't. Nothing about this apk
// changed and it still passes, so keep the sources that are already registered for it.
if (alreadyLoaded != null &&
alreadyLoaded.versionCode == versionCode &&
alreadyLoaded.isShared == extensionInfo.isShared
) {
return alreadyLoaded
}
val classLoader = try {
DelegateLastClassLoaderCompat(appInfo.sourceDir, null, context.classLoader)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" }
return LoadResult.Error
return notLoaded(Extension.NotLoaded.Reason.Failed(e.rootMessage, e.stackTraceToString()), libVersion)
}
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
val sourceClasses = metaData.getString(METADATA_SOURCE_CLASS)
if (sourceClasses.isNullOrBlank()) {
logcat(LogPriority.WARN) { "Missing source class for extension $extName" }
return notLoaded(Extension.NotLoaded.Reason.Malformed, libVersion)
}
val sources = sourceClasses
.split(";")
.map {
val sourceClass = it.trim()
@@ -312,7 +409,10 @@ internal object ExtensionLoader {
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
return LoadResult.Error
return notLoaded(
Extension.NotLoaded.Reason.Failed(e.rootMessage, e.stackTraceToString()),
libVersion,
)
}
}
@@ -323,7 +423,7 @@ internal object ExtensionLoader {
else -> "all"
}
val extension = Extension.Installed(
return Extension.Loaded(
name = extName,
pkgName = pkgName,
versionName = versionName,
@@ -332,11 +432,10 @@ internal object ExtensionLoader {
lang = lang,
contentWarning = contentWarning,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
icon = appInfo.loadIcon(pkgManager),
pkgFactory = metaData.getString(METADATA_SOURCE_FACTORY),
icon = runCatching { appInfo.loadIcon(pkgManager) }.getOrNull(),
isShared = extensionInfo.isShared,
)
return LoadResult.Success(extension)
}
/**
@@ -378,11 +477,11 @@ internal object ExtensionLoader {
*/
private fun getSignatures(pkgInfo: PackageInfo): List<String>? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val signingInfo = pkgInfo.signingInfo!!
if (signingInfo.hasMultipleSigners()) {
signingInfo.apkContentsSigners
} else {
signingInfo.signingCertificateHistory
val signingInfo = pkgInfo.signingInfo
when {
signingInfo == null -> null
signingInfo.hasMultipleSigners() -> signingInfo.apkContentsSigners
else -> signingInfo.signingCertificateHistory
}
} else {
@Suppress("DEPRECATION")
@@ -410,3 +509,12 @@ internal object ExtensionLoader {
val isShared: Boolean,
)
}
/**
* The message of the deepest cause, which is the one that actually says what went wrong.
*/
private val Throwable.rootMessage: String
get() {
val root = generateSequence(this) { it.cause }.last()
return listOfNotNull(root::class.simpleName, root.message).joinToString(": ")
}
@@ -48,7 +48,7 @@ class AndroidSourceManager(
init {
scope.launch {
extensionManager.installedExtensionsFlow
extensionManager.loadedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, Source>(
mapOf(LocalSource.ID to localSource),
@@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.source.bookoasis
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Request
import org.json.JSONObject
import java.net.URI
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
private const val BOOKOASIS_PACKAGE_PREFIX = "eu.kanade.tachiyomi.extension.all.bookoasis."
private val SUPPORTED_FORMATS = setOf("zip", "cbz", "imgdir", "epub", "pdf", "txt")
private val DOCUMENT_FORMATS = setOf("epub", "pdf", "txt")
internal data class BookOasisChapterRef(
val bookId: Long,
val dbType: String,
val format: String,
)
internal fun Source.isBookOasisSource(): Boolean {
return this::class.java.name.startsWith(BOOKOASIS_PACKAGE_PREFIX)
}
internal fun String.toBookOasisChapterRef(): BookOasisChapterRef? {
val path = substringBefore('?')
val match = Regex("""^/api/media/books/(\d+)/info$""").matchEntire(path) ?: return null
val params = parseQueryParameters(this)
val format = params["bo_format"]?.lowercase() ?: return null
if (format !in DOCUMENT_FORMATS) return null
return BookOasisChapterRef(
bookId = match.groupValues[1].toLongOrNull() ?: return null,
dbType = params["type"].orEmpty().ifBlank { "general" },
format = format,
)
}
/**
* BookOasis extension v1.4.x filters its chapter list to ZIP/CBZ.
* Fetch the same detail JSON with the extension's authenticated client and
* rebuild the chapter list so EPUB/PDF/TXT remain visible to the host app.
*/
internal suspend fun HttpSource.fetchBookOasisChapters(manga: SManga): List<SChapter> {
val detailUrl = manga.url.toAbsoluteUrl(baseUrl)
val dbType = parseQueryParameters(detailUrl)["type"].orEmpty().ifBlank { "general" }
val request = Request.Builder()
.url(detailUrl)
.headers(headers)
.get()
.build()
val root = client.newCall(request).await().use { response ->
if (!response.isSuccessful) {
error("BookOasis detail request failed: HTTP ${response.code}")
}
JSONObject(response.body.string())
}
val rawBooks = root.optJSONArray("books") ?: return emptyList()
val books = buildList {
for (index in 0 until rawBooks.length()) {
val item = rawBooks.optJSONObject(index) ?: continue
val id = item.optLong("id", -1L)
val format = item.optString("file_format", "").lowercase()
if (id <= 0L || format !in SUPPORTED_FORMATS) continue
add(
Triple(
id,
item.optString("title", "").ifBlank { "Book $id" },
format,
),
)
}
}
return books.mapIndexed { index, (bookId, title, format) ->
SChapter.create().apply {
url = buildString {
append("/api/media/books/")
append(bookId)
append("/info?type=")
append(dbType)
if (format in DOCUMENT_FORMATS) {
append("&bo_format=")
append(format)
}
}
name = title
chapter_number = (books.size - index).toFloat()
}
}.reversed()
}
private fun String.toAbsoluteUrl(baseUrl: String): String {
return if (startsWith("http://") || startsWith("https://")) {
this
} else {
baseUrl.trimEnd('/') + "/" + trimStart('/')
}
}
private fun parseQueryParameters(urlOrPath: String): Map<String, String> {
val rawQuery = runCatching {
if (urlOrPath.startsWith("http://") || urlOrPath.startsWith("https://")) {
URI(urlOrPath).rawQuery
} else {
urlOrPath.substringAfter('?', "")
}
}.getOrDefault(urlOrPath.substringAfter('?', ""))
if (rawQuery.isNullOrBlank()) return emptyMap()
return rawQuery.split('&')
.mapNotNull { part ->
if (part.isBlank()) return@mapNotNull null
val key = part.substringBefore('=')
val value = part.substringAfter('=', "")
decodeQuery(key) to decodeQuery(value)
}
.toMap()
}
private fun decodeQuery(value: String): String {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name())
}
@@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.source.kavita
import eu.kanade.tachiyomi.source.Source
private const val KAVITA_PACKAGE_PREFIX = "eu.kanade.tachiyomi.extension.all.kavita."
internal fun Source.isKavitaSource(): Boolean =
this::class.java.name.startsWith(KAVITA_PACKAGE_PREFIX)
/**
* Kavita extension chapter URLs are normally /Chapter/{id}. Keep compatibility
* with older/alternate forms used by reading lists and previous releases.
*/
internal fun String.toKavitaChapterId(): Int? {
Regex("""(?:^|/)Chapter/(\d+)""", RegexOption.IGNORE_CASE)
.find(this)
?.groupValues
?.getOrNull(1)
?.toIntOrNull()
?.let { return it }
Regex("""(?:^|/)chapter_(\d+)""", RegexOption.IGNORE_CASE)
.find(this)
?.groupValues
?.getOrNull(1)
?.toIntOrNull()
?.let { return it }
Regex("""[?&]chapterId=(\d+)""", RegexOption.IGNORE_CASE)
.find(this)
?.groupValues
?.getOrNull(1)
?.toIntOrNull()
?.let { return it }
return null
}
@@ -32,7 +32,7 @@ fun extensionsTab(
val context = LocalContext.current
val updatesCount by extensionsViewModel.updatesCount.collectAsStateWithLifecycle()
var privateExtensionToUninstall by remember { mutableStateOf<Extension?>(null) }
var privateExtensionToUninstall by remember { mutableStateOf<Extension.Installed?>(null) }
return TabContent(
titleRes = MR.strings.label_extensions,
@@ -62,7 +62,7 @@ fun extensionsTab(
onLongClickItem = { extension ->
when (extension) {
is Extension.Available -> extensionsViewModel.installExtension(extension)
else -> {
is Extension.Installed -> {
if (context.isPackageInstalled(extension.pkgName)) {
extensionsViewModel.uninstallExtension(extension)
} else {
@@ -74,17 +74,21 @@ class ExtensionsViewModel(
.map { searchQueryPredicate(it ?: "") },
currentDownloads,
getExtensions.subscribe(),
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
) { predicate, downloads, (_updates, _loaded, _available, _notLoaded) ->
buildMap {
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
if (updates.isNotEmpty()) {
put(ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending), updates)
}
val installed = _installed.filter(predicate).map(extensionMapper(downloads))
val untrusted = _untrusted.filter(predicate).map(extensionMapper(downloads))
if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
put(ExtensionUiModel.Header.Resource(MR.strings.ext_installed), installed + untrusted)
val notLoaded = _notLoaded.filter(predicate).map(extensionMapper(downloads))
if (notLoaded.isNotEmpty()) {
put(ExtensionUiModel.Header.Resource(MR.strings.ext_not_loaded), notLoaded)
}
val loaded = _loaded.filter(predicate).map(extensionMapper(downloads))
if (loaded.isNotEmpty()) {
put(ExtensionUiModel.Header.Resource(MR.strings.ext_installed), loaded)
}
val languagesWithExtensions = _available
@@ -138,7 +142,7 @@ class ExtensionsViewModel(
if (extension.name.contains(subquery, ignoreCase = true)) return@any true
when (extension) {
is Extension.Installed -> extension.sources.any { source ->
is Extension.Loaded -> extension.sources.any { source ->
source.name.contains(subquery, ignoreCase = true) ||
(source as? HttpSource)?.getHomeUrl()?.contains(subquery, ignoreCase = true) == true ||
source.id == subquery.toLongOrNull()
@@ -164,7 +168,7 @@ class ExtensionsViewModel(
viewModelScope.launchIO {
state.value.items.values.flatten()
.map { it.extension }
.filterIsInstance<Extension.Installed>()
.filterIsInstance<Extension.Loaded>()
.filter { it.hasUpdate }
.forEach(::updateExtension)
}
@@ -176,7 +180,7 @@ class ExtensionsViewModel(
}
}
fun updateExtension(extension: Extension.Installed) {
fun updateExtension(extension: Extension.Loaded) {
viewModelScope.launchIO {
extensionManager.updateExtension(extension).collectToInstallUpdate(extension)
}
@@ -202,7 +206,7 @@ class ExtensionsViewModel(
.onCompletion { removeDownloadState(extension) }
.collect()
fun uninstallExtension(extension: Extension) {
fun uninstallExtension(extension: Extension.Installed) {
extensionManager.uninstallExtension(extension)
}
@@ -219,10 +223,8 @@ class ExtensionsViewModel(
}
}
fun trustExtension(extension: Extension.Untrusted) {
viewModelScope.launch {
extensionManager.trust(extension)
}
fun trustExtension(extension: Extension.NotLoaded) {
extensionManager.trust(extension)
}
@Immutable
@@ -56,7 +56,7 @@ class ExtensionDetailsViewModel(
fun create(pkgName: String): ExtensionDetailsViewModel
}
val state: StateFlow<State> = extensionManager.installedExtensionsFlow
val state: StateFlow<State> = extensionManager.loadedExtensionsFlow
.map { it.firstOrNull { extension -> extension.pkgName == pkgName } }
.distinctUntilChanged()
.flatMapLatest { extension ->
@@ -73,7 +73,7 @@ class ExtensionDetailsViewModel(
private val successState: State.Success?
get() = state.value as? State.Success
private fun subscribeToSources(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
private fun subscribeToSources(extension: Extension.Loaded): Flow<List<ExtensionSourceItem>> {
return getExtensionSources.subscribe(extension)
.map {
it.sortedWith(
@@ -142,7 +142,7 @@ class ExtensionDetailsViewModel(
@Immutable
data class Success(
val extension: Extension.Installed,
val extension: Extension.Loaded,
val isIncognito: Boolean,
val sources: List<ExtensionSourceItem>,
) : State
@@ -106,7 +106,7 @@ abstract class SearchViewModel(
return enabledSources
}
return extensionManager.getInstalledExtensions()
return extensionManager.getLoadedExtensions()
.filter { it.pkgName == filter }
.flatMap { it.sources }
.filter { it in enabledSources }
@@ -79,6 +79,7 @@ import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
@@ -142,6 +143,8 @@ class MainActivity : BaseActivity() {
@Inject private lateinit var extensionApi: ExtensionApi
@Inject private lateinit var extensionManager: ExtensionManager
// To be checked by splash screen. If true then splash screen will be removed.
var ready = false
@@ -339,7 +342,7 @@ class MainActivity : BaseActivity() {
// Extensions updates
LaunchedEffect(Unit) {
try {
extensionApi.checkForUpdates(context)
extensionApi.checkForUpdates(extensionManager.getLoadedExtensions())
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
@@ -223,6 +223,16 @@ class ReaderActivity : BaseActivity() {
ReaderViewModel.Event.ReloadViewerChapters -> {
viewModel.state.value.viewerChapters?.let(::setChapters)
}
ReaderViewModel.Event.RefreshCurrentTextPage -> {
viewModel.state.value.viewer?.refreshCurrentPage()
}
ReaderViewModel.Event.ReloadTextViewerChapters -> {
val state = viewModel.state.value
val chapters = state.viewerChapters
if (chapters != null) {
state.viewer?.refreshTextChapters(chapters)
}
}
ReaderViewModel.Event.PageChanged -> {
displayRefreshHost.flash()
}
@@ -254,6 +264,8 @@ class ReaderActivity : BaseActivity() {
readerState = viewModel.state,
onChangeReadingMode = viewModel::setMangaReadingMode,
onChangeOrientation = viewModel::setMangaOrientationType,
onTextSettingsPreview = viewModel::previewTextReaderStyle,
onTextSettingsCommit = viewModel::commitTextReaderStyle,
preferences = readerPreferences,
)
}
@@ -40,6 +40,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
import eu.kanade.tachiyomi.ui.reader.loader.TextReaderPage
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@@ -56,6 +57,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -90,6 +92,7 @@ import tachiyomi.domain.source.service.SourceManager
import tachiyomi.source.local.image.LocalCoverManager
import tachiyomi.source.local.isLocal
import java.util.Date
import kotlin.math.roundToInt
import kotlin.time.Clock
/**
@@ -326,7 +329,7 @@ class ReaderViewModel(
mutableState.update { it.copy(manga = manga, source = source) }
if (chapterId == -1L) chapterId = initialChapterId
loader = ChapterLoader(context, downloadManager, downloadProvider, chapterCache, manga, source)
loader = ChapterLoader(context, downloadManager, downloadProvider, chapterCache, manga, source, readerPreferences)
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
} catch (e: Throwable) {
@@ -822,6 +825,40 @@ class ReaderViewModel(
mutableState.update { it.copy(dialog = null) }
}
fun previewTextReaderStyle() {
val chapter = state.value.currentChapter ?: return
if (chapter.pageLoader?.usesTextReaderStyle != true) return
val style = readerPreferences.textReaderStyle(context)
val textPages = chapter.pages?.filterIsInstance<TextReaderPage>().orEmpty()
if (textPages.isEmpty()) return
textPages.forEach { it.updateStyle(style) }
eventChannel.trySend(Event.RefreshCurrentTextPage)
}
fun commitTextReaderStyle() {
val chapter = state.value.currentChapter ?: return
if (chapter.pageLoader?.usesTextReaderStyle != true) return
if (chapter.pages?.any { it is TextReaderPage } != true) return
val chapterLoader = loader ?: return
val requestedPage = (state.value.currentPage - 1).coerceAtLeast(0)
val oldLastIndex = (chapter.pages?.lastIndex ?: 0).coerceAtLeast(0)
val readingProgress = if (oldLastIndex == 0) 0f else requestedPage.toFloat() / oldLastIndex
viewModelScope.launchIO {
try {
val oldLoader = chapterLoader.reloadTextChapter(chapter, readingProgress) ?: return@launchIO
eventChannel.send(Event.ReloadTextViewerChapters)
// Give old visible holders time to detach before closing resources such as EpubReader.
delay(2000)
oldLoader.recycle()
} catch (e: Throwable) {
if (e is CancellationException) throw e
logcat(LogPriority.ERROR, e) { "Failed to repaginate text reader style" }
}
}
}
fun setBrightnessOverlayValue(value: Int) {
mutableState.update { it.copy(brightnessOverlayValue = value) }
}
@@ -1011,6 +1048,8 @@ class ReaderViewModel(
sealed interface Event {
data object ReloadViewerChapters : Event
data object RefreshCurrentTextPage : Event
data object ReloadTextViewerChapters : Event
data object PageChanged : Event
data class SetOrientation(val orientation: Int) : Event
data class SetCoverResult(val result: SetAsCoverResult) : Event
@@ -0,0 +1,110 @@
package eu.kanade.tachiyomi.ui.reader.loader
import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.bookoasis.BookOasisChapterRef
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
import mihon.core.archive.epubReader
import okhttp3.Request
import java.io.File
import java.io.IOException
/**
* Opens BookOasis EPUB/PDF/TXT as native Mihon reader pages.
*/
internal class BookOasisFilePageLoader(
private val context: Context,
private val source: HttpSource,
private val ref: BookOasisChapterRef,
private val textStyle: TextReaderStyle,
) : PageLoader() {
override var isLocal: Boolean = true
override val usesTextReaderStyle: Boolean = ref.format == "epub" || ref.format == "txt"
private var delegate: PageLoader? = null
override suspend fun getPages(): List<ReaderPage> {
check(!isRecycled)
val file = getOrDownloadFile()
val uniFile = UniFile.fromFile(file)
?: error("Unable to open cached BookOasis file: ${file.absolutePath}")
val loader = when (ref.format) {
"epub" -> EpubPageLoader(uniFile.epubReader(context), textStyle, context.cacheDir)
"pdf" -> PdfPageLoader(context, uniFile)
"txt" -> TextPageLoader(uniFile, textStyle, context.cacheDir)
else -> error("Unsupported BookOasis document format: ${ref.format}")
}
delegate = loader
return loader.getPages()
}
override suspend fun loadPage(page: ReaderPage) {
delegate?.loadPage(page)
}
override fun retryPage(page: ReaderPage) {
delegate?.retryPage(page)
}
override fun recycle() {
delegate?.recycle()
delegate = null
super.recycle()
}
private suspend fun getOrDownloadFile(): File {
val cacheDir = File(context.cacheDir, "bookoasis_reader").apply { mkdirs() }
val target = File(cacheDir, "${ref.dbType}-${ref.bookId}.${ref.format}")
if (target.isFile && target.length() > 0L) {
return target
}
val temp = File(cacheDir, "${target.name}.part")
if (temp.exists()) temp.delete()
val downloadUrl = buildString {
append(source.baseUrl.trimEnd('/'))
append("/api/media/books/")
append(ref.bookId)
append("/download?type=")
append(ref.dbType)
}
val request = Request.Builder()
.url(downloadUrl)
.headers(source.headers)
.get()
.build()
source.client.newCall(request).await().use { response ->
if (!response.isSuccessful) {
throw IOException(
"BookOasis ${ref.format.uppercase()} download failed: HTTP ${response.code}",
)
}
temp.outputStream().use { output ->
response.body.byteStream().use { input ->
input.copyTo(output)
}
}
}
if (temp.length() <= 0L) {
temp.delete()
throw IOException("BookOasis returned an empty ${ref.format.uppercase()} file")
}
if (!temp.renameTo(target)) {
temp.copyTo(target, overwrite = true)
temp.delete()
}
return target
}
}
@@ -5,8 +5,13 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.bookoasis.isBookOasisSource
import eu.kanade.tachiyomi.source.bookoasis.toBookOasisChapterRef
import eu.kanade.tachiyomi.source.kavita.isKavitaSource
import eu.kanade.tachiyomi.source.kavita.toKavitaChapterId
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import mihon.core.archive.archiveReader
import mihon.core.archive.epubReader
import tachiyomi.core.common.i18n.stringResource
@@ -17,6 +22,7 @@ import tachiyomi.domain.source.model.StubSource
import tachiyomi.i18n.MR
import tachiyomi.source.local.LocalSource
import tachiyomi.source.local.io.Format
import kotlin.math.roundToInt
/**
* Loader used to retrieve the [PageLoader] for a given chapter.
@@ -28,6 +34,7 @@ class ChapterLoader(
private val chapterCache: ChapterCache,
private val manga: Manga,
private val source: Source,
private val readerPreferences: ReaderPreferences,
) {
/**
@@ -67,6 +74,36 @@ class ChapterLoader(
}
}
suspend fun reloadTextChapter(chapter: ReaderChapter, readingProgress: Float): PageLoader? {
return withIOContext {
val newLoader = getPageLoader(chapter)
if (!newLoader.usesTextReaderStyle) {
newLoader.recycle()
return@withIOContext null
}
try {
val pages = newLoader.getPages()
.onEach { it.chapter = chapter }
if (pages.isEmpty()) {
throw Exception(context.stringResource(MR.strings.page_list_empty_error))
}
val oldLoader = chapter.pageLoader
chapter.pageLoader = newLoader
chapter.requestedPage = (readingProgress * pages.lastIndex)
.roundToInt()
.coerceIn(0, pages.lastIndex)
chapter.state = ReaderChapter.State.Loaded(pages)
oldLoader
} catch (e: Throwable) {
newLoader.recycle()
throw e
}
}
}
/**
* Checks [chapter] to be loaded based on present pages and loader in addition to state.
*/
@@ -86,6 +123,9 @@ class ChapterLoader(
manga.title,
source,
)
val bookOasisRef = dbChapter.url.toBookOasisChapterRef()
val kavitaChapterId = dbChapter.url.toKavitaChapterId()
return when {
isDownloaded -> DownloadPageLoader(
chapter,
@@ -98,9 +138,24 @@ class ChapterLoader(
when (format) {
is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Archive -> ArchivePageLoader(format.file.archiveReader(context))
is Format.Epub -> EpubPageLoader(format.file.epubReader(context))
is Format.Epub -> EpubPageLoader(format.file.epubReader(context), readerPreferences.textReaderStyle(context), context.cacheDir)
is Format.Pdf -> PdfPageLoader(context, format.file)
is Format.Text -> TextPageLoader(format.file, readerPreferences.textReaderStyle(context), context.cacheDir)
}
}
source is HttpSource && source.isBookOasisSource() && bookOasisRef != null -> {
BookOasisFilePageLoader(context, source, bookOasisRef, readerPreferences.textReaderStyle(context))
}
source is HttpSource && source.isKavitaSource() && kavitaChapterId != null -> {
KavitaCompatPageLoader(
context = context,
chapter = chapter,
source = source,
chapterCache = chapterCache,
chapterId = kavitaChapterId,
textStyle = readerPreferences.textReaderStyle(context),
)
}
source is HttpSource -> HttpPageLoader(chapter, source, chapterCache)
source is StubSource -> error(context.stringResource(MR.strings.source_not_installed, source.toString()))
else -> error(context.stringResource(MR.strings.loader_not_implemented_error))
@@ -2,22 +2,137 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
import mihon.core.archive.EpubReader
import org.jsoup.Jsoup
import java.io.File
import java.io.InputStream
import java.io.StringReader
/**
* Loader used to load a chapter from a .epub file.
*
* Text-based EPUBs are rendered as regular Mihon pages. EPUB spine documents
* are processed one at a time so the full book text is never retained in heap.
* Image-only EPUBs keep the original image extraction behavior.
*/
internal class EpubPageLoader(private val reader: EpubReader) : PageLoader() {
internal class EpubPageLoader(
private val reader: EpubReader,
private val style: TextReaderStyle,
private val cacheDir: File,
) : PageLoader() {
override var isLocal: Boolean = true
override val usesTextReaderStyle: Boolean = true
private var pageStore: TextPageStore? = null
override suspend fun getPages(): List<ReaderPage> {
return reader.getImagesFromPages().mapIndexed { i, path ->
ReaderPage(i).apply {
stream = { reader.getInputStream(path)!! }
status = Page.State.Ready
check(!isRecycled)
pageStore?.close()
pageStore = null
val store = TextPageStore(cacheDir)
return try {
val textPages = buildTextPages(store)
if (textPages.isNotEmpty()) {
store.finishWriting()
pageStore = store
textPages
} else {
store.close()
reader.getImagesFromPages().mapIndexed { index, path ->
ReaderPage(index).apply {
stream = { reader.getInputStream(path)!! }
status = Page.State.Ready
}
}
}
} catch (e: Throwable) {
store.close()
throw e
}
}
private fun buildTextPages(store: TextPageStore): MutableList<ReaderPage> {
val packageHref = reader.getPackageHref()
val packageDocument = reader.getPackageDocument(packageHref)
val manifest = packageDocument.select("manifest > item")
.filter { item ->
val mediaType = item.attr("media-type")
mediaType == "application/xhtml+xml" || mediaType == "text/html"
}
.associateBy { it.attr("id") }
val spine = packageDocument.select("spine > itemref")
.map { it.attr("idref") }
val result = mutableListOf<ReaderPage>()
spine.forEach { id ->
val href = manifest[id]?.attr("href").orEmpty()
if (href.isBlank()) return@forEach
openEntry(packageHref, href)?.use { input ->
val text = extractText(input)
if (text.isBlank()) return@use
StringReader(text).use { textReader ->
TextPageRenderer.paginate(textReader, style) { pageText ->
val ref = store.append(pageText)
result += TextReaderPage(
index = result.size,
pageTextProvider = { store.read(ref) },
initialStyle = style,
)
}
}
}
}
return result
}
private fun extractText(input: InputStream): String {
val pageDoc = Jsoup.parse(input, null, "")
pageDoc.select("br").append("\n")
pageDoc.select("p,div,li,h1,h2,h3,h4,h5,h6,blockquote").append("\n")
return pageDoc.body().wholeText()
?.replace(Regex("[ \\t]+\\n"), "\n")
?.replace(Regex("\\n{3,}"), "\n\n")
?.trim()
.orEmpty()
}
private fun openEntry(packageHref: String, href: String): InputStream? {
val resolved = resolveEntryPath(packageHref, href)
val candidates = linkedSetOf(
resolved,
resolved.replace('\\', '/'),
resolved.replace('/', '\\'),
)
return candidates.firstNotNullOfOrNull { path ->
reader.getInputStream(path)
}
}
private fun resolveEntryPath(packageHref: String, href: String): String {
val relative = href
.substringBefore('#')
.substringBefore('?')
.trimStart('/', '\\')
val separator = if ('\\' in packageHref) '\\' else '/'
val base = packageHref.substringBeforeLast(separator, "")
val combined = if (base.isBlank()) relative else "$base$separator$relative"
val parts = combined.split('/', '\\')
val normalized = ArrayDeque<String>()
parts.forEach { part ->
when (part) {
"", "." -> Unit
".." -> if (normalized.isNotEmpty()) normalized.removeLast()
else -> normalized.addLast(part)
}
}
return normalized.joinToString(separator.toString())
}
override suspend fun loadPage(page: ReaderPage) {
@@ -25,6 +140,8 @@ internal class EpubPageLoader(private val reader: EpubReader) : PageLoader() {
}
override fun recycle() {
pageStore?.close()
pageStore = null
super.recycle()
reader.close()
}
@@ -0,0 +1,169 @@
package eu.kanade.tachiyomi.ui.reader.loader
import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
import mihon.core.archive.epubReader
import okhttp3.Request
import org.json.JSONObject
import java.io.File
import java.io.IOException
import java.util.Locale
/**
* Compatibility loader for Kavita document chapters.
*
* The Kavita extension renders every chapter through /Reader/image. That works
* for image/archive content, but EPUB/TXT can fail while Kavita is preparing an
* image cache. For document files, download the original chapter and use
* Mihon's native document reader instead. Other formats keep the extension's
* normal HttpPageLoader behavior.
*/
internal class KavitaCompatPageLoader(
private val context: Context,
private val chapter: ReaderChapter,
private val source: HttpSource,
private val chapterCache: ChapterCache,
private val chapterId: Int,
private val textStyle: TextReaderStyle,
) : PageLoader() {
override var isLocal: Boolean = false
override val usesTextReaderStyle: Boolean
get() = delegate?.usesTextReaderStyle == true
private var delegate: PageLoader? = null
override suspend fun getPages(): List<ReaderPage> {
check(!isRecycled)
val format = detectDocumentFormat()
val loader = when (format) {
"epub", "txt" -> createDocumentLoader(format)
else -> HttpPageLoader(chapter, source, chapterCache)
}
delegate = loader
isLocal = loader.isLocal
return loader.getPages()
}
override suspend fun loadPage(page: ReaderPage) {
delegate?.loadPage(page)
}
override fun retryPage(page: ReaderPage) {
delegate?.retryPage(page)
}
override fun recycle() {
delegate?.recycle()
delegate = null
super.recycle()
}
private suspend fun detectDocumentFormat(): String? {
val apiBase = source.baseUrl.trimEnd('/') + "/api"
val request = Request.Builder()
.url("$apiBase/Chapter?chapterId=$chapterId")
.headers(source.headers)
.get()
.build()
source.client.newCall(request).await().use { response ->
if (!response.isSuccessful) {
// Preserve the extension's normal behavior when chapter metadata
// cannot be queried. It may still be readable as image content.
return null
}
val body = response.body.string()
if (body.isBlank()) return null
val chapterJson = JSONObject(body)
val files = chapterJson.optJSONArray("files") ?: return null
for (i in 0 until files.length()) {
val file = files.optJSONObject(i) ?: continue
val extension = normalizeExtension(
file.optString("extension").ifBlank { file.optString("filePath") },
)
if (extension == "epub" || extension == "txt") {
return extension
}
// Current Kavita format enum: EPUB == 3. TXT has no dedicated
// enum value in upstream Kavita, so it is detected by extension.
if (file.optInt("format", -1) == 3) {
return "epub"
}
}
}
return null
}
private fun normalizeExtension(value: String): String =
value.substringAfterLast('.', value)
.trim()
.trimStart('.')
.lowercase(Locale.ROOT)
private suspend fun createDocumentLoader(format: String): PageLoader {
val file = getOrDownloadDocument(format)
val uniFile = UniFile.fromFile(file)
?: throw IOException("Unable to open cached Kavita ${format.uppercase()} file")
return when (format) {
"epub" -> EpubPageLoader(uniFile.epubReader(context), textStyle, context.cacheDir)
"txt" -> TextPageLoader(uniFile, textStyle, context.cacheDir)
else -> error("Unsupported Kavita document format: $format")
}
}
private suspend fun getOrDownloadDocument(format: String): File {
val cacheDir = File(context.cacheDir, "kavita_reader").apply { mkdirs() }
val target = File(cacheDir, "${source.id}-$chapterId.$format")
if (target.isFile && target.length() > 0L) return target
val temp = File(cacheDir, "${target.name}.part")
if (temp.exists()) temp.delete()
val apiBase = source.baseUrl.trimEnd('/') + "/api"
val request = Request.Builder()
.url("$apiBase/Download/chapter?chapterId=$chapterId")
.headers(source.headers)
.get()
.build()
source.client.newCall(request).await().use { response ->
if (!response.isSuccessful) {
val message = when (response.code) {
403 -> "Kavita account does not have Download permission"
404 -> "Kavita original file is missing or no longer available"
500 -> "Kavita could not access the original $format file"
else -> "Kavita document download failed: HTTP ${response.code}"
}
throw IOException(message)
}
temp.outputStream().use { output ->
response.body.byteStream().use { input -> input.copyTo(output) }
}
}
if (temp.length() <= 0L) {
temp.delete()
throw IOException("Kavita returned an empty ${format.uppercase()} file")
}
if (!temp.renameTo(target)) {
temp.copyTo(target, overwrite = true)
temp.delete()
}
return target
}
}
@@ -17,6 +17,8 @@ abstract class PageLoader {
abstract var isLocal: Boolean
open val usesTextReaderStyle: Boolean = false
/**
* Returns the list of pages of a chapter.
*/
@@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.ui.reader.loader
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
internal class PdfPageLoader(
context: Context,
file: UniFile,
) : PageLoader() {
override var isLocal: Boolean = true
private val descriptor: ParcelFileDescriptor =
context.contentResolver.openFileDescriptor(file.uri, "r")
?: error("Unable to open PDF: ${file.name}")
private val renderer = PdfRenderer(descriptor)
private val renderLock = Any()
override suspend fun getPages(): List<ReaderPage> {
return (0 until renderer.pageCount).map { index ->
ReaderPage(index).apply {
stream = { renderPage(index) }
status = Page.State.Ready
}
}
}
private fun renderPage(index: Int): InputStream = synchronized(renderLock) {
check(!isRecycled)
renderer.openPage(index).use { page ->
val scale = (1600f / page.width.coerceAtLeast(1)).coerceIn(1f, 2.5f)
val width = (page.width * scale).toInt().coerceAtLeast(1)
val height = (page.height * scale).toInt().coerceAtLeast(1)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.eraseColor(Color.WHITE)
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
val output = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
bitmap.recycle()
ByteArrayInputStream(output.toByteArray())
}
}
override fun recycle() {
if (!isRecycled) {
synchronized(renderLock) {
renderer.close()
descriptor.close()
}
}
super.recycle()
}
}
@@ -0,0 +1,106 @@
package eu.kanade.tachiyomi.ui.reader.loader
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
import java.io.File
import java.io.InputStreamReader
import java.io.Reader
import java.nio.charset.Charset
import java.nio.charset.CodingErrorAction
import java.nio.charset.StandardCharsets
internal class TextPageLoader(
private val file: UniFile,
private val style: TextReaderStyle,
private val cacheDir: File,
) : PageLoader() {
override var isLocal: Boolean = true
override val usesTextReaderStyle: Boolean = true
private var pageStore: TextPageStore? = null
override suspend fun getPages(): List<ReaderPage> {
pageStore?.close()
pageStore = null
val store = TextPageStore(cacheDir)
return try {
val pages = mutableListOf<ReaderPage>()
openDecodedReader().use { reader ->
TextPageRenderer.paginate(reader, style) { pageText ->
val ref = store.append(pageText)
pages += TextReaderPage(
index = pages.size,
pageTextProvider = { store.read(ref) },
initialStyle = style,
)
}
}
store.finishWriting()
pageStore = store
pages
} catch (e: Throwable) {
store.close()
throw e
}
}
override fun recycle() {
pageStore?.close()
pageStore = null
super.recycle()
}
private fun openDecodedReader(): Reader {
val charset = detectCharset()
return InputStreamReader(file.openInputStream().buffered(), charset)
}
private fun detectCharset(): Charset {
val prefix = ByteArray(3)
val prefixSize = file.openInputStream().buffered().use { input ->
input.read(prefix)
}
if (
prefixSize >= 3 &&
prefix[0] == 0xEF.toByte() &&
prefix[1] == 0xBB.toByte() &&
prefix[2] == 0xBF.toByte()
) {
return StandardCharsets.UTF_8
}
if (prefixSize >= 2 && prefix[0] == 0xFF.toByte() && prefix[1] == 0xFE.toByte()) {
return StandardCharsets.UTF_16LE
}
if (prefixSize >= 2 && prefix[0] == 0xFE.toByte() && prefix[1] == 0xFF.toByte()) {
return StandardCharsets.UTF_16BE
}
return if (isValidUtf8()) {
StandardCharsets.UTF_8
} else {
Charset.forName("MS949")
}
}
private fun isValidUtf8(): Boolean {
val decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
return try {
InputStreamReader(file.openInputStream().buffered(), decoder).use { reader ->
val buffer = CharArray(8 * 1024)
while (reader.read(buffer) >= 0) {
// Validation only. Decoded characters are intentionally discarded.
}
}
true
} catch (_: Exception) {
false
}
}
}
@@ -0,0 +1,212 @@
package eu.kanade.tachiyomi.ui.reader.loader
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.text.Layout
import android.text.Spannable
import android.text.SpannableString
import android.text.StaticLayout
import android.text.TextPaint
import android.text.style.LineHeightSpan
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.Reader
internal object TextPageRenderer {
private const val PAGE_WIDTH = 1440
private const val PAGE_HEIGHT = 2160
private const val MARGIN = 96
private const val CONTENT_WIDTH = PAGE_WIDTH - (MARGIN * 2)
private const val CONTENT_HEIGHT = PAGE_HEIGHT - (MARGIN * 2)
private const val STREAM_BUFFER_CHARS = 64 * 1024
private const val STREAM_READ_CHARS = 8 * 1024
fun paginate(text: String, style: TextReaderStyle): List<String> {
if (text.isBlank()) return emptyList()
return calculatePageSlices(text, style)
.mapNotNull { slice ->
text.substring(slice.start, slice.endExclusive)
.trim()
.takeIf { it.isNotEmpty() }
}
.ifEmpty { listOf(text) }
}
/**
* Incrementally paginates a text stream while keeping only a bounded text
* window in memory. The final incomplete page is retained between windows
* so normal page filling is preserved across chunk boundaries.
*/
fun paginate(
reader: Reader,
style: TextReaderStyle,
onPage: (String) -> Unit,
) {
val working = StringBuilder(STREAM_BUFFER_CHARS + STREAM_READ_CHARS)
val chars = CharArray(STREAM_READ_CHARS)
var firstChunk = true
while (true) {
val read = reader.read(chars)
if (read < 0) break
if (read == 0) continue
var offset = 0
if (firstChunk) {
firstChunk = false
if (chars[0] == '\uFEFF') {
offset = 1
}
}
if (offset < read) {
working.append(chars, offset, read - offset)
}
if (working.length >= STREAM_BUFFER_CHARS) {
emitCompletePages(working, style, onPage)
}
}
if (working.isNotEmpty()) {
paginate(working.toString(), style).forEach(onPage)
}
}
private fun emitCompletePages(
working: StringBuilder,
style: TextReaderStyle,
onPage: (String) -> Unit,
) {
val text = working.toString()
val slices = calculatePageSlices(text, style)
if (slices.size <= 1) return
for (index in 0 until slices.lastIndex) {
val slice = slices[index]
text.substring(slice.start, slice.endExclusive)
.trim()
.takeIf { it.isNotEmpty() }
?.let(onPage)
}
working.delete(0, slices.last().start)
}
private fun calculatePageSlices(text: String, style: TextReaderStyle): List<PageSlice> {
if (text.isEmpty()) return emptyList()
val layout = createLayout(text, style)
if (layout.lineCount == 0) return listOf(PageSlice(0, text.length))
val pages = mutableListOf<PageSlice>()
var startLine = 0
while (startLine < layout.lineCount) {
val startTop = layout.getLineTop(startLine)
var endLine = startLine
while (
endLine + 1 < layout.lineCount &&
layout.getLineBottom(endLine + 1) - startTop <= CONTENT_HEIGHT
) {
endLine++
}
val start = layout.getLineStart(startLine)
val end = layout.getLineEnd(endLine).coerceAtLeast(start + 1).coerceAtMost(text.length)
pages += PageSlice(start, end)
startLine = endLine + 1
}
return pages.ifEmpty { listOf(PageSlice(0, text.length)) }
}
fun render(text: String, style: TextReaderStyle): InputStream {
val bitmap = Bitmap.createBitmap(PAGE_WIDTH, PAGE_HEIGHT, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val (backgroundColor, _) = resolveColors(style)
canvas.drawColor(backgroundColor)
canvas.save()
canvas.translate(MARGIN.toFloat(), MARGIN.toFloat())
createLayout(text, style).draw(canvas)
canvas.restore()
val output = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
bitmap.recycle()
return ByteArrayInputStream(output.toByteArray())
}
private fun createLayout(text: String, style: TextReaderStyle): StaticLayout {
val (_, textColor) = resolveColors(style)
val paint = TextPaint().apply {
isAntiAlias = true
color = textColor
textSize = style.fontSize * 3f
typeface = style.fontFamily.androidFamilyName
?.let { Typeface.create(it, Typeface.NORMAL) }
?: Typeface.DEFAULT
}
val styledText = addParagraphSpacing(text, style.paragraphSpacing * 3)
return StaticLayout.Builder
.obtain(styledText, 0, styledText.length, paint, CONTENT_WIDTH)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setIncludePad(true)
.setLineSpacing(0f, style.lineSpacingPercent / 100f)
.build()
}
private fun resolveColors(style: TextReaderStyle): Pair<Int, Int> {
val useDark = when (style.readerTheme) {
0 -> false
1 -> true
2 -> true
3 -> style.automaticDark
else -> style.automaticDark
}
val background = when (style.readerTheme) {
2 -> Color.rgb(48, 48, 48)
else -> if (useDark) Color.BLACK else Color.WHITE
}
val foreground = if (useDark) Color.WHITE else Color.BLACK
return background to foreground
}
private fun addParagraphSpacing(text: String, spacing: Int): CharSequence {
if (spacing <= 0 || '\n' !in text) return text
val spannable = SpannableString(text)
text.forEachIndexed { index, char ->
if (char == '\n') {
spannable.setSpan(
ParagraphSpacingSpan(spacing),
index,
(index + 1).coerceAtMost(text.length),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE,
)
}
}
return spannable
}
private class ParagraphSpacingSpan(private val spacing: Int) : LineHeightSpan {
override fun chooseHeight(
text: CharSequence?,
start: Int,
end: Int,
spanstartv: Int,
v: Int,
fm: Paint.FontMetricsInt,
) {
fm.descent += spacing
fm.bottom += spacing
}
}
private data class PageSlice(
val start: Int,
val endExclusive: Int,
)
}
@@ -0,0 +1,69 @@
package eu.kanade.tachiyomi.ui.reader.loader
import java.io.Closeable
import java.io.File
import java.io.RandomAccessFile
import java.nio.charset.StandardCharsets
/**
* Disk-backed storage for text reader pages.
*
* Keeping page strings here avoids retaining an entire large TXT/EPUB in the
* Java heap. Individual pages are loaded only when the viewer renders them.
*/
internal class TextPageStore(cacheRoot: File) : Closeable {
private val cacheDir = File(cacheRoot, "text_reader_pages").apply {
mkdirs()
pruneStaleFiles()
}
private val file = File.createTempFile("pages-", ".utf8", cacheDir)
private var writer: RandomAccessFile? = RandomAccessFile(file, "rw")
@Synchronized
fun append(text: String): PageRef {
val output = checkNotNull(writer) { "Text page store is already finalized" }
val bytes = text.toByteArray(StandardCharsets.UTF_8)
val offset = output.filePointer
output.write(bytes)
return PageRef(offset, bytes.size)
}
@Synchronized
fun finishWriting() {
writer?.fd?.sync()
writer?.close()
writer = null
}
fun read(ref: PageRef): String {
val bytes = ByteArray(ref.byteLength)
RandomAccessFile(file, "r").use { input ->
input.seek(ref.offset)
input.readFully(bytes)
}
return String(bytes, StandardCharsets.UTF_8)
}
override fun close() {
writer?.close()
writer = null
file.delete()
}
internal data class PageRef(
val offset: Long,
val byteLength: Int,
)
private fun File.pruneStaleFiles() {
val cutoff = System.currentTimeMillis() - STALE_FILE_AGE_MS
listFiles()
?.filter { it.isFile && it.lastModified() < cutoff }
?.forEach { it.delete() }
}
private companion object {
const val STALE_FILE_AGE_MS = 24L * 60L * 60L * 1000L
}
}
@@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.setting.TextReaderStyle
/**
* A rendered text page whose style can be changed without rebuilding the chapter.
* Page boundaries remain stable during the live preview; a full repagination is done
* once when the settings dialog is dismissed.
*/
internal class TextReaderPage(
index: Int,
private val pageTextProvider: () -> String,
initialStyle: TextReaderStyle,
) : ReaderPage(index) {
@Volatile
private var style: TextReaderStyle = initialStyle
init {
stream = { TextPageRenderer.render(pageTextProvider(), style) }
status = Page.State.Ready
}
constructor(
index: Int,
pageText: String,
initialStyle: TextReaderStyle,
) : this(index, { pageText }, initialStyle)
fun updateStyle(newStyle: TextReaderStyle) {
style = newStyle
}
}
@@ -1,5 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.setting
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.compose.ui.graphics.BlendMode
import dev.icerock.moko.resources.StringResource
@@ -11,6 +13,7 @@ import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.preference.getEnum
import tachiyomi.core.common.preference.getEnumSet
import tachiyomi.i18n.MR
import kotlin.math.roundToInt
@Inject
@SingleIn(AppScope::class)
@@ -108,6 +111,44 @@ class ReaderPreferences(
val webtoonDisableZoomOut: Preference<Boolean> = preferenceStore.getBoolean("webtoon_disable_zoom_out", false)
// region Text reader
val textFontSize: Preference<Int> = preferenceStore.getInt("reader_text_font_size", 16)
val textLineSpacing: Preference<Int> = preferenceStore.getInt("reader_text_line_spacing", 115)
val textParagraphSpacing: Preference<Int> = preferenceStore.getInt("reader_text_paragraph_spacing", 4)
val textFontFamily: Preference<TextFontFamily> = preferenceStore.getEnum(
"reader_text_font_family",
TextFontFamily.SYSTEM,
)
private val textUnitsMigrated = preferenceStore.getBoolean("reader_text_units_v2_migrated", false)
init {
if (!textUnitsMigrated.get()) {
val legacyFontSize = textFontSize.get()
if (legacyFontSize > 30) {
textFontSize.set((legacyFontSize / 3f).roundToInt().coerceIn(10, 30))
textParagraphSpacing.set((textParagraphSpacing.get() / 3f).roundToInt().coerceIn(0, 16))
}
textUnitsMigrated.set(true)
}
}
fun textReaderStyle(context: Context) = TextReaderStyle(
fontSize = textFontSize.get(),
lineSpacingPercent = textLineSpacing.get(),
paragraphSpacing = textParagraphSpacing.get(),
fontFamily = textFontFamily.get(),
readerTheme = readerTheme.get(),
automaticDark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES,
)
// endregion
// endregion
// region Split two-page spread
@@ -217,6 +258,13 @@ class ReaderPreferences(
// endregion
enum class TextFontFamily(val androidFamilyName: String?) {
SYSTEM(null),
SANS_SERIF("sans-serif"),
SERIF("serif"),
MONOSPACE("monospace"),
}
enum class FlashColor {
BLACK,
WHITE,
@@ -342,3 +390,13 @@ class ReaderPreferences(
}
}
}
data class TextReaderStyle(
val fontSize: Int,
val lineSpacingPercent: Int,
val paragraphSpacing: Int,
val fontFamily: ReaderPreferences.TextFontFamily,
val readerTheme: Int,
val automaticDark: Boolean,
)
@@ -6,13 +6,18 @@ import eu.kanade.tachiyomi.ui.reader.ReaderViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class ReaderSettingsViewModel(
readerState: StateFlow<ReaderViewModel.State>,
val onChangeReadingMode: (ReadingMode) -> Unit,
val onChangeOrientation: (ReaderOrientation) -> Unit,
private val onTextSettingsPreview: () -> Unit,
private val onTextSettingsCommit: () -> Unit,
val preferences: ReaderPreferences,
) : ViewModel() {
@@ -25,4 +30,26 @@ class ReaderSettingsViewModel(
.map { it.manga }
.distinctUntilChanged()
.stateIn(viewModelScope, SharingStarted.Lazily, null)
private var textSettingsReloadJob: Job? = null
private var textSettingsDirty = false
fun requestTextSettingsReload() {
textSettingsDirty = true
textSettingsReloadJob?.cancel()
textSettingsReloadJob = viewModelScope.launch {
delay(200)
onTextSettingsPreview()
textSettingsReloadJob = null
}
}
fun flushTextSettingsReload() {
if (!textSettingsDirty) return
textSettingsReloadJob?.cancel()
textSettingsReloadJob = null
onTextSettingsPreview()
textSettingsDirty = false
onTextSettingsCommit()
}
}
@@ -26,6 +26,14 @@ interface Viewer {
*/
fun setChapters(chapters: ViewerChapters)
/** Rebinds only the currently visible page(s), used for lightweight text-style preview. */
fun refreshCurrentPage() {}
/** Replaces repaginated text pages without expensive per-item diffing where possible. */
fun refreshTextChapters(chapters: ViewerChapters) {
setChapters(chapters)
}
/**
* Tells this viewer to move to the given [page].
*/
@@ -380,6 +380,17 @@ abstract class PagerViewer(val activity: ReaderActivity) : Viewer {
pager.setCurrentItem(currentItem, false)
}
override fun refreshCurrentPage() {
refreshAdapter()
}
override fun refreshTextChapters(chapters: ViewerChapters) {
setChaptersInternal(chapters)
val pages = chapters.currChapter.pages ?: return
val page = pages[min(chapters.currChapter.requestedPage, pages.lastIndex)]
moveToPage(page)
}
/**
* Called from the containing activity when a key [event] is received. It should return true
* if the event was handled, false otherwise.
@@ -35,7 +35,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
* Updates this adapter with the given [chapters]. It handles setting a few pages of the
* next/previous chapter to allow seamless transitions.
*/
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean, useDiff: Boolean = true) {
val newItems = mutableListOf<Any>()
// Forces chapter transition if there is missing chapters
@@ -65,13 +65,18 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : RecyclerView.Adapter<RecyclerV
chapters.nextChapter?.pages?.let(newItems::addAll)
updateItems(newItems)
updateItems(newItems, useDiff)
}
private fun updateItems(newItems: List<Any>) {
val result = DiffUtil.calculateDiff(Callback(items, newItems))
items = newItems
result.dispatchUpdatesTo(this)
private fun updateItems(newItems: List<Any>, useDiff: Boolean) {
if (useDiff) {
val result = DiffUtil.calculateDiff(Callback(items, newItems))
items = newItems
result.dispatchUpdatesTo(this)
} else {
items = newItems
notifyDataSetChanged()
}
}
fun refresh() {
@@ -353,6 +353,19 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
min(position + 3, adapter.itemCount - 1),
)
}
override fun refreshCurrentPage() {
refreshAdapter()
}
override fun refreshTextChapters(chapters: ViewerChapters) {
val forceTransition = config.alwaysShowChapterTransition || currentPage is ChapterTransition
adapter.setChapters(chapters, forceTransition, useDiff = false)
val pages = chapters.currChapter.pages ?: return
val page = pages[min(chapters.currChapter.requestedPage, pages.lastIndex)]
moveToPage(page)
currentPage = page
}
}
// Double the cache size to reduce rebinds/recycles incurred by the extra layout space on scroll direction changes
@@ -6,6 +6,7 @@ import dev.zacsweers.metro.Inject
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
@@ -65,7 +66,7 @@ class CrashLogUtil(
private suspend fun getExtensionsInfo(): String? {
val availableExtensions = extensionManager.availableExtensionsFlow.value.associateBy { it.pkgName }
val extensionInfoList = extensionManager.getInstalledExtensions()
val outdatedInfoList = extensionManager.getLoadedExtensions()
.sortedBy { it.name }
.mapNotNull {
val availableExtension = availableExtensions[it.pkgName]
@@ -80,6 +81,24 @@ class CrashLogUtil(
""".trimIndent()
}
val notLoadedInfoList = extensionManager.getNotLoadedExtensions()
.sortedBy { it.name }
.map { extension ->
buildString {
appendLine("- ${extension.name}")
appendLine(" Installed: ${extension.versionName} (lib ${extension.libVersion ?: "?"})")
append(" Not loaded: ${extension.reason.description}")
val reason = extension.reason
if (reason is Extension.NotLoaded.Reason.Failed) {
appendLine()
append(reason.stackTrace.trimEnd().prependIndent(" "))
}
}
}
val extensionInfoList = outdatedInfoList + notLoadedInfoList
return if (extensionInfoList.isNotEmpty()) {
(listOf("Problematic extensions:") + extensionInfoList)
.joinToString("\n")
@@ -88,3 +107,13 @@ class CrashLogUtil(
}
}
}
private val Extension.NotLoaded.Reason.description: String
get() = when (this) {
is Extension.NotLoaded.Reason.Untrusted -> "Untrusted"
Extension.NotLoaded.Reason.Filtered -> "Filtered by content warning"
Extension.NotLoaded.Reason.Unsigned -> "Unsigned"
Extension.NotLoaded.Reason.UnsupportedLibVersion -> "Unsupported lib version"
Extension.NotLoaded.Reason.Malformed -> "Malformed"
is Extension.NotLoaded.Reason.Failed -> "Failed ($message)"
}
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadJob
import eu.kanade.tachiyomi.data.download.DownloadManager
@@ -29,6 +30,7 @@ import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegateImpl
@@ -39,8 +41,10 @@ import eu.kanade.tachiyomi.ui.setting.track.BaseOAuthLoginActivity
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.CrashLogUtil
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import mihon.core.metro.IsDebugBuild
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.category.interactor.ResetCategoryFlags
@@ -101,10 +105,14 @@ interface AppGraph : ViewModelGraph {
val trackerManager: TrackerManager
val extensionManager: ExtensionManager
val chapterCache: ChapterCache
val coverCache: CoverCache
val downloadCache: DownloadCache
val json: Json
val protoBuf: ProtoBuf
val xml: XML
val networkHelper: NetworkHelper
val javaScriptEngine: JavaScriptEngine
val getFavorites: GetFavorites
val getCategories: GetCategories
@@ -0,0 +1,100 @@
package mihon.app.di.injekt
import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import mihon.app.di.AppGraph
import mihon.core.metro.GraphProvider
import nl.adaptivity.xmlutil.serialization.XML
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.InjektionException
import uy.kohesive.injekt.api.TypeReference
import java.lang.reflect.Type
class MetroInjektRegistrar(
private val application: Application,
private val graphProvider: GraphProvider<AppGraph>,
) : InjektRegistrar {
private val graph: AppGraph inline get() = graphProvider.graph
private val bindings = mapOf<Type, () -> Any>(
Application::class.java to { application },
Context::class.java to { application },
Json::class.java to { graph.json },
ProtoBuf::class.java to { graph.protoBuf },
XML::class.java to { graph.xml },
NetworkHelper::class.java to { graph.networkHelper },
JavaScriptEngine::class.java to { graph.javaScriptEngine },
)
override fun <R : Any> getInstance(forType: Type): R = getInstanceOrNull(forType)
?: throw InjektionException("$forType is not exposed to Injekt, add it to ${this::class.simpleName}")
override fun <R : Any> getInstanceOrElse(forType: Type, default: R): R = getInstanceOrNull(forType) ?: default
override fun <R : Any> getInstanceOrElse(forType: Type, default: () -> R): R =
getInstanceOrNull(forType) ?: default()
@Suppress("UNCHECKED_CAST")
override fun <R : Any> getInstanceOrNull(forType: Type): R? = bindings[forType]?.invoke() as R?
override fun <R : Any, K : Any> getKeyedInstance(forType: Type, key: K): R = unsupported("Keyed injection")
override fun <R : Any, K : Any> getKeyedInstanceOrElse(forType: Type, key: K, default: R): R =
unsupported("Keyed injection")
override fun <R : Any, K : Any> getKeyedInstanceOrElse(forType: Type, key: K, default: () -> R): R =
unsupported("Keyed injection")
@Suppress("RedundantNullableReturnType")
override fun <R : Any, K : Any> getKeyedInstanceOrNull(forType: Type, key: K): R? = unsupported("Keyed injection")
override fun <R : Any> getLogger(expectedLoggerType: Type, byName: String): R = unsupported("Logger injection")
override fun <R : Any, T : Any> getLogger(expectedLoggerType: Type, forClass: Class<T>): R =
unsupported("Logger injection")
override fun <T : Any> addSingleton(forType: TypeReference<T>, singleInstance: T) = readOnly()
override fun <R : Any> addSingletonFactory(forType: TypeReference<R>, factoryCalledOnce: () -> R) = readOnly()
override fun <R : Any> addFactory(forType: TypeReference<R>, factoryCalledEveryTime: () -> R) = readOnly()
override fun <R : Any> addPerThreadFactory(forType: TypeReference<R>, factoryCalledOncePerThread: () -> R) =
readOnly()
override fun <R : Any, K : Any> addPerKeyFactory(forType: TypeReference<R>, factoryCalledPerKey: (K) -> R) =
readOnly()
override fun <R : Any, K : Any> addPerThreadPerKeyFactory(
forType: TypeReference<R>,
factoryCalledPerKeyPerThread: (K) -> R,
) = readOnly()
override fun <R : Any> addLoggerFactory(
forLoggerType: TypeReference<R>,
factoryByName: (String) -> R,
factoryByClass: (Class<Any>) -> R,
) = readOnly()
override fun <O : Any, T : O> addAlias(
existingRegisteredType: TypeReference<T>,
otherAncestorOrInterface: TypeReference<O>,
) = readOnly()
override fun <T : Any> hasFactory(forType: TypeReference<T>): Boolean = forType.type in bindings
override fun importModule(submodule: InjektModule) = readOnly()
private fun readOnly(): Nothing = throw UnsupportedOperationException("Mihon's Injekt instance is read-only")
private fun unsupported(feature: String): Nothing =
throw UnsupportedOperationException("$feature is not supported by Mihon's Injekt instance")
}
@@ -1,49 +0,0 @@
package mihon.app.di.injekt
import dev.zacsweers.metro.Inject
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.preference.PreferenceStore
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
@Inject
class MetroInteropModule(
private val json: Json,
private val protoBuf: ProtoBuf,
private val xml: XML,
private val networkHelper: NetworkHelper,
private val javaScriptEngine: JavaScriptEngine,
private val preferenceStore: PreferenceStore,
private val trackPreferences: TrackPreferences,
private val extensionManager: ExtensionManager,
private val coverCache: CoverCache,
) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(json)
addSingleton(protoBuf)
addSingleton(xml)
addSingleton(networkHelper)
addSingleton(javaScriptEngine)
addSingleton(preferenceStore)
addSingleton(trackPreferences)
addSingleton(extensionManager)
addSingleton(coverCache)
}
}
@@ -8,7 +8,10 @@ import eu.kanade.domain.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.bookoasis.fetchBookOasisChapters
import eu.kanade.tachiyomi.source.bookoasis.isBookOasisSource
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import logcat.LogPriority
import mihon.domain.source.models.RemoteMangaUpdate
import tachiyomi.core.common.util.lang.withIOContext
@@ -70,8 +73,28 @@ class UpdateMangaFromRemote(
)
}
awaitUpdateFromSource(manga, update.manga, manualFetch)
val rawSourceChapters = if (
fetchChapters &&
source is HttpSource &&
source.isBookOasisSource()
) {
try {
withIOContext {
source.fetchBookOasisChapters(manga.toSManga())
}
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) {
"BookOasis document chapter compatibility fallback failed"
}
update.chapters
}
} else {
update.chapters
}
val newChapters = syncChaptersWithSource.await(
rawSourceChapters = update.chapters,
rawSourceChapters = rawSourceChapters,
manga = manga,
source = source,
manualFetch = manualFetch,
@@ -26,6 +26,9 @@ fun SManga.getComicInfo() = ComicInfo(
letterer = null,
coverArtist = null,
tags = null,
year = null,
month = null,
day = null,
categories = null,
source = null,
)
@@ -80,6 +83,9 @@ data class ComicInfo(
val genre: Genre?,
val tags: Tags?,
val web: Web?,
val year: Year?,
val month: Month?,
val day: Day?,
val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi?,
val source: SourceMihon?,
@@ -148,6 +154,18 @@ data class ComicInfo(
@XmlSerialName("Web", "", "")
data class Web(@XmlValue(true) val value: String = "")
@Serializable
@XmlSerialName("Year", "", "")
data class Year(@XmlValue(true) val value: Int = -1)
@Serializable
@XmlSerialName("Month", "", "")
data class Month(@XmlValue(true) val value: Int = -1)
@Serializable
@XmlSerialName("Day", "", "")
data class Day(@XmlValue(true) val value: Int = -1)
// The spec doesn't have a good field for this
@Serializable
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
@@ -34,6 +34,27 @@ class EpubReader(private val reader: ArchiveReader) : Closeable by reader {
return getImagesFromPages(pages, ref)
}
/**
* Returns readable text for each spine document in reading order.
*/
fun getTextFromPages(): List<String> {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val basePath = getParentDirectory(ref)
return getPagesFromDocument(doc).mapNotNull { page ->
val entryPath = resolveZipPath(basePath, page)
getInputStream(entryPath)?.use { input ->
val pageDoc = Jsoup.parse(input, null, "")
pageDoc.select("br").append("\n")
pageDoc.select("p,div,li,h1,h2,h3,h4,h5,h6,blockquote").append("\n")
pageDoc.body()?.wholeText()
?.replace(Regex("[ \\t]+\\n"), "\n")
?.replace(Regex("\\n{3,}"), "\n\n")
?.trim()
}
}
}
/**
* Returns the path to the package document.
*/
@@ -6,33 +6,26 @@ import mihon.domain.extension.model.ContentWarning
import mihon.domain.extension.model.ExtensionStore
import tachiyomi.domain.source.model.StubSource
sealed class Extension {
sealed interface Extension {
abstract val name: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Long
abstract val libVersion: Double
abstract val lang: String?
abstract val contentWarning: ContentWarning
val name: String
val pkgName: String
val versionName: String
val versionCode: Long
val libVersion: Double?
val lang: String?
val contentWarning: ContentWarning
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
override val lang: String,
override val contentWarning: ContentWarning,
val pkgFactory: String?,
val sources: List<Source>,
val icon: Drawable?,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isShared: Boolean,
val store: ExtensionStore? = null,
) : Extension()
/**
* An extension whose apk is on the device, whether or not it ended up being loaded.
*/
sealed interface Installed : Extension {
val isShared: Boolean
}
/**
* An extension that isn't on the device yet, as listed by an [ExtensionStore].
*/
data class Available(
override val name: String,
override val pkgName: String,
@@ -45,7 +38,7 @@ sealed class Extension {
val apkUrl: String,
val iconUrl: String,
val store: ExtensionStore,
) : Extension() {
) : Extension {
data class Source(
val id: Long,
@@ -63,14 +56,60 @@ sealed class Extension {
}
}
data class Untrusted(
/**
* An installed extension whose sources are registered and usable.
*/
data class Loaded(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val libVersion: Double,
val signatureHash: String,
override val lang: String,
override val contentWarning: ContentWarning,
override val isShared: Boolean,
val pkgFactory: String?,
val sources: List<Source>,
val icon: Drawable?,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val store: ExtensionStore? = null,
) : Installed
/**
* An installed extension that was never loaded, so it provides no sources. [lang] is derived
* from the sources and [libVersion] from metadata, so neither is always known here.
*/
data class NotLoaded(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
override val isShared: Boolean,
override val contentWarning: ContentWarning,
override val libVersion: Double? = null,
override val lang: String? = null,
override val contentWarning: ContentWarning = ContentWarning.SAFE,
) : Extension()
val reason: Reason,
) : Installed {
sealed interface Reason {
/** Signature isn't trusted yet. Resolvable by the user accepting it. */
data class Untrusted(val signatureHash: String) : Reason
/** Its [contentWarning] isn't one the user chose to load. */
data object Filtered : Reason
/** No signature to check against at all. */
data object Unsigned : Reason
/** Built against an extension lib this app version can't run. */
data object UnsupportedLibVersion : Reason
/** Required package metadata is missing. */
data object Malformed : Reason
/** Threw while its classes or sources were being instantiated. */
data class Failed(val message: String, val stackTrace: String) : Reason
}
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ logcat = "0.4"
markdown = "0.45.0"
material = "1.14.0"
materialKolor = "5.0.1"
metro = "1.4.3"
metro = "1.4.4"
mockk = "1.14.11"
moko-resources = "0.26.4"
okhttp = "5.5.0"
@@ -34,7 +34,6 @@
<string name="portrait">የቁም ስዕል</string>
<string name="pref_library_columns">ዕቃዎች በአንድ ረድፍ</string>
<string name="pref_category_display">ማሳያ</string>
<string name="parental_controls_info">ይህ በይፋዊ ወይም በስህተት የተጠቆሙ ሊሆኑ የሚችሉ ቅጥያዎች በመተግበሪያው ውስጥ 18+ ይዘትን እንዳያሳዩ አያግደውም ፡፡</string>
<string name="hide_notification_content">የማሳወቂያ ይዘትን ደብቅ</string>
<string name="secure_screen_summary">መተግበሪያዎችን ሲቀይሩ የመተግበሪያ ይዘቶችን ደብቅ እና ቅጽበታዊ ገጽ እይታዎችን ያግዳል</string>
<string name="secure_screen">ደህንነቱ የተጠበቀ ማያ ገጽ</string>
@@ -212,8 +211,6 @@
<string name="pref_page_transitions">የገጽ ሽግግሮች</string>
<string name="pref_cutout_short">በመቁረጥ አካባቢ ውስጥ ይዘትን አሳይ</string>
<string name="pref_fullscreen">ሙሉ ማያ</string>
<string name="ext_nsfw_warning">18+ ይዘትን ይያዝ</string>
<string name="ext_nsfw_short">18+</string>
<string name="action_sort_alpha">በፊደል ደርድር</string>
<string name="information_webview_required">ለታቺዮሚ የድር እይታ ያስፈልጋል</string>
<string name="information_cloudflare_bypass_failure">ክላዉድ ፍሌር ን ማለፍ አልተሳካም</string>
@@ -386,9 +386,6 @@
<string name="pref_remove_bookmarked_chapters">حذف الفصول ذات العلامة المرجعية</string>
<string name="pref_category_delete_chapters">حذف الفصول</string>
<string name="pref_category_reading_mode">وضع القراءة</string>
<string name="ext_nsfw_warning">بعض المصادر من هذا الامتداد قد تحتوى على محتوى للبالغين (+18)</string>
<string name="ext_nsfw_short">محتوى غير لائق</string>
<string name="parental_controls_info">هذا لا يمنع الإضافات غير الرسمية أو التي تم وضع علامة عليها بشكل غير صحيح من إظهار محتوى غير لائق داخل التطبيق.</string>
<string name="tapping_inverted_both">كليهما</string>
<string name="tapping_inverted_vertical">عمودي</string>
<string name="tapping_inverted_horizontal">أفقي</string>
@@ -421,8 +418,6 @@
<string name="spen_next_page">الصفحة التالية</string>
<string name="spen_previous_page">الصفحة السابقة</string>
<string name="migration_help_guide">دليل نقل المصدر</string>
<string name="pref_category_nsfw_content">محتوى +18</string>
<string name="pref_show_nsfw_source">اعرض في قوائم المصادر والامتدادات</string>
<string name="myanimelist_relogin">رجاء سجل دخولك في MAL مجدداً</string>
<string name="file_picker_error">لم يتم العثور على تطبيق منتقي الملفات</string>
<string name="pref_viewer_nav">مناطق اللمس</string>
@@ -576,7 +571,6 @@
<string name="pref_reset_viewer_flags_success">اعادة تعيين جميع اعدادات القارئ</string>
<string name="pref_reset_viewer_flags_summary">إعادة تعيين اعدادات القراءه لكل القصص</string>
<string name="ext_info_language">اللغة</string>
<string name="ext_info_age_rating">التقييم العمري</string>
<string name="ext_info_version">الإصدار</string>
<string name="pref_reset_viewer_flags_error">لم يمكن إعادة تعيين إعدادات القارئ</string>
<string name="empty_screen">هذا محرج</string>
@@ -32,7 +32,6 @@
<string name="default_category">ডিফল্ট শ্ৰেণী</string>
<string name="all">সকলো</string>
<string name="ext_obsolete">অচল</string>
<string name="ext_info_age_rating">বয়স মূল্যায়ন</string>
<string name="ext_installer_pref">ইন্সটলাৰ</string>
<string name="ext_installer_legacy">লিগাচি</string>
<string name="action_open_repo">উৎস ৰেপ\' খোলক</string>
@@ -262,9 +261,6 @@
<string name="hide_notification_content">অধিসূচনাৰ বিষয়বস্তু লুকুৱাওক</string>
<string name="secure_screen">সুৰক্ষিত স্ক্ৰীন</string>
<string name="secure_screen_summary">সুৰক্ষিত স্ক্ৰীনে এপ সলনি কৰোঁতে এপৰ বিষয়বস্তু লুকুৱায় আৰু স্ক্ৰীনশ্বট ব্লক কৰে</string>
<string name="pref_category_nsfw_content">NSFW (১৮+) উৎস</string>
<string name="pref_show_nsfw_source">উৎস আৰু এক্সটেনশ্বন তালিকাত দেখুৱাওক</string>
<string name="parental_controls_info">এইটোৱে অনানুষ্ঠানিক বা সম্ভাৱ্যভাৱে ভুল পতাকা লগোৱা এক্সটেনশ্বনসমূহক এপৰ ভিতৰত NSFW (১৮+) বিষয়বস্তু দেখুওৱাত বাধা নিদিয়ে।</string>
<string name="relative_time_today">আজি</string>
<string name="pref_category_display">প্ৰদৰ্শন</string>
<string name="pref_library_columns">প্ৰতি শাৰীত আইটেম</string>
@@ -308,9 +304,7 @@
<string name="extension_api_error">উপলব্ধ এক্সটেনশ্বন অনাত বিফল</string>
<string name="untrusted_extension_message">দুৰ্ভাৱপূৰ্ণ এক্সটেনশ্বনে সংৰক্ষিত যিকোনো লগইন প্ৰমাণপত্ৰ পঢ়িব পাৰে বা ইচ্ছামত কোড কাৰ্যকৰী কৰিব পাৰে।\n\nএই এক্সটেনশ্বন বিশ্বাস কৰি, আপুনি এই বিপদসমূহ গ্ৰহণ কৰে।</string>
<string name="ext_info_version">সংস্কৰণ</string>
<string name="ext_nsfw_warning">এই এক্সটেনশ্বনৰ উৎসসমূহত NSFW (১৮+) বিষয়বস্তু থাকিব পাৰে</string>
<string name="ext_info_language">ভাষা</string>
<string name="ext_nsfw_short">১৮+</string>
<string name="ext_permission_install_apps_warning">এক্সটেনশ্বন ইন্সটল কৰিবলৈ অনুমতিৰ প্ৰয়োজন। অনুমতি দিবলৈ ইয়াত টাপ কৰক।</string>
<string name="ext_install_service_notif">এক্সটেনশ্বন ইন্সটল কৰি আছে…</string>
<string name="ext_installer_shizuku_stopped">Shizuku চলি থকা নাই</string>
@@ -268,7 +268,9 @@
<string name="pref_category_extensions">Extensions</string>
<string name="pref_allowed_content_warnings">Allowed content warnings</string>
<string name="content_warnings_info">Requires app restart to take effect. Extensions that aren\'t loaded won\'t provide their sources anywhere in the app. This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing 18+ content within the app.</string>
<string name="pref_apply_content_warnings_to_installed">Also apply to installed extensions</string>
<string name="pref_apply_content_warnings_to_installed_summary">When off, extensions you already installed keep loading and updating regardless of their warning</string>
<string name="content_warnings_info">Installed extensions that aren\'t loaded stay listed without their sources; ones you haven\'t installed are hidden. This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing 18+ content within the app.</string>
<string name="relative_time_today">Today</string>
@@ -338,6 +340,15 @@
<string name="ext_installed">Installed</string>
<string name="ext_trust">Trust</string>
<string name="ext_untrusted">Untrusted</string>
<string name="ext_filtered">Filtered</string>
<string name="ext_not_loaded">Not loaded</string>
<string name="ext_not_loaded_dialog">Couldn\'t load</string>
<string name="ext_copy_stacktrace">Copy stack trace</string>
<string name="ext_filtered_message">This extension\'s content warning isn\'t one you chose to load, so its sources aren\'t available.</string>
<string name="ext_unsigned_message">This extension isn\'t signed, so there\'s no signature to check it against.</string>
<string name="ext_unsupported_message">This extension was built for a version of the extension library this app version can\'t load. Updating the app or the extension may help.</string>
<string name="ext_malformed_message">This extension is missing information the app needs to load it.</string>
<string name="ext_load_failed_message">This extension threw an error while loading, so none of its sources are available.</string>
<string name="ext_uninstall">Uninstall</string>
<string name="ext_remove">Remove</string>
<string name="ext_confirm_remove">Remove Extension?</string>
@@ -1088,4 +1099,16 @@
<string name="donationCampaign.paragraph3">Regardless, Mihon will continue to be maintained by me and the volunteers, and it will stay open source.</string>
<string name="donationCampaign.contactPlatform">Discord</string>
<string name="donationCampaign.dismiss">Dismiss</string>
<!-- Text reader -->
<string name="pref_category_text_reader">Text</string>
<string name="pref_text_reader_font_size">Font size</string>
<string name="pref_text_reader_line_spacing">Line spacing</string>
<string name="pref_text_reader_paragraph_spacing">Paragraph spacing</string>
<string name="pref_text_reader_font_family">Font</string>
<string name="pref_text_reader_font_system">System</string>
<string name="pref_text_reader_font_sans">Sans-serif</string>
<string name="pref_text_reader_font_serif">Serif</string>
<string name="pref_text_reader_font_mono">Monospace</string>
<string name="pref_text_reader_pdf_note">EPUB and TXT use these typography settings. PDF keeps its original page layout.</string>
</resources>
@@ -124,9 +124,6 @@
<string name="portrait">Кніжная</string>
<string name="pref_library_columns">Элементаў у радку</string>
<string name="pref_category_display">Адлюстраванне</string>
<string name="parental_controls_info">Гэта не перашкаджае неафіцыйным або патэнцыйна няправільна пазначаным пашырэнням паказваць кантэнт 18+ (NSFW) у праграме.</string>
<string name="pref_show_nsfw_source">Паказваць у спісе крыніц і пашырэнняў</string>
<string name="pref_category_nsfw_content">Крыніцы 18+ (NSFW)</string>
<string name="hide_notification_content">Схаваць змесціва апавяшчэнняў</string>
<string name="secure_screen_summary">Абарона экрана хавае змесціва праграмы пры пераключэнні паміж праграмамі і стварэнні скрыншотаў</string>
<string name="secure_screen">Абарона экрана</string>
@@ -182,8 +179,6 @@
<string name="pref_show_navigation_mode_summary">Кароткачасова паказваць пры адкрыцці чытанкі</string>
<string name="pref_show_navigation_mode">Паказваць накладанне зон дотыку</string>
<string name="pref_fullscreen">На ўвесь экран</string>
<string name="ext_nsfw_warning">Крыніцы з гэтага пашырэння могуць утрымліваць 18+ кантэнт (NSFW)</string>
<string name="ext_nsfw_short">18+</string>
<string name="obsolete_extension_message">Гэта пашырэнне больш недаступнае. Яно можа працаваць некарэктна і выклікаць праблемы ў праграме. Рэкамендуецца яго дэінсталяваць.</string>
<string name="untrusted_extension_message">Шкодныя пашырэнні могуць счытваць любыя захаваныя ўліковыя даныя або выконваць адвольны код.\n\nДавяраючы гэтаму пашырэнню, вы прымаеце гэтыя рызыкі.</string>
<string name="untrusted_extension">Ненадзейнае пашырэнне</string>
@@ -308,7 +303,6 @@
<string name="delete_category_confirmation">Вы хочаце выдаліць катэгорыю «%s»?</string>
<string name="internal_error">InternalError: Праверце журнал збояў для дадатковых зведак</string>
<string name="ext_update_all">Абнавіць усё</string>
<string name="ext_info_age_rating">Узроставы цэнз</string>
<string name="pref_update_only_started">Прапускаць непачатыя творы</string>
<string name="action_show_manga">Паказаць твор</string>
<string name="ext_info_version">Версія</string>
@@ -397,13 +397,9 @@
<string name="pref_remove_bookmarked_chapters">Позволи изтриване на отметнати глави</string>
<string name="pref_category_delete_chapters">Изтрий глави</string>
<string name="pref_category_reading_mode">Режим на четене</string>
<string name="ext_nsfw_warning">Може да съдържа неприлично (18+) съдържание</string>
<string name="ext_nsfw_short">18+</string>
<string name="parental_controls_info">Тази настройка няма да спре неофициални или потенциално неправилно маркирани разширения да позволяват достъп до съдържание за възрастни (18+) в апликацията.</string>
<string name="pref_category_theme">Тема</string>
<string name="action_sort_date_added">Добавено на</string>
<string name="no_chapters_error">Не са намерени глави</string>
<string name="pref_category_nsfw_content">NSFW (18+) източници</string>
<string name="action_desc">Низходящ</string>
<string name="action_asc">Възходящ</string>
<string name="action_order_by_chapter_number">По брой на главите</string>
@@ -514,7 +510,6 @@
<string name="spen_previous_page">Предишна страница</string>
<string name="spen_next_page">Следваща страница</string>
<string name="action_display_language_badge">Език</string>
<string name="pref_show_nsfw_source">Показвай в списъка с източници и разширения</string>
<string name="relative_time_today">Днес</string>
<string name="ext_app_info">Информация</string>
<string name="pref_show_navigation_mode_summary">Покажи зони за натискане, когато четецът се отвори</string>
@@ -596,7 +591,6 @@
<string name="theme_lavender">Лавандула</string>
<string name="pref_app_language">Език на приложението</string>
<string name="delete_category">Изтрий категория</string>
<string name="ext_info_age_rating">Възрастово ограничение</string>
<string name="wish_list">Списък с желания</string>
<string name="save_chapter_as_cbz">Запази като CBZ архив</string>
<string name="webview_data_deleted">Данните на WebView изчистени</string>
@@ -293,7 +293,6 @@
<string name="label_sources">সোর্স</string>
<string name="label_more">আরও</string>
<string name="pref_category_display">প্রদর্শন</string>
<string name="parental_controls_info">এটি অ্যাপের মধ্যে প্রাপ্তবয়স্কদের (18+) বিষয়বস্তু সারফেস করা থেকে অনানুষ্ঠানিক বা সম্ভাব্য ভুলভাবে পতাকাঙ্কিত এক্সটেনশানগুলিকে বাধা দেয় না।</string>
<string name="hide_notification_content">বিজ্ঞপ্তির বিষয়বস্তু লুকান</string>
<string name="secure_screen">পর্দা নিরাপদ করুন</string>
<string name="lock_never">কখনই না</string>
@@ -352,8 +351,6 @@
<string name="gray_background">ধূসর</string>
<string name="pref_skip_filtered_chapters">ফিলটারড অধ্যায় গুলো এড়িয়ে যান</string>
<string name="pref_cutout_short">বিষয়বস্তু কাটআউটে দেখুন</string>
<string name="ext_nsfw_warning">প্রাপ্তবয়স্কদের (18+) কন্টেন্ট থাকতে পারে</string>
<string name="ext_nsfw_short">১৮+</string>
<string name="ext_updates_pending">অনিষ্পন্ন হালনাগাদ</string>
<string name="pref_library_update_refresh_metadata_summary">সংগ্রহশালা হালনাগেদের সময় নতুন মোড়ক এবং বর্ণনা খুঁজুন</string>
<string name="pinned_sources">পিন করা</string>
@@ -418,8 +415,6 @@
<string name="pref_disable_battery_optimization_summary">পিছেনে লাইব্রেরী হালনাগাদ ও ব্যাকআপে সাহায্য করে</string>
<string name="pref_disable_battery_optimization">ব্যাটারি অপ্টিমাইজেশন অক্ষম করুন</string>
<string name="tracking_info">ট্র্যাকিং সার্ভিসে অধ্যায়ের অগ্রগতি হালনাগাদের একমুখী পথ। স্বতন্ত্র মাঙ্গার ট্র্যকিং সেটাপের জন্য তাদের নিজস্ব ট্র্যাকিং বাটন ব্যবহার করুন।</string>
<string name="pref_show_nsfw_source">উৎস এবং এক্সটেনশন তালিকায় প্রদর্শন করুন</string>
<string name="pref_category_nsfw_content">প্রাপ্তবয়স্কদের জন্য (18+) উৎস</string>
<string name="action_filter_tracked">ট্র্যাক করা হয়েছে</string>
<string name="pref_viewer_nav">ন্যাভিগেশন নকশা</string>
<string name="action_display_show_number_of_items">আইটেমের সংখ্যা প্রদর্শন করুন</string>
@@ -692,7 +687,6 @@
<string name="remove_private_extension_message">আপনি কি নিশ্চিত \"%s\" এক্সটেনশন সরাতে চান?</string>
<string name="ext_info_version">সংস্করণ</string>
<string name="ext_info_language">ভাষা</string>
<string name="ext_info_age_rating">বয়স সীমা</string>
<string name="ext_permission_install_apps_warning">এক্সটেনশন ইনস্টল করতে অনুমতি লাগবে। অনুমতি দিতে এখানে ট্যাপ করুন।</string>
<string name="ext_revoke_trust">অজানা এক্সটেনশনের ট্রাস্ট বাতিল</string>
<string name="action_open_repo">রিপোর সোর্সে যান</string>
@@ -2,12 +2,12 @@
<resources>
<plurals name="lock_after_mins">
<item quantity="one">Després d%1$s minut</item>
<item quantity="many">Després de %1$s minuts</item>
<item quantity="many">Després de %1$s de minuts</item>
<item quantity="other">Després de %1$s minuts</item>
</plurals>
<plurals name="notification_chapters_generic">
<item quantity="one">%1$d capítol nou</item>
<item quantity="many">%1$d capítols nous</item>
<item quantity="many">%1$d de capítols nous</item>
<item quantity="other">%1$d capítols nous</item>
</plurals>
<plurals name="notification_chapters_multiple_and_more">
@@ -17,12 +17,12 @@
</plurals>
<plurals name="notification_new_chapters_summary">
<item quantity="one">Per a %d element</item>
<item quantity="many">Per a %d elements</item>
<item quantity="many">Per a %d delements</item>
<item quantity="other">Per a %d elements</item>
</plurals>
<plurals name="update_check_notification_ext_updates">
<item quantity="one">Hi ha una actualització duna extensió</item>
<item quantity="many">Hi ha actualitzacions de %d extensions</item>
<item quantity="many">Hi ha actualitzacions de %d dextensions</item>
<item quantity="other">Hi ha actualitzacions de %d extensions</item>
</plurals>
<plurals name="download_queue_summary">
@@ -32,82 +32,87 @@
</plurals>
<plurals name="restore_completed_message">
<item quantity="one">Fet en %1$s amb %2$s error</item>
<item quantity="many">Fet en %1$s amb %2$s errors</item>
<item quantity="many">Fet en %1$s amb %2$s derrors</item>
<item quantity="other">Fet en %1$s amb %2$s errors</item>
</plurals>
<plurals name="num_categories">
<item quantity="one">%d categoria</item>
<item quantity="many">%d categories</item>
<item quantity="many">%d de categories</item>
<item quantity="other">%d categories</item>
</plurals>
<plurals name="manga_num_chapters">
<item quantity="one">%1$s capítol</item>
<item quantity="many">%1$s capítols</item>
<item quantity="many">%1$s de capítols</item>
<item quantity="other">%1$s capítols</item>
</plurals>
<plurals name="num_trackers">
<item quantity="one">%d servei de seguiment</item>
<item quantity="many">%d serveis de seguiment</item>
<item quantity="many">%d de serveis de seguiment</item>
<item quantity="other">%d serveis de seguiment</item>
</plurals>
<plurals name="missing_chapters_warning">
<item quantity="one">Sha omès %d capítol. És possible que manqui a la font o que hagi estat filtrat</item>
<item quantity="many">Shan omès %d capítols. És possible que manquin a la font o que hagin estat filtrats</item>
<item quantity="many">Shan omès %d de capítols. És possible que manquin a la font o que hagin estat filtrats</item>
<item quantity="other">Shan omès %d capítols. És possible que manquin a la font o que hagin estat filtrats</item>
</plurals>
<plurals name="relative_time">
<item quantity="one">Ahir</item>
<item quantity="many">Fa %1$d dies</item>
<item quantity="many">Fa %1$d de dies</item>
<item quantity="other">Fa %1$d dies</item>
</plurals>
<plurals name="next_unread_chapters">
<item quantity="one">El següent capítol no llegit</item>
<item quantity="many">Els següents %d capítols no llegits</item>
<item quantity="many">Els següents %d de capítols no llegits</item>
<item quantity="other">Els següents %d capítols no llegits</item>
</plurals>
<plurals name="download_amount">
<item quantity="one">El següent capítol</item>
<item quantity="many">Els següents %d capítols</item>
<item quantity="many">Els següents %d de capítols</item>
<item quantity="other">Els següents %d capítols</item>
</plurals>
<plurals name="missing_chapters">
<item quantity="one">Manca %1$s capítol</item>
<item quantity="many">Manquen %1$s capítols</item>
<item quantity="many">Manquen %1$s de capítols</item>
<item quantity="other">Manquen %1$s capítols</item>
</plurals>
<plurals name="day">
<item quantity="one">1 dia</item>
<item quantity="many">%d dies</item>
<item quantity="many">%d de dies</item>
<item quantity="other">%d dies</item>
</plurals>
<plurals name="num_repos">
<item quantity="one">%d repositori</item>
<item quantity="many">%d repositoris</item>
<item quantity="other">%d repositoris</item>
<item quantity="one">%d magatzem</item>
<item quantity="many">%d de magatzems</item>
<item quantity="other">%d magatzems</item>
</plurals>
<plurals name="upcoming_relative_time">
<item quantity="one">Demà</item>
<item quantity="many">Daquí a %1$d dies</item>
<item quantity="many">Daquí a %1$d de dies</item>
<item quantity="other">Daquí a %1$d dies</item>
</plurals>
<plurals name="pref_pages">
<item quantity="one">1 pàgina</item>
<item quantity="many">%1$s pàgines</item>
<item quantity="many">%1$s de pàgines</item>
<item quantity="other">%1$s pàgines</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.migrateTitle">
<item quantity="one">Voleu migrar %1$d element?</item>
<item quantity="many">Voleu migrar %1$d elements?</item>
<item quantity="many">Voleu migrar %1$d delements?</item>
<item quantity="other">Voleu migrar %1$d elements?</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.copyTitle">
<item quantity="one">Voleu copiar %1$d element?</item>
<item quantity="many">Voleu copiar %1$d elements?</item>
<item quantity="many">Voleu copiar %1$d delements?</item>
<item quantity="other">Voleu copiar %1$d elements?</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.skipText">
<item quantity="one">Sha omès un element</item>
<item quantity="many">Shan omès %1$d elements</item>
<item quantity="many">Shan omès %1$d delements</item>
<item quantity="other">Shan omès %1$d elements</item>
</plurals>
<plurals name="notification_update_error">
<item quantity="one">Ha fallat %1$d actualització</item>
<item quantity="many">Han fallat %1$d dactualitzacions</item>
<item quantity="other">Han fallat %1$d actualitzacions</item>
</plurals>
</resources>
@@ -279,8 +279,8 @@
<string name="action_webview_forward">Endavant</string>
<string name="action_webview_refresh">Actualitza</string>
<string name="pref_category_library">Biblioteca</string>
<string name="ext_obsolete">Obsoleta</string>
<string name="obsolete_extension_message">Aquesta extensió ja no està disponible. És possible que no funcioni correctament i pot causar problemes a laplicació. És recomanable que la desinstal·leu.</string>
<string name="ext_obsolete">Òrfena</string>
<string name="obsolete_extension_message">Aquesta extensió no està associada a cap magatzem. No rebrà actualitzacions, és possible que no funcioni correctament i pot causar problemes a laplicació. És recomanable que la desinstal·leu.</string>
<string name="pref_date_format">Format de data</string>
<string name="pref_category_library_update">Actualització global</string>
<string name="logout_title">Voleu tancar la sessió a %1$s?</string>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">Serveis de seguiment sense sessió iniciada:</string>
<string name="pref_remove_bookmarked_chapters">Permet la supressió dels capítols marcats</string>
<string name="pref_category_delete_chapters">Suprimeix capítols</string>
<string name="ext_nsfw_warning">Les fonts daquesta extensió poden contenir contingut NSFW (+18)</string>
<string name="ext_nsfw_short">+18</string>
<string name="parental_controls_info">Això no evita que les extensions no oficials o possiblement mal etiquetades puguin mostrar contingut NSFW (+18) a laplicació.</string>
<string name="no_chapters_error">No sha trobat cap capítol</string>
<string name="chapter_settings_updated">Sha actualitzat la configuració per defecte dels capítols</string>
<string name="set_chapter_settings_as_default">Estableix com a per defecte</string>
@@ -421,9 +418,7 @@
<string name="spen_next_page">Pàgina següent</string>
<string name="spen_previous_page">Pàgina anterior</string>
<string name="migration_help_guide">Guia de migració de fonts</string>
<string name="pref_category_nsfw_content">Fonts NSFW (+18)</string>
<string name="file_picker_error">No sha trobat cap aplicació de selecció de fitxers</string>
<string name="pref_show_nsfw_source">Mostra a la llista de fonts i extensions</string>
<string name="myanimelist_relogin">Torneu a iniciar la sessió a MAL</string>
<string name="track_finished_reading_date">Data de finalització</string>
<string name="track_started_reading_date">Data dinici</string>
@@ -580,7 +575,6 @@
<string name="empty_screen">Ostres, que estrany</string>
<string name="ext_info_language">Llengua</string>
<string name="ext_info_version">Versió</string>
<string name="ext_info_age_rating">Valoració dedat</string>
<string name="reading_list">Llista de lectura</string>
<string name="wish_list">Llista de desitjos</string>
<string name="complete_list">Llista de completats</string>
@@ -875,4 +869,85 @@
<string name="migrationListScreen.progressDialog.cancelLabel">Cancel·la</string>
<string name="migrationListScreen.matchWithoutChapterToast">No sha trobat cap capítol, aquest element no es pot utilitzar per a migrar</string>
<string name="action_filter_excluded_scanlators">Filtra els scanlators exclosos</string>
<string name="label_support_us">Doneu-nos suport</string>
<string name="theme_tokyonight">Nit a Tòquio</string>
<string name="pref_category_extensions">Extensions</string>
<string name="pref_allowed_content_warnings">Advertències de contingut permeses</string>
<string name="pref_apply_content_warnings_to_installed">Aplica-ho també a les extensions instal·lades</string>
<string name="pref_apply_content_warnings_to_installed_summary">Quan està desactivat, les extensions que ja havíeu instal·lat es continuaran carregant i actualitzant independentment de les advertències</string>
<string name="content_warnings_info">Les extensions instal·lades que no shan carregat continuaran llistades sense les fonts; les que no hàgiu instal·lat samagaran. Això no evita que les extensions no oficials o potencialment mal etiquetades mostrin contingut +18 a laplicació.</string>
<string name="ext_filtered">Filtrada</string>
<string name="ext_not_loaded">No carregada</string>
<string name="ext_not_loaded_dialog">No sha pogut carregar</string>
<string name="ext_copy_stacktrace">Copia la traça de la pila</string>
<string name="ext_filtered_message">Ladvertència de contingut daquesta extensió no és cap que hàgiu triat carregar, de manera que les seves fonts no estan disponibles.</string>
<string name="ext_unsigned_message">Aquesta extensió no està signada, de manera que no sen pot comprovar la signatura.</string>
<string name="ext_unsupported_message">Aquesta extensió sha desenvolupat per a una versió de la biblioteca dextensions que aquesta versió de laplicació no pot carregar. És possible que actualitzar laplicació o lextensió ho corregeixi.</string>
<string name="ext_malformed_message">A aquesta extensió li manca informació necessària perquè laplicació la pugui carregar.</string>
<string name="ext_load_failed_message">Aquesta extensió ha provocat un error en carregar-la, de manera que cap de les seves fonts està disponible.</string>
<string name="ext_info_warning">Advertència</string>
<string name="ext_content_warning_safe">Segura</string>
<string name="ext_content_warning_mixed">Mixta</string>
<string name="ext_content_warning_nsfw">+18</string>
<string name="ext_content_warning_mixed_description">Les fonts daquesta extensió poden contenir tant contingut segur com contingut +18.</string>
<string name="ext_content_warning_nsfw_description">Les fonts daquesta extensió contenen principalment contingut +18.</string>
<string name="downloading_with_progress">Sestà baixant… (%d%%)</string>
<string name="extensionStores">Magatzems dextensions</string>
<string name="action.addExtensionStore">Afegeix un magatzem dextensions</string>
<string name="extensionStoresScreen.emptyLabel">Encara no heu afegit cap magatzem dextensions</string>
<string name="extensionStoresScreen.addStore.title">Afegeix un magatzem dextensions</string>
<string name="extensionStoresScreen.addStore.alreadyExists">Aquest magatzem dextensions ja existeix</string>
<string name="extensionStoresScreen.addStore.processing">Sestà processant…</string>
<string name="extensionStoresScreen.addStoreInput.inputLabel">URL de l’índex del magatzem</string>
<string name="extensionStoresScreen.addStoreDeeplink.bodyText">Voleu afegir el magatzem dextensions següent?</string>
<string name="extensionStoresScreen.deleteStore.title">Suprimeix el magatzem dextensions</string>
<string name="extensionStoresScreen.deleteStore.body">Voleu suprimir el magatzem dextensions «%1$s» (%2$s)?</string>
<string name="pref_vertical_navigator">Utilitza el navegador de capítols vertical a</string>
<string name="pref_vertical_navigator_height">Alçada del navegador vertical</string>
<string name="pref_webtoon_vertical_navigator_on_left">Posa el navegador vertical al costat esquerre</string>
<string name="pref_high_quality_renderer">Utilitza el renderitzador dalta qualitat</string>
<string name="webgpu_viewer">WebGPU</string>
<string name="pref_transition_animation">Animació de transició</string>
<string name="pref_transition_animation_dual">Animació de transició (doble pàgina)</string>
<string name="transition_animation_basic">Bàsica</string>
<string name="transition_animation_flip_left">Canvi de pàgina (esquerra)</string>
<string name="transition_animation_flip_right">Canvi de pàgina (dreta)</string>
<string name="transition_animation_stack_left">Pila (esquerra)</string>
<string name="transition_animation_stack_right">Pila (dreta)</string>
<string name="transition_animation_stack_up">Pila (amunt)</string>
<string name="transition_animation_stack_down">Pila (avall)</string>
<string name="transition_animation_sphere">Esfera</string>
<string name="transition_animation_cube_inside">Cub (interior)</string>
<string name="transition_animation_cube_outside">Cub (exterior)</string>
<string name="transition_animation_fade">Esvaïment</string>
<string name="transition_animation_fade_white">Esvaïment a blanc</string>
<string name="transition_animation_none">Cap</string>
<string name="transition_animation_flip">Canvi de pàgina</string>
<string name="pref_cutout_mode">Mode de retall de pantalla</string>
<string name="pref_cutout_mode_dual">Mode de retall de pantalla (doble pàgina)</string>
<string name="cutout_mode_ignore">Ignora</string>
<string name="cutout_mode_avoid">Evita</string>
<string name="cutout_mode_shift">Desplaça</string>
<string name="pref_dual_page_view">Visualització de doble pàgina</string>
<string name="dual_page_view_never">Mai</string>
<string name="dual_page_view_always">Sempre</string>
<string name="dual_page_view_wide">Quan sigui ampla</string>
<string name="pref_continuous_minwidth">Amplada mínima</string>
<string name="pref_continuous_gap">Separació</string>
<string name="download_bookmarked">Preferida</string>
<string name="considering">Considerant</string>
<string name="pref_filter_update_categories_details">Les actualitzacions de les categories excloses no es mostraran fins i tot si són també en qualsevol categoria inclosa.</string>
<string name="pref_filter_upcoming_categories_details">Els elements de les categories excloses no es mostren, fins i tot si són també en qualsevol categoria inclosa.</string>
<string name="supportUsScreen.perks">Els mecenes tenen accés a canals exclusius, i en el futur tindran més avantatges.</string>
<string name="supportUsScreen.donationPlatform.patreon">Patreon (preferit)</string>
<string name="supportUsScreen.donationPlatform.opencollective">OpenCollective</string>
<string name="supportUsScreen.currentlySupportedBy">El Mihon rep el suport de més de %d mecenes. Volem donar-vos les gràcies a tots vosaltres. Junts, fem que el Mihon sigui encara millor.</string>
<string name="supportUsScreen.contactForDetailsMessage">Per a opcions alternatives o altres qüestions relacionades, uniu-vos al nostre servidor del Discord.</string>
<string name="supportUsScreen.contactPlatform">Discord</string>
<string name="donationCampaign.title">Us agrada laplicació?</string>
<string name="donationCampaign.paragraph1">Ep! Sóc el desenvolupador principal del Mihon, i magradaria demanar-vos que considereu donar suport financer al desenvolupament de laplicació. Amb prou suport, podré treballar en el Mihon a temps complet i centrar-me a corregir problemes que fa temps que existeixen i a desenvolupar funcionalitats molt demanades.</string>
<string name="donationCampaign.paragraph2">Podeu fer un donatiu mitjançant el Patreon (preferit) o OpenCollective. Si teniu qüestions o voleu donar-hi suport mitjançant altres mètodes alternatius, no dubteu a contactar amb mi mitjançant el servidor del Discord del Mihon.</string>
<string name="donationCampaign.paragraph3">Sigui com sigui, els voluntaris i jo continuarem mantenint el Mihon i continuarà sent de codi obert.</string>
<string name="donationCampaign.contactPlatform">Discord</string>
<string name="donationCampaign.dismiss">Ignora</string>
</resources>
@@ -129,7 +129,6 @@
<string name="lock_when_idle">I-lock kung walay trabaho</string>
<string name="hide_notification_content">Tagoa ang sulod sa pahibalo</string>
<string name="secure_screen">Lig-on nga screen</string>
<string name="pref_category_nsfw_content">NSFW (18+) Mga Tinubdan</string>
<string name="relative_time_today">Karon</string>
<string name="pref_category_display">Pagpakita</string>
<string name="pref_library_columns">Mga butang kada laray</string>
@@ -168,9 +167,7 @@
<string name="theme_strawberrydaiquiri">Strawberry Daiquiri</string>
<string name="pref_manage_notifications">Pagdumala sa mga pahibalo</string>
<string name="pref_category_security">Seguridad</string>
<string name="pref_show_nsfw_source">Ipakita sa mga tinubdan ug mga lista sa mga extension</string>
<string name="pref_dark_theme_pure_black">Purong itom nga dark mode</string>
<string name="parental_controls_info">Dili kini makapugong sa dili opisyal o posibleng sayop nga gi-flag nga mga extension gikan sa pag-surf sa NSFW (18+) nga sulod sulod sa app.</string>
<string name="lock_always">Kanunay</string>
<string name="secure_screen_summary">Ang luwas nga screen nagtago sa mga sulod sa app sa dihang nagbalhin-balhin og mga app ug nag-block sa mga screenshot</string>
<string name="pref_library_update_interval">Awtomatikong mga update</string>
@@ -192,8 +189,6 @@
<string name="untrusted_extension">Dili kasaligan nga extension</string>
<string name="obsolete_extension_message">Kini nga extension dili na magamit.</string>
<string name="extension_api_error">Napakyas sa pagkuha sa listahan sa mga extension</string>
<string name="ext_nsfw_short">18+</string>
<string name="ext_nsfw_warning">Ang mga tinubdan gikan niini nga extension mahimong adunay sulod nga NSFW (18+)</string>
<string name="ext_install_service_notif">Nag-instalar og extension…</string>
<string name="ext_installer_pref">Installer</string>
<string name="ext_installer_legacy">Kabilin</string>
@@ -300,7 +295,6 @@
<string name="network_not_metered">Lamang sa ummetered network</string>
<string name="ext_info_language">Pinulongan sa Extension</string>
<string name="ext_info_version">Extension nga Bersyon</string>
<string name="ext_info_age_rating">Rating sa Edad sa Extension</string>
<string name="vertical_plus_viewer">Padayon nga bertikal</string>
<string name="scale_type_original_size">Orihinal nga gidak-on</string>
<string name="scale_type_fit_screen">Haom nga screen</string>
@@ -169,7 +169,7 @@
<string name="portrait">Na výšku</string>
<string name="landscape">Na šířku</string>
<string name="pref_library_update_interval">Automatické aktualizace</string>
<string name="update_never">Nikdy</string>
<string name="update_never">Vyp.</string>
<string name="all">Vše</string>
<string name="charging">Při nabíjení</string>
<string name="default_category">Výchozí kategorie</string>
@@ -365,13 +365,8 @@
<string name="tapping_inverted_vertical">Vertikální</string>
<string name="tapping_inverted_horizontal">Horizontální</string>
<string name="tapping_inverted_none">Žádné</string>
<string name="ext_nsfw_warning">Zdroje z tohoto rozšíření mohou obsahovat NSFW (18+) obsah</string>
<string name="ext_nsfw_short">18+</string>
<string name="pref_library_update_refresh_metadata_summary">Kontrolovat dostupnost nových přebalů a detailů při aktualizaci knihovny</string>
<string name="pref_library_update_refresh_metadata">Automaticky obnovovat metadata</string>
<string name="parental_controls_info">Toto nastavení nemůže zamezit neoficiálním nebo špatně označeným rozšířením v zobrazování NSFW (18+) obsahu v aplikaci.</string>
<string name="pref_show_nsfw_source">Zobrazovat v seznamu zdrojů a rozšíření</string>
<string name="pref_category_nsfw_content">NSFW (18+) zdroje</string>
<string name="secure_screen_summary">Secure screen skryje obsah aplikace při přepínání aplikací a blokuje tvorbu snímků obrazovky</string>
<string name="secure_screen">Chráněná obrazovka</string>
<string name="lock_with_biometrics">Vyžadovat odemknutí</string>
@@ -577,7 +572,6 @@
<string name="on_hold_list">Seznam pozastavených</string>
<string name="unfinished_list">Seznam nedokončených</string>
<string name="ext_info_language">Jazyk</string>
<string name="ext_info_age_rating">Věkové hodnocení</string>
<string name="ext_info_version">Verze</string>
<string name="split_tall_images_summary">Zlepšuje výkon čtečky</string>
<string name="pref_reset_viewer_flags_error">Nastavení čtečky se nepodařilo resetovat</string>
@@ -907,4 +901,33 @@
<string name="downloading_with_progress">Stahování… (%d%%)</string>
<string name="pref_filter_update_categories_details">Aktualizace v kategoriích, které byly vyloučeny, se nezobrazí, i když se zároveň nacházejí v některé ze zahrnutých kategorií.</string>
<string name="pref_filter_upcoming_categories_details">Položky ve vyloučených kategoriích se nezobrazují, i když patří také do některé ze zahrnutých kategorií.</string>
<string name="transition_animation_flip_right">Otočení stránky (doprava)</string>
<string name="webgpu_viewer">WebGPU</string>
<string name="transition_animation_cube_inside">Krychle (vnitřek)</string>
<string name="transition_animation_cube_outside">Krychle (vnějšek)</string>
<string name="cutout_mode_avoid">Vyhnout se</string>
<string name="transition_animation_sphere">Koule</string>
<string name="transition_animation_stack_down">Balíčky (dolů)</string>
<string name="pref_continuous_gap">Mezera</string>
<string name="transition_animation_basic">Základní</string>
<string name="pref_continuous_minwidth">Minimální šířka</string>
<string name="transition_animation_flip_left">Otočení stránky (doleva)</string>
<string name="cutout_mode_ignore">Ignorovat</string>
<string name="transition_animation_stack_right">Balíčky (vpravo)</string>
<string name="transition_animation_fade">Blednutí</string>
<string name="transition_animation_none">Žádné</string>
<string name="dual_page_view_never">Nikdy</string>
<string name="transition_animation_stack_up">Balíčky (nahoru)</string>
<string name="dual_page_view_wide">Při širokém</string>
<string name="pref_transition_animation_dual">Přechodová animace (dvojitá)</string>
<string name="transition_animation_flip">Otočení stránky</string>
<string name="dual_page_view_always">Vždy</string>
<string name="transition_animation_fade_white">Přechod do bílé</string>
<string name="pref_high_quality_renderer">Použít renderer vysoké kvality</string>
<string name="pref_dual_page_view">Zobrazení dvou stránek</string>
<string name="pref_transition_animation">Přechodová animace</string>
<string name="pref_cutout_mode">Režim výřezu displeje</string>
<string name="transition_animation_stack_left">Balíčky (vlevo)</string>
<string name="pref_cutout_mode_dual">Režim výřezu displeje (duální)</string>
<string name="cutout_mode_shift">Posun</string>
</resources>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">Кӗмен йӗрлевсем:</string>
<string name="pref_remove_bookmarked_chapters">Картнӑ сыпӑксене катертмелле</string>
<string name="pref_category_delete_chapters">Сыпӑксене катерт</string>
<string name="ext_nsfw_warning">NSFW (18+) шалаш пулма пултарать</string>
<string name="ext_nsfw_short">18+</string>
<string name="parental_controls_info">Ҫакӑ апра чӑнлавлӑ мар йе тӗрӗс мар палӑртнӑ хушмасен NSFW (18+) шалашне кӑтартма чӑрмантармасть.</string>
<string name="no_chapters_error">Сыпӑксем ҫук</string>
<string name="also_set_chapter_settings_for_library">Тата манӑн вулавăшри пур манкӑ валли усӑ кур</string>
<string name="chapter_settings_updated">Яланхилле сыпӑк ӗнерлев ҫӗнетнӗ</string>
@@ -421,10 +418,8 @@
<string name="spen_next_page">Маллали сыпӑк</string>
<string name="spen_previous_page">Умӗнхи эл</string>
<string name="migration_help_guide">Ҫӑл куҫ куҫарассипе пулӑшу</string>
<string name="pref_category_nsfw_content">NSFW (18+) ҫӑл куҫсем</string>
<string name="file_picker_error">Файлсене суйламалли хушӑм тупӑнман</string>
<string name="myanimelist_relogin">Тархасшӑн MAL-а ҫӗнӗрен кӗр</string>
<string name="pref_show_nsfw_source">Ҫӑл куҫсен тата хушмасен йат-йышӗнче кӑтартни</string>
<string name="track_finished_reading_date">Вулама вӗҫленӗ вӑхӑчӗ</string>
<string name="track_started_reading_date">Вулама пуҫланӑ вӑхӑчӗ</string>
<string name="edge_nav">Хӗррипе</string>
@@ -545,7 +540,6 @@
<string name="pref_chapter_swipe_start">Сулахайалла туртсан сыпӑк</string>
<string name="ext_installer_pref">Ларткӑч</string>
<string name="ext_installer_shizuku_stopped">Shizuku ӗҫлемест</string>
<string name="ext_info_age_rating">Ҫул-ӳсӗм чаравӗ</string>
<string name="action_sort_next_updated">Тепӗр кӗтӗнекен ҫӗнетӳпе</string>
<string name="ext_install_service_notif">Хушма лартни…</string>
<string name="action_set_interval">Тӑтӑшлӑх палӑрт</string>
@@ -142,9 +142,6 @@
<string name="pref_manage_notifications">Håndter notifikationer</string>
<string name="pref_category_security">Sikkerhed</string>
<string name="lock_with_biometrics">Kræv oplåsning</string>
<string name="pref_category_nsfw_content">NSFW (18+) kilder</string>
<string name="pref_show_nsfw_source">Vis i kilder og udvidelsesliste</string>
<string name="parental_controls_info">Dette forhindrer ikke uofficielle eller potentielt ukorrekt markerede udvidelser fra at vise NSFW (18+) indhold i appen.</string>
<string name="relative_time_today">I dag</string>
<string name="lock_when_idle">Lås ved inaktivitet</string>
<string name="lock_always">Altid</string>
@@ -243,7 +240,6 @@
<string name="nav_zone_right">Højre</string>
<string name="nav_zone_prev">Forrige</string>
<string name="ext_downloading">Henter ned</string>
<string name="ext_nsfw_short">18+</string>
<string name="ext_uninstall">Afinstallér</string>
<string name="action_not_now">Ikke nu</string>
<string name="ext_install_service_notif">Installerer udvidelse…</string>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">Nicht-angemeldete Tracker:</string>
<string name="pref_remove_bookmarked_chapters">Erlauben, mit Lesezeichen versehene Kapitel zu löschen</string>
<string name="pref_category_delete_chapters">Kapitel löschen</string>
<string name="ext_nsfw_warning">Quellen dieser Erweiterung könnten NSFW/Ü18-Inhalte enthalten</string>
<string name="ext_nsfw_short">Ü18</string>
<string name="parental_controls_info">Dies verhindert nicht, dass inoffizielle oder möglicherweise falsch gekennzeichnete Erweiterungen NSFW/Ü18-Inhalte in der App anzeigen.</string>
<string name="no_chapters_error">Keine Kapitel gefunden</string>
<string name="chapter_settings_updated">Standard-Kapiteleinstellungen aktualisiert</string>
<string name="set_chapter_settings_as_default">Als Standard setzen</string>
@@ -421,8 +418,6 @@
<string name="spen_next_page">Nächste Seite</string>
<string name="spen_previous_page">Vorherige Seite</string>
<string name="migration_help_guide">Quellenmigration</string>
<string name="pref_category_nsfw_content">NSFW/Ü18-Quellen</string>
<string name="pref_show_nsfw_source">In Quellen- und Erweiterungslisten anzeigen</string>
<string name="file_picker_error">Keine Dateiauswahl-App gefunden</string>
<string name="myanimelist_relogin">Bitte logge dich erneut in MAL ein</string>
<string name="edge_nav">Rand</string>
@@ -580,7 +575,6 @@
<string name="empty_screen">Tja, das ist jetzt etwas peinlich</string>
<string name="ext_info_version">Version</string>
<string name="ext_info_language">Sprache</string>
<string name="ext_info_age_rating">Altersfreigabe</string>
<string name="wish_list">Wunschliste</string>
<string name="complete_list">Abgeschlossen-Liste</string>
<string name="on_hold_list">Warteliste</string>
@@ -907,4 +901,13 @@
<string name="downloading_with_progress">Wird heruntergeladen… (%d%%)</string>
<string name="pref_filter_update_categories_details">Aktualisierungen in ausgeschlossen Kategorien werden nicht angezeigt, selbst wenn sie in eingeschlossenen Kategorien vorkommen.</string>
<string name="pref_filter_upcoming_categories_details">Titel in ausgeschlossen Kategorien werden nicht angezeigt, selbst wenn sie in eingeschlossenen Kategorien vorkommen.</string>
<string name="pref_category_extensions">Erweiterungen</string>
<string name="pref_allowed_content_warnings">Erlaubte Inhaltswarnungen</string>
<string name="pref_apply_content_warnings_to_installed">Auch auf installierte Erweiterungen anwenden</string>
<string name="pref_apply_content_warnings_to_installed_summary">Wenn deaktiviert, werden bereits installierte Erweiterungen unabhängig von ihren Warnungen weiterhin geladen und aktualisiert</string>
<string name="content_warnings_info">Installierte Erweiterungen, die nicht geladen sind, bleiben ohne ihre Quellen gelistet; nicht installierte Erweiterungen bleiben versteckt. Dies verhindert nicht, dass inoffizielle oder potentiell falsch markierte Erweiterungen Ü18-Inhalte in der App anzeigen.</string>
<string name="ext_filtered">Gefiltert</string>
<string name="ext_not_loaded">Nicht geladen</string>
<string name="ext_not_loaded_dialog">Konnte nicht geladen werden</string>
<string name="ext_copy_stacktrace">Stacktrace kopieren</string>
</resources>
@@ -399,10 +399,7 @@
<string name="channel_errors">Σφάλματα</string>
<string name="backup_restore_missing_trackers">Trackers που δεν έχουν συνδεθεί:</string>
<string name="pref_category_delete_chapters">Διαγραφή κεφαλαίων</string>
<string name="ext_nsfw_warning">Πηγές από αυτήν την επέκταση ενδέχεται να περιέχουν περιεχόμενο NSFW (18+)</string>
<string name="ext_nsfw_short">18+</string>
<string name="pref_remove_bookmarked_chapters">Να επιτρέπεται η διαγραφή κεφαλαίων με σελιδοδείκτη</string>
<string name="parental_controls_info">Αυτό δεν εμποδίζει τις ανεπίσημες ή ενδεχομένως εσφαλμένα επισημασμένες επεκτάσεις να εμφανίζουν περιεχόμενο NSFW (18+) μέσα στην εφαρμογή.</string>
<string name="no_chapters_error">Δεν βρέθηκαν κεφάλαια</string>
<string name="chapter_settings_updated">Ενημερώθηκαν οι προεπιλεγμένες ρυθμίσεις κεφαλαίου</string>
<string name="set_chapter_settings_as_default">Ορισμός ως προεπιλογής</string>
@@ -421,8 +418,6 @@
<string name="spen_next_page">Επόμενη σελίδα</string>
<string name="spen_previous_page">Προηγούμενη σελίδα</string>
<string name="migration_help_guide">Οδηγός μεταφοράς πηγής</string>
<string name="pref_category_nsfw_content">Πηγές NSFW (18+)</string>
<string name="pref_show_nsfw_source">Εμφάνιση στις λίστες πηγών και επεκτάσεων</string>
<string name="file_picker_error">Δεν βρέθηκε εφαρμογή επιλογής αρχείων</string>
<string name="myanimelist_relogin">Συνδεθείτε ξανά στο MAL</string>
<string name="pref_viewer_nav">Ζώνες πατήματος</string>
@@ -580,7 +575,6 @@
<string name="empty_screen">Λοιπόν, αυτό είναι άβολο</string>
<string name="ext_info_version">Έκδοση</string>
<string name="ext_info_language">Γλώσσα</string>
<string name="ext_info_age_rating">Ηλικιακή βαθμολογία</string>
<string name="unfinished_list">Ημιτελής λίστα</string>
<string name="complete_list">Πλήρης Λίστα</string>
<string name="wish_list">Λίστα επιθυμιών</string>
@@ -120,7 +120,6 @@
<string name="white_background">Blanka</string>
<string name="pref_reader_theme">Fona koloro</string>
<string name="pref_read_with_volume_keys">Laŭtecaj klavoj</string>
<string name="ext_nsfw_short">18+</string>
<string name="nav_zone_next">Sekva</string>
<string name="nav_zone_prev">Antaŭa</string>
<string name="browse">Foliumi</string>
@@ -175,8 +174,6 @@
<string name="landscape">Horizontale</string>
<string name="portrait">Vertikale</string>
<string name="pref_category_display">Montrado</string>
<string name="pref_show_nsfw_source">Montri en fontaj kaj etendaĵaj listoj</string>
<string name="pref_category_nsfw_content">NSFW-fontoj (18+)</string>
<string name="pref_category_general">Ĝeneralaj</string>
<string name="app_not_available">Apo maldisponebla</string>
<string name="action_restore">Restaŭri</string>
@@ -379,7 +376,6 @@
<string name="rotation_portrait">Vertikala</string>
<string name="rotation_landscape">Horizontala</string>
<string name="pref_grayscale">Grizoskalo</string>
<string name="ext_nsfw_warning">Fontoj de ĉi tiu etendaĵo eble enhavas NSFW-enhavon (18+)</string>
<string name="untrusted_extension">Malfidinda etendaĵo</string>
<string name="charging">Dum ŝargado</string>
<string name="lock_when_idle">Ŝlosi kiam senokupa</string>
@@ -553,7 +549,6 @@
<string name="pref_reader_summary">Legada reĝimo, montrado, navigado</string>
<string name="pref_firebase">Analitikoj kaj protokoloj</string>
<string name="firebase_summary">Sendado de prikolapsaj protokoloj kaj analitikoj eblos al ni identigi kaj ripari problemojn, plibonigi rendimenton kaj igi estontajn ĝisdatigojn pli rilataj al viaj bezonoj</string>
<string name="parental_controls_info">Tio ne preventas neoficialajn aŭ eventuale malĝuste markitajn etendaĵojn montri NSFW-enhavon (18+) ene de la apo.</string>
<string name="update_72hour">Ĉiun 3-an tagon</string>
<string name="pref_update_only_started">Preterpasi nekomencitajn elementojn</string>
<string name="pref_chapter_swipe_end">Ĉapitro je ŝovumo dekstren</string>
@@ -574,7 +569,6 @@
<string name="multi_lang">Plur</string>
<string name="ext_app_info">Pri apo</string>
<string name="extension_api_error">Malsukcesis akiri etendaĵan liston</string>
<string name="ext_info_age_rating">Aĝa takso</string>
<string name="ext_permission_install_apps_warning">Permesoj bezonatas por instali etendaĵojn. Tuŝetu ĉi tie por permesi.</string>
<string name="ext_install_service_notif">Instalado de etendaĵo…</string>
<string name="network_not_metered">Nur per nemezurata reto</string>
@@ -907,4 +901,5 @@
<string name="considering">Konsiderataj</string>
<string name="pref_filter_update_categories_details">Ĝisdatigoj en ekskluzivitaj kategorioj ne estos montrotaj, eĉ se ili estas ankaŭ en inkluzivitaj kategorioj.</string>
<string name="pref_filter_upcoming_categories_details">Titoloj en ekskluzivitaj kategorioj ne estos montrotaj, eĉ se ili estas ankaŭ en inkluzivitaj kategorioj.</string>
<string name="pref_category_extensions">Etendaĵoj</string>
</resources>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">Servicios de seguimiento sin conectar:</string>
<string name="pref_remove_bookmarked_chapters">Permitir el borrado de capítulos marcados como favoritos</string>
<string name="pref_category_delete_chapters">Borrar capítulos</string>
<string name="ext_nsfw_warning">Puede contener contenido para adultos (+18)</string>
<string name="ext_nsfw_short">+18</string>
<string name="parental_controls_info">Esto no evita que las extensiones extraoficiales o que estén mal clasificadas muestren contenido para mayores de 18 años en la aplicación.</string>
<string name="no_chapters_error">No hay capítulos</string>
<string name="chapter_settings_updated">Se han actualizado los ajustes predeterminados de capítulo</string>
<string name="set_chapter_settings_as_default">Establecer como predeterminado</string>
@@ -421,8 +418,6 @@
<string name="spen_next_page">Siguiente página</string>
<string name="spen_previous_page">Página anterior</string>
<string name="migration_help_guide">Guía de migración de fuentes</string>
<string name="pref_category_nsfw_content">Fuentes para adultos (+18)</string>
<string name="pref_show_nsfw_source">Mostrar en la lista de fuentes y extensiones</string>
<string name="file_picker_error">No se ha encontrado ninguna aplicación con la que elegir archivos</string>
<string name="myanimelist_relogin">Vuelve a iniciar sesión en MAL</string>
<string name="kindlish_nav">Estilo Kindle</string>
@@ -580,7 +575,6 @@
<string name="empty_screen">Houston, tenemos un problema</string>
<string name="ext_info_version">Versión</string>
<string name="ext_info_language">Idioma</string>
<string name="ext_info_age_rating">Clasificación por edades</string>
<string name="reading_list">Leyendo</string>
<string name="wish_list">A leer en un futuro</string>
<string name="complete_list">Leídos del todo</string>
@@ -99,7 +99,6 @@
<string name="ext_pending">Egiteke</string>
<string name="ext_downloading">Deskargatzen</string>
<string name="ext_installing">Instalatzen</string>
<string name="ext_nsfw_warning">NSFW (18+) edukia izan dezake</string>
<string name="ext_installer_pref">Instalatzailea</string>
<string name="pref_fullscreen">Pantaila osoa</string>
<string name="pref_grayscale">Gris-eskala</string>
@@ -163,7 +162,6 @@
<string name="update_check_confirm">Deskargatu</string>
<string name="download_notifier_title_error">Errorea</string>
<string name="ext_installer_legacy">Ondare</string>
<string name="ext_nsfw_short">18+</string>
<string name="off">Itzali</string>
<string name="pref_reader_navigation">Nabigazioa</string>
<string name="color_filter_b_value">U</string>
@@ -395,9 +393,6 @@
<string name="pref_category_security">Segurtasuna</string>
<string name="lock_always">Beti</string>
<string name="secure_screen_summary">Pantaila seguruak aplikazioaren edukia ezkutatzen du aplikazioak aldatzean, eta pantaila-argazkiak blokeatzen ditu</string>
<string name="pref_category_nsfw_content">NSFW (18+) iturriak</string>
<string name="pref_show_nsfw_source">Erakutsi iturrien eta luzapenen zerrendetan</string>
<string name="parental_controls_info">Honek ez du eragozten ofizialak ez diren edo oker markatutako luzapenek NSFW (18+) edukia aplikazioan erakustea.</string>
<string name="relative_time_today">Gaur</string>
<string name="pref_category_display">Bistaratu</string>
<string name="pref_library_columns">Errenkada bakoitzeko elementu kopurua</string>
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="update_check_notification_ext_updates">
<item quantity="one">آپدیت برای یک افرونه موجود است</item>
<item quantity="other">آپدیت برای %d افرونه موجود است</item>
<item quantity="one">به‌روزرسانی برای افزونه موجود است</item>
<item quantity="other">%d به‌روزرسانی برای افزونه موجود است</item>
</plurals>
<plurals name="notification_chapters_multiple_and_more">
<item quantity="one">قسمت های %1$s و 1 قسمت دیگر</item>
@@ -64,4 +64,32 @@
<item quantity="one">1 روز</item>
<item quantity="other">%d روز</item>
</plurals>
<plurals name="pref_pages">
<item quantity="one">۱ صفحه</item>
<item quantity="other">%1$s صفحه</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.migrateTitle">
<item quantity="one">%1$d عنوان منتقل شود؟</item>
<item quantity="other">%1$d عنوان منتقل شود؟</item>
</plurals>
<plurals name="upcoming_relative_time">
<item quantity="one">فردا</item>
<item quantity="other">در %1$d روز</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.skipText">
<item quantity="one">یک عنوان نادیده گرفته شد</item>
<item quantity="other">%1$d عنوان نادیده گرفته شدند</item>
</plurals>
<plurals name="num_repos">
<item quantity="one">%d فروشگاه</item>
<item quantity="other">%d فروشگاه</item>
</plurals>
<plurals name="migrationListScreen.migrateDialog.copyTitle">
<item quantity="one">%1$d عنوان رونوشت شود؟</item>
<item quantity="other">%1$d عنوان رونوشت شود؟</item>
</plurals>
<plurals name="notification_update_error">
<item quantity="one">%1$d به‌روزرسانی ناموفق بود</item>
<item quantity="other">%1$d به‌روزرسانی ناموفق بود</item>
</plurals>
</resources>
+308 -23
View File
@@ -78,8 +78,8 @@
<string name="in_library">در کتابخانه</string>
<string name="tracking_info">برای به روز رسانی وضعیت پیشرفت قسمت ها، در سایت های خدمات ردیابی، همگام سازی یک طرفه انجام دهید. برای ردیابی هر داده از تب ردیابی اقدام کنید.</string>
<string name="webtoon_viewer">وب تون(Webtoon)</string>
<string name="filter_mode_darken">Burn / Darken</string>
<string name="filter_mode_lighten">Dodge / Lighten</string>
<string name="filter_mode_darken">سوزاندن / تیره‌کردن</string>
<string name="filter_mode_lighten">روشن کردن / کاهش تیرگی</string>
<string name="update_12hour">هر ۱۲ ساعت یکبار</string>
<string name="action_move_category">انتخاب دسته بندی</string>
<string name="information_empty_category">هیچ دسته بندی وجود ندارد. با زدن دکمه به علاوه دسته بندی جدیدی بسازید.</string>
@@ -138,9 +138,9 @@
<string name="decode_image_error">بازکردن عکس با خطا مواجه شد</string>
<string name="plan_to_read">قصد خواندن دارم(Plan to read)</string>
<string name="scale_type_smart_fit">هم اندازه حالت هوشمند</string>
<string name="filter_mode_multiply">Multiply</string>
<string name="filter_mode_overlay">Overlay</string>
<string name="pref_double_tap_anim_speed">Double tap animation speed</string>
<string name="filter_mode_multiply">ضرب کردن</string>
<string name="filter_mode_overlay">پوشش</string>
<string name="pref_double_tap_anim_speed">سرعت انیمیشن دو بار ضربه زدن</string>
<string name="ext_install">نصب</string>
<string name="action_bookmark">نشانک قسمت</string>
<string name="channel_ext_updates">آپدیت افزونه‌ها</string>
@@ -155,7 +155,7 @@
<string name="information_webview_outdated">لطفا برنامه وب نما را برای سازگاری بهتر آپدیت کنید</string>
<string name="information_webview_required">وب نما برای تاچیومی مورد نیاز است</string>
<string name="information_cloudflare_bypass_failure">عدم موفقیت در دور زدن کولدفیر</string>
<string name="color_filter_a_value">opacity</string>
<string name="color_filter_a_value">آلفا</string>
<string name="color_filter_b_value">آبی</string>
<string name="color_filter_g_value">سبز</string>
<string name="color_filter_r_value">قرمز</string>
@@ -204,7 +204,7 @@
<string name="pref_show_page_number">نمایش شماره صفحه</string>
<string name="pref_page_transitions">انمیشن‌های تغییر صفحه</string>
<string name="pref_fullscreen">تمام صفحه</string>
<string name="obsolete_extension_message">این افزونه دیگر در دسترس نیست. ممکن است به درستی کار نکند و باعث ایجاد مشکلاتی در برنامه شود. حذف نصب آن توصیه می شود.</string>
<string name="obsolete_extension_message">این افزونه به هیچ فروشگاهی وابسته نیست. به‌روزرسانی دریافت نخواهد کرد، ممکن است بهدرستی کار نکند و می‌تواند باعث بروز مشکلاتی در برنامه شود. حذف آن توصیه میشود.</string>
<string name="untrusted_extension_message">یک افزونه مخرب می تواند هر گونه طلاعات ورود به سیستم زخیره شده را بخواند یا اختیاری یک کد را اجرا کند. \n \nبا اعتماد به این گواهی شما این خطرات را می‌پذیرید.</string>
<string name="untrusted_extension">افزونه نامطمئن</string>
<string name="ext_uninstall">حذف نصب</string>
@@ -214,7 +214,7 @@
<string name="ext_installing">در حال نصب</string>
<string name="ext_downloading">در حال بارگیری</string>
<string name="ext_pending">در انتظار</string>
<string name="ext_obsolete">منسوخ شده</string>
<string name="ext_obsolete">بدون منبع</string>
<string name="ext_update">آپدیت</string>
<string name="ext_updates_pending">آپدیت‌های در انتظار</string>
<string name="all">همه</string>
@@ -243,7 +243,7 @@
<string name="lock_always">همیشه</string>
<string name="lock_when_idle">قفل صفحه هنگام بیکاری</string>
<string name="lock_with_biometrics">نیاز به باز کردن قفل</string>
<string name="pref_category_security">امنیت</string>
<string name="pref_category_security">امنیت و حریم خصوصی</string>
<string name="pref_manage_notifications">مدیریت اعلان‌ها</string>
<string name="pref_date_format">فرمت تاریخ</string>
<string name="theme_dark">روشن</string>
@@ -341,7 +341,7 @@
<string name="restore_completed">بازیابی کامل شد</string>
<string name="backup_restore_missing_sources">منابع ناموجود:</string>
<string name="invalid_backup_file_missing_manga">فایل پشتیبان حاوی هیچ ورودی کتابخانه نیست.</string>
<string name="invalid_backup_file">فایل پشتیبان نامعتبر است</string>
<string name="invalid_backup_file">فایل پشتیبان نامعتبر است:</string>
<string name="backup_created">نسخه پشتیبان ایجاد شد</string>
<string name="action_edit_cover">ویرایش عکس روی جلد</string>
<string name="action_rename_category">تغییر نام دسته</string>
@@ -400,16 +400,12 @@
<string name="pref_incognito_mode">حالت ناشناس</string>
<string name="clear_history_confirmation">آیا مطمئن هستید؟ تمام سابقه از دست خواهد رفت.</string>
<string name="clear_history_completed">سابقه پاک شد</string>
<string name="ext_nsfw_short">18+</string>
<string name="pref_category_nsfw_content">منابع محتوای جنسی (+18)</string>
<string name="parental_controls_info">این کار از نمایش محتوای 18+ (محتوای نامناسب) توسط افزونه‌های غیررسمی یا اشتباه برچسب‌خورده جلوگیری نمی‌کند.</string>
<string name="no_chapters_error">هیچ قسمتی پیدا نشد</string>
<string name="set_chapter_settings_as_default">تنظیم به عنوان پیشفرض</string>
<string name="also_set_chapter_settings_for_library">همچنین برای تمام مانگاهای موجود در کتابخانه من اعمال شود</string>
<string name="confirm_set_chapter_settings">آیا مطمئن هستید که می خواهید این تنظیمات را به صورت پیش فرض ذخیره کنید؟</string>
<string name="pref_remove_bookmarked_chapters">اجازه‌ی پاک‌کردن قسمت‌های نشانک‌گذاری شده را بده</string>
<string name="pref_category_delete_chapters">حذف قسمت ها</string>
<string name="ext_nsfw_warning">منابع این افزونه ممکن است شامل محتوای نامناسب (+18) باشند</string>
<string name="spen_next_page">صفحه بعدی</string>
<string name="spen_previous_page">صفحه قبلی</string>
<string name="tapping_inverted_both">هر دو</string>
@@ -424,7 +420,6 @@
<string name="file_picker_error">هیچ نرم افزار انتخاب فایلی یافت نشد</string>
<string name="migration_help_guide">راهنمای تعویض منبع</string>
<string name="pref_clear_history">پاک کردن سابقه</string>
<string name="pref_show_nsfw_source">نمایش در لیست منابع و افزونه‌ها</string>
<string name="nav_zone_left">چپ</string>
<string name="nav_zone_right">راست</string>
<string name="ext_installer_legacy">قدیمی</string>
@@ -491,7 +486,6 @@
<string name="pref_show_navigation_mode_summary">یک لحظه هنگام باز شدن متن‌خوان نمایش بده</string>
<string name="ext_info_version">نسخه</string>
<string name="ext_info_language">زبان</string>
<string name="ext_info_age_rating">رده‌بندی سنی</string>
<string name="kindlish_nav">شبیه Kindle</string>
<string name="rotation_landscape">افقی</string>
<string name="rotation_reverse_portrait">عمودی معکوس</string>
@@ -561,15 +555,15 @@
<string name="action_not_now">الان نه</string>
<string name="action_display_show_continue_reading_button">نمایش دکمه ادامه خواندن</string>
<string name="split_tall_images_summary">بهبود کارکرد ابزار خواندن</string>
<string name="download_ahead_info">اگر قسمت فعلی و بعدی دانلود شده باشند، تنها برای ورودی‌های کتابخانه کار می‌کند</string>
<string name="missing_storage_permission">اجازه دسترسی به حافظه داده نشده.</string>
<string name="backup_restore_content_full">داده ها از فایل پشتیبان برگردانده میشوند. \n \nلازم است که افزونه های جا مانده را دستی نصب کنید و وارد یک سرویس دنبال کننده شوید تا بتوانید پیشرفت هر کار را ذخیره کنید.</string>
<string name="download_ahead_info">اگر قسمت فعلی و بعدی دانلود شده باشند، تنها برای ورودی‌های کتابخانه کار می‌کند.</string>
<string name="missing_storage_permission">اجازه دسترسی به حافظه داده نشد</string>
<string name="backup_restore_content_full">ممکن است لازم باشد افزونههای احتمالیِ موجود نبودن را نصب کنید و پس از آن، برای استفاده از سرویس‌های ردیابی، به آن‌ها وارد شوید.</string>
<string name="tracking_guide">راهنمای ردیابی</string>
<string name="pref_skip_dupe_chapters">چپتر های تکراری را رد کن</string>
<string name="enhanced_tracking_info">فعالسازی سرویس پیشرفته برای منابعی که پشتیبانی کنند. کار های افزوده شده به کتابخانه به صورت خودکار ردگیری میشوند.</string>
<string name="action_track">ردگیری</string>
<string name="empty_backup_error">هیچ داده کتابخانه ای نیست که پشتیبانی شود</string>
<string name="restore_miui_warning">پشتیبانی/بازگردانی ممکن است کار نکند اگر بهینه‌سازی MIUI غیر فعال باشد</string>
<string name="restore_miui_warning">اگر قابلیت «بهینه‌سازی MIUI» غیرفعال باشد، ممکن است عملیات پشتیبان‌گیری/بازیابی به‌درستی کار نکند.</string>
<string name="enhanced_services_not_installed">در دسترس است اما افزونه منبع نصب نشده است: %s</string>
<string name="backup_info">شما باید از پشتیبانی ها در جا های دیگر هم کپی داشته باشید.</string>
<string name="action_update_category">بروزرسانی دسته بندی</string>
@@ -589,15 +583,15 @@
<string name="action_ok">باشه</string>
<string name="action_sort_next_updated">به روز رسانی مورد انتظار بعدی</string>
<string name="download_queue_size_warning">هشدار: حجم زیاد بارگیری ممکن است باعث اهسته تر شدن سرعت ویا مسدود کردن %s از منبع شود. برای اطلاعات بیشتر لمس کنید.</string>
<string name="skipped_reason_not_always_update">به دلیل این که این مجموعه نیازی به به روز رسانی نداشت رد شد</string>
<string name="skipped_reason_not_always_update">به دلیل این که این سری نیازی به به روز رسانی نداشت رد شد</string>
<string name="skipped_reason_not_caught_up">به دلیل وجود چپتر های خوانده نشده رد شد</string>
<string name="skipped_reason_not_started">به دلیل این که هیچ چپتری خوانده نشده بود رد شد</string>
<string name="pref_relative_format_summary">\"%1$s\" به جای \"%2$s\"</string>
<string name="pref_page_rotate">صفحات عریض را بچرخان تا جا شوند</string>
<string name="popular">محبوب</string>
<string name="pref_chapter_swipe_end">حرکت کشیدن به راست</string>
<string name="pref_chapter_swipe_end">چپتر مربوط به کشیدن به راست</string>
<string name="pref_flash_page">در هنگام عوض شدن صفحه فلاش سفید بزن</string>
<string name="pref_chapter_swipe_start">حرکت کشیدن به چپ</string>
<string name="pref_chapter_swipe_start">چپتر مربوط به کشیدن به چپ</string>
<string name="updates_last_update_info">آخرین به روز رسانی کتابخانه: %s</string>
<string name="pref_update_only_in_release_period">خارج از دوره انتشار موزد انتظار</string>
<string name="pref_double_tap_zoom">برای بزرگ نمایی دوبار ضربه بزنید</string>
@@ -645,4 +639,295 @@
<string name="last_auto_backup_info">آخرین پشتیبان گیری خودکار: %s</string>
<string name="pref_dns_over_https">DNS بیش از HTTPS (DoH)</string>
<string name="library_sync_complete">همگام سازی کتاب خانه کامل شد</string>
<string name="transition_animation_flip_right">ورق زدن (راست)</string>
<string name="webgpu_viewer">پردازش گرافیکی وب</string>
<string name="ext_confirm_remove">افزونه حذف شود؟</string>
<string name="extensionStoresScreen.deleteStore.body">آیا می‌خواهید فروشگاه افزونه \"%1$s\" (%2$s) را حذف کنید؟</string>
<string name="upcoming_guide">راهنمای پیش‌رو</string>
<string name="crash_screen_title">اوه!</string>
<string name="information_cloudflare_help">برای دریافت کمک در مورد کلادفلر اینجا ضربه بزنید</string>
<string name="library_exported">کتابخانه صادر شد</string>
<string name="information_no_entries_found">هیچ موردی در این دسته‌بندی یافت نشد</string>
<string name="supportUsScreen.currentlySupportedBy">میهون در حال حاضر از حمایت %d+ حامی برخوردار است. می‌خواهیم از تک‌تک شما تشکر کنیم. بیایید با هم میهون را حتی بهتر کنیم.</string>
<string name="trackers_updated_summary">ردیاب‌ها به چپتر %d به‌روزرسانی شدند</string>
<string name="remove_private_extension_message">آیا واقعاً می‌خواهید افزونه «%s» را حذف کنید؟</string>
<string name="tracked_privately">درحال ردیابی خصوصی</string>
<string name="track_remove_start_date_conf_text">این کار تاریخ شروعی را که قبلاً انتخاب کرده‌اید، از %s حذف خواهد کرد</string>
<string name="label_overview_section">نمای کلی</string>
<string name="relative_time_span_never">هرگز</string>
<string name="pref_reset_viewer_flags_error">امکان بازنشانی تنظیمات خواندن وجود نداشت</string>
<string name="transition_animation_cube_inside">مکعب (داخل)</string>
<string name="transition_animation_cube_outside">مکعب (بیرونی)</string>
<string name="pref_vertical_navigator">از مسیریاب عمودی چپتر ها استفاده کنید در</string>
<string name="downloading_with_progress">در حال بارگیری... (%d%%)</string>
<string name="migrationListScreen.unknownLatestChapter">ناشناخته</string>
<string name="extensionStoresScreen.addStore.processing">در حال پردازش…</string>
<string name="migrationConfigScreen.deepSearchModeSubtitle">عنوان را برای جستجوی گسترده‌تر به کلمات کلیدی تجزیه می‌کند</string>
<string name="donationCampaign.paragraph2">می‌توانید از طریق Patreon (که روش پیشنهادی است) یا اوپن‌کالکتیو کمک مالی کنید. اگر سؤالی دارید یا مایلید از روش‌های دیگری حمایت کنید، می‌توانید به‌راحتی از طریق سرور دیسکورد Mihon با من در تماس باشید.</string>
<string name="are_you_sure">مطمئن هستید؟</string>
<string name="pref_firebase">تحلیل‌ها و گزارش‌های خرابی</string>
<string name="action_toggle_private_on">ردیابی خصوصی</string>
<string name="pref_onboarding_guide">راهنمای آشناسازی</string>
<string name="firebase_summary">ارسال گزارش‌های خرابی و داده‌های تحلیلی به ما امکان می‌دهد تا مشکلات را شناسایی و برطرف کنیم، عملکرد را بهبود بخشیم و به‌روزرسانی‌های آینده را متناسب‌تر با نیازهای شما ارائه دهیم</string>
<string name="pref_disallow_non_ascii_filenames_details">این قابلیت سازگاری با برخی رسانه‌های ذخیره‌سازی را که از یونیکد پشتیبانی نمی‌کنند، تضمین می‌کند. با فعال‌سازی این گزینه، باید نام پوشه‌های منبع و مانگا را به‌صورت دستی تغییر دهید و کاراکترهای غیر-ASCII را با معادل‌های هگزادسیمال UTF-8 آن‌ها (به‌صورت حروف کوچک) جایگزین کنید. نیازی به تغییر نام فایل‌های فصل‌ها نیست.</string>
<string name="fdroid_warning">نسخه‌های F-Droid به‌طور رسمی پشتیبانی نمی‌شوند.\nبرای کسب اطلاعات بیشتر، ضربه بزنید.</string>
<string name="updates_last_update_info_just_now">همین الان</string>
<string name="possible_duplicates_title">موارد تکراری احتمالی</string>
<string name="track_remove_finish_date_conf_text">این کار تاریخ پایانِ از پیش انتخاب‌شده‌ی شما را از %s حذف خواهد کرد</string>
<string name="cutout_mode_avoid">اجتناب کردن</string>
<string name="pref_invalidate_download_cache">بازنمایه‌سازی بارگیری‌ها</string>
<string name="minute_short">%dدقیقه</string>
<string name="transition_animation_sphere">کُره</string>
<string name="empty_screen">خب، وضعیت معذب‌کننده‌ای است</string>
<string name="considering">با در نظر گرفتن</string>
<string name="migrationListScreen.migrateDialog.copyLabel">کپی کردن</string>
<string name="pref_flash_style_white">سفید</string>
<string name="appwidget_unavailable_locked">هنگامی که قفل برنامه فعال است، ابزارک در دسترس نیست</string>
<string name="theme_monochrome">تک‌رنگ</string>
<string name="migrationConfigScreen.prioritizeByChaptersTitle">تطبیق بر اساس شماره چپتر</string>
<string name="transition_animation_stack_down">پشته (پایین)</string>
<string name="donationCampaign.dismiss">نادیده‌‎گرفتن</string>
<string name="action_set_interval">تنظیم بازه زمانی</string>
<string name="migrationConfigScreen.prioritizeByChaptersSubtitle">در صورت فعال بودن، عنوانی را انتخاب می‌کند که در دورترین فاصله در پیش‌رو قرار دارد. در غیر این صورت، بر اساس اولویت منبع، اولین مورد منطبق را انتخاب می‌کند.</string>
<string name="label_mean_score">میانگین نمره</string>
<string name="file_picker_uri_permission_unsupported">دسترسی پایدار به پوشه برقرار نشد. ممکن است برنامه رفتار غیرمنتظره‌ای داشته باشد.</string>
<string name="extensionStores">فروشگاه‌های افزونه</string>
<string name="supportUsScreen.contactPlatform">دیسکورد</string>
<string name="manga_display_modified_interval_title">تنظیم‌شده برای به‌روزرسانی در هر</string>
<string name="pref_debug_info">اطلاعات عیب‌یابی</string>
<string name="pref_continuous_gap">شکاف</string>
<string name="pref_reset_viewer_flags">بازنشانی تنظیمات خواندن برای هر سری</string>
<string name="pref_dump_crash_logs_summary">گزارش‌های خطا را برای اشتراک‌گذاری با توسعه‌دهندگان در یک فایل ذخیره می‌کند</string>
<string name="onboarding_storage_help_info">قصد به‌روزرسانی از نسخه‌ای قدیمی‌تر را دارید و نمی‌دانید چه گزینه‌ای را انتخاب کنید؟ برای کسب اطلاعات بیشتر، به راهنمای فضای ذخیره‌سازی مراجعه کنید.</string>
<string name="donationCampaign.paragraph3">به هر حال، پشتیبانی از می‌هون توسط من و داوطلبان ادامه خواهد یافت و این پروژه همچنان متن‌باز باقی خواهد ماند.</string>
<string name="manga_interval_expected_update">پیش‌بینی می‌شود فصل‌های جدید حدود %1$s منتشر شوند؛ بررسی‌ها تقریباً هر %2$s یک‌بار انجام می‌شود.</string>
<string name="action_add_anyway">به هر حال اضافه کن</string>
<string name="upcoming_calendar_next">ماه آینده</string>
<string name="pref_security">امنیت</string>
<string name="clear_database_history_warning">اطلاعات مربوط به چپترهای خوانده‌شده و میزان پیشرفت در موارد خارج از کتابخانه، از دست خواهد رفت</string>
<string name="action_sort_random">تصادفی</string>
<string name="date">تاریخ</string>
<string name="error_sharing_cover">خطا در اشتراک‌گذاری جلد</string>
<string name="upcoming_calendar_prev">ماه قبلی</string>
<string name="pref_behavior">رفتار</string>
<string name="pref_clear_webview_data">پاک‌سازی داده‌ نمای وب</string>
<string name="pref_download_concurrent_pages">بارگیری همزمان صفحات</string>
<string name="migrationListScreen.latestChapterLabel">جدیدترین‌ها: %1$s</string>
<string name="transition_animation_basic">ساده</string>
<string name="pref_incognito_mode_extension_summary">توقف ثبت تاریخچه مطالعه برای افزونه</string>
<string name="pref_hide_missing_chapter_indicators">پنهان‌سازی نشانگرهای چپترهای مفقود</string>
<string name="learn_more">برای کسب اطلاعات بیشتر ضربه بزنید</string>
<string name="onboarding_permission_crashlytics_description">گزارش‌های خرابیِ ناشناس‌سازی‌شده را برای توسعه‌دهندگان ارسال کنید.</string>
<string name="intervals_header">فواصل</string>
<string name="day_short">%dروز</string>
<string name="download_bookmarked">نشان‌گذاری شد</string>
<string name="action_view_upcoming">مشاهده به‌روزرسانی‌های پیش‌رو</string>
<string name="error_saving_cover">خطا در ذخیره‌سازی جلد</string>
<string name="not_installed">نصب‌نشده</string>
<string name="migrationConfigScreen.selectNoneLabel">انتخاب هیچ‌‎کدام</string>
<string name="appwidget_updates_description">موارد به‌تازگی به‌روزرسانی‌شده در کتابخانه خود را مشاهده کنید</string>
<string name="complete_list">فهرست کامل‌شده</string>
<string name="pref_continuous_minwidth">حداقل عرض</string>
<string name="error_user_agent_string_blank">رشته عامل کاربر نمی‌تواند خالی باشد</string>
<string name="pref_webtoon_disable_zoom_out">غیرفعال‌سازی کوچک‌نمایی</string>
<string name="transition_animation_flip_left">ورق زدن (چپ)</string>
<string name="pref_download_concurrent_pages_summary">تعداد صفحات بارگیری‌شده همزمان از هر منبع</string>
<string name="migrationConfigScreen.additionalSearchQueryLabel">کلمات کلیدی اضافی (اختیاری)</string>
<string name="migrationListScreen.searchManuallyActionLabel">جستجوی دستی</string>
<string name="export">صادرات</string>
<string name="cutout_mode_ignore">نادیده‌گرفتن</string>
<string name="label_support_us">از ما حمایت کنید</string>
<string name="notification_incognito_text">غیرفعال کردن حالت ناشناس</string>
<string name="clear_db_exclude_read">عناوین دارای چپترهای خوانده‌شده را نگه دارید</string>
<string name="track_error">%1$s خطا: %2$s</string>
<string name="manga_interval_expected_update_soon">به‌زودی</string>
<string name="migrationConfigScreen.selectEnabledLabel">انتخاب منابع فعال</string>
<string name="transition_animation_stack_right">پشته (راست)</string>
<string name="local_filter_order_by">مرتب‌سازی بر اساس</string>
<string name="label_upcoming">آینده</string>
<string name="invalid_backup_file_json">پشتیبان‌گیری JSON پشتیبانی نمی‌شود</string>
<string name="download_notifier_cache_renewal">بررسی بارگیری‌ها</string>
<string name="logging_in">در حال ورود…</string>
<string name="transition_animation_fade">محو شدن</string>
<string name="transition_animation_none">هیچ‌کدام</string>
<string name="database_clean">چیزی برای پاک‌سازی وجود ندارد</string>
<string name="extensionStoresScreen.addStoreInput.inputLabel">ذخیره نشانی اینترنتی فهرست</string>
<string name="label_read_chapters">خواندن</string>
<string name="source_filter_empty_screen">هیچ منبع نصب‌شده‌ای یافت نشد</string>
<string name="invalid_backup_file_unknown">فایل پشتیبان آسیب‌دیده است</string>
<string name="overlay_header">پوشش</string>
<string name="dual_page_view_never">هرگز</string>
<string name="storage_failed_to_create_download_directory">ایجاد پوشه بارگیری با شکست مواجه شد</string>
<string name="update_already_running">یک به‌روزرسانی از قبل در حال اجراست</string>
<string name="pref_flash_style_black">مشکی</string>
<string name="transition_animation_stack_up">پشته (بالا)</string>
<string name="action_copy_link">رونوشت پیوند</string>
<string name="cover_saved">جلد ذخیره شد</string>
<string name="pref_dump_crash_logs">اشتراک‌گذاری گزارش‌های خرابی</string>
<string name="label_total_chapters">مجموع</string>
<string name="pref_reset_viewer_flags_summary">حالت مطالعه و جهت‌گیری تمام سری‌ها را بازنشانی می‌کند</string>
<string name="manga_interval_custom_amount">بسامد به‌روزرسانی سفارشی:</string>
<string name="copied_to_clipboard_plain">در بریده‌دان رونوشت شد</string>
<string name="webview_data_deleted">داده‌های نمای وب پاک شد</string>
<string name="migrationListScreen.noMatchFoundText">هیچ جایگزینی یافت نشد</string>
<string name="pref_display_profile">نمایه نمایش سفارشی</string>
<string name="chapter_not_found">چپتر یافت نشد</string>
<string name="source_unsupported">منبع پشتیبانی نمی‌شود</string>
<string name="migrationConfigScreen.removeDownloadsTitle">حذف فایل‌های دانلودی مورد جاری پس از انتقال</string>
<string name="pref_flash_style_white_black">سفید و سیاه</string>
<string name="migrationListScreenTitleWithProgress">مهاجرت (%1$d/%2$d)</string>
<string name="clear_database_source_item_count">%1$d ورودی غیرکتابخانه‌ای در پایگاه داده</string>
<string name="source_empty_screen">هیچ منبعی یافت نشد</string>
<string name="action_webview_close_tab">بستن زبانه</string>
<string name="pref_user_agent_string">رشته عامل کاربر پیش‌فرض</string>
<string name="supportUsScreen.perks">حامیان به کانال‌های اختصاصی دسترسی پیدا می‌کنند و مزایای بیشتری نیز در راه است.</string>
<string name="no_scanlators_found">ترجمه غیر رسمی یافت نشد</string>
<string name="pref_filter_upcoming_categories_details">عنوان‌های موجود در دسته‌بندی‌های مستثنی‌شده نمایش داده نمی‌شوند، حتی اگر در هر یک از دسته‌بندی‌های مشمول نیز قرار داشته باشند.</string>
<string name="scanlator">گروه ترجمه غیررسمی</string>
<string name="pref_flash_duration_summary">%1$s میلی‌ثانیه</string>
<string name="onboarding_permission_analytics">اجازه دادن به تحلیل‌ها</string>
<string name="label_used">استفاده شده</string>
<string name="unknown_title">عنوان نامشخص</string>
<string name="help_translate">کمک به ترجمه</string>
<string name="migrationConfigScreen.selectPinnedLabel">انتخاب منابع سنجاق‌شده</string>
<string name="dual_page_view_wide">وقتی عریض است</string>
<string name="migrationConfigScreen.additionalSearchQuerySupportingText">با افزودن کلمات کلیدی بیشتر، به محدود کردن نتایج جستجو کمک می‌کند</string>
<string name="reading_list">فهرست خواندن</string>
<string name="pref_transition_animation_dual">انیمیشن انتقال (دوگانه)</string>
<string name="transition_animation_flip">چرخش زبانه</string>
<string name="not_applicable">ناموجود</string>
<string name="exception_offline">اتصال اینترنت وجود ندارد</string>
<string name="label_completed_titles">عناوین تکمیل شده</string>
<string name="migrationConfigScreen.hideWithoutUpdatesTitle">پنهان‌سازی موارد فاقد چپترهای جدیدتر</string>
<string name="pref_auto_update_manga_on_mark_read">به‌روزرسانی پیشرفت هنگام علامت‌گذاری به عنوان خوانده‌شده</string>
<string name="dual_page_view_always">همیشه</string>
<string name="library_list">فهرست کتابخانه</string>
<string name="on_hold_list">فهرست موارد معلق</string>
<string name="extensionStoresScreen.addStoreDeeplink.bodyText">آیا می‌خواهید فروشگاه افزونه‌های زیر را اضافه کنید؟</string>
<string name="pref_reset_user_agent_string">بازنشانی رشته عامل کاربر پیش‌فرض</string>
<string name="pref_download_concurrent_sources">دانلود هم‌زمان از منابع</string>
<string name="action.addExtensionStore">افزودن فروشگاه افزونه‌ها</string>
<string name="pref_flash_page_interval">فلاش هرکدام</string>
<string name="action_filter_excluded_scanlators">فیلتر کردن ترجمه‌های غیر رسمی حذف‌شده</string>
<string name="notification_updating_progress">در حال به‌روزرسانی کتابخانه… (%s)</string>
<string name="label_titles_section">عناوین</string>
<string name="download_cache_invalidated">بازسازی فهرست بارگیری</string>
<string name="migrationConfigScreen.dataToMigrateHeader">داده‌های قابل انتقال</string>
<string name="possible_duplicates_summary">موارد مشابهی با این نام در کتابخانه شما وجود دارد.\n\nیک مورد را برای انتقال انتخاب کنید یا آن را به هر حال اضافه نمایید.</string>
<string name="custom_cover">جلد سفارشی</string>
<string name="supportUsScreen.donationPlatform.opencollective">اوپن کالکتیو</string>
<string name="unfinished_list">فهرست ناتمام</string>
<string name="supportUsScreen.donationPlatform.patreon">پتریون (ترجیحی)</string>
<string name="label_read_duration">مدت زمان مطالعه</string>
<string name="pref_invalidate_download_cache_summary">اجبار برنامه به بررسی مجدد چپترهای دانلودشده</string>
<string name="pref_mark_duplicate_read_chapter_read_existing">پس از خواندن یک چپتر</string>
<string name="remove_manga">شما در حال حذف «%s» از کتابخانه خود هستید</string>
<string name="pref_verbose_logging_summary">چاپ گزارش‌های مشروح در گزارش سیستم (عملکرد برنامه را کاهش می‌دهد)</string>
<string name="artist">هنرمند</string>
<string name="notification_size_warning">به‌روزرسانی‌های حجیم به منابع سیستم آسیب می‌رسانند و ممکن است منجر به کندی روند به‌روزرسانی و افزایش مصرف باتری شوند. برای کسب اطلاعات بیشتر، ضربه بزنید.</string>
<string name="pref_webtoon_vertical_navigator_on_left">ناوبر عمودی را در سمت چپ قرار دهید</string>
<string name="library_errors_help">برای راهنمایی در مورد نحوه رفع خطاهای به‌روزرسانی کتابخانه، به %1$s مراجعه کنید</string>
<string name="extensionStoresScreen.deleteStore.title">حذف فروشگاه افزونه‌ها</string>
<string name="skipped_reason_not_in_release_period">به دلیل عدم انتظار انتشار در امروز، نادیده گرفته شد</string>
<string name="file_null_uri_error">هیچ فایلی انتخاب نشده</string>
<string name="pref_download_new_unread_chapters_only">صرف‌نظر از بارگیری چپترهای خوانده‌شده‌ تکراری</string>
<string name="description_placeholder">بدون توضیحات</string>
<string name="privacy_policy">سیاست حفظ حریم خصوصی</string>
<string name="onboarding_permission_crashlytics">ارسال گزارش‌های خرابی</string>
<string name="track_started_reading_date">تاریخ شروع</string>
<string name="error_user_agent_string_invalid">رشتهٔ عامل کاربر نامعتبر است</string>
<string name="migrationListScreen.migrateNowActionLabel">مهاجرت همین حالا</string>
<string name="track_delete_title">آیا ردیابی %s حذف شود؟</string>
<string name="migrationListScreen.migrateActionLabel">مهاجرت کردن</string>
<string name="migrationListScreen.matchWithoutChapterToast">هیچ چپتری یافت نشد؛ از این عنوان نمی‌توان برای انتقال استفاده کرد</string>
<string name="pref_tablet_ui_mode">رابط کاربری تبلت</string>
<string name="pref_disallow_non_ascii_filenames">عدم مجاز دانستن نام‌فایل‌های غیر ASCII</string>
<string name="pref_mark_duplicate_read_chapter_read_new">پس از دریافت چپتر جدید</string>
<string name="action_toggle_private_off">ردیابی عمومی</string>
<string name="pref_update_library_manga_titles_summary">هشدار: اگر نام یک مانگا تغییر کند، آن مانگا از صف بارگیر (در صورت وجود) حذف خواهد شد.</string>
<string name="pref_reset_viewer_flags_success">تمام تنظیمات خواندن بازنشانی شدند</string>
<string name="migrationListScreen.migrateDialog.cancelLabel">لغو</string>
<string name="theme_tokyonight">شبِ توکیو</string>
<string name="pref_verbose_logging">گزارش‌گیری طولانی</string>
<string name="hour_short">%dساعت</string>
<string name="migrationConfigScreen.availableHeader">در‌دسترس</string>
<string name="extensionStoresScreen.addStore.alreadyExists">این فروشگاه افزونه از قبل وجود دارد</string>
<string name="about_dont_kill_my_app">برخی از سازندگان محدودیت‌های اضافی برای برنامه‌ها اعمال می‌کنند که باعث متوقف شدن سرویس‌های پس‌زمینه می‌شود. این وب‌سایت اطلاعات بیشتری درباره نحوه رفع این مشکل ارائه می‌دهد.</string>
<string name="track_delete_remote_text">همچنین از %s حذف کنید</string>
<string name="action_display_unread_badge">چپتر‌های خوانده‌نشده</string>
<string name="notes_placeholder">از آن بخشی که خوشم آمد…</string>
<string name="migrationListScreen.copyNowActionLabel">همین حالا کپی کنید</string>
<string name="label_titles_in_global_update">در به‌روزرسانی جهانی</string>
<string name="migrationListScreen.exitDialog.stopLabel">توقف</string>
<string name="transition_animation_fade_white">محو شدن به سفیدی</string>
<string name="has_results">دارای نتایج است</string>
<string name="information_required_plain">*الزامی</string>
<string name="migrationConfigScreen.hideUnmatchedTitle">پنهان کردن موارد بدون مطابقت</string>
<string name="pref_high_quality_renderer">استفاده از رندرکننده باکیفیت</string>
<string name="manga_display_interval_title">تخمینِ تک‌تکِ موارد</string>
<string name="clear_database_text">شما در شرف حذف ورودی‌ها از پایگاه داده هستید</string>
<string name="pref_dual_page_view">نمای دو صفحه‌ای</string>
<string name="cant_open_last_read_chapter">امکان باز کردن آخرین چپتر مطالعه‌شده وجود ندارد</string>
<string name="information_no_manga_category">این دسته‌بندی خالی است</string>
<string name="manga_cover">جلد</string>
<string name="migrationConfigScreen.hideWithoutUpdatesSubtitle">تنها در صورتی که عنوان دارای چپترهای اضافی باشد، آن را نمایش بده</string>
<string name="track_delete_text">این کار ردیابی را به‌صورت محلی حذف می‌کند.</string>
<string name="migrationListScreen.migrateDialog.migrateLabel">مهاجرت</string>
<string name="migrationListScreen.skipActionLabel">مهاجرت نکنید</string>
<string name="track_finished_reading_date">تاریخ پایان</string>
<string name="theme_catppuccin">کت‌پاچین</string>
<string name="pref_transition_animation">انیمیشن انتقال</string>
<string name="storage_failed_to_create_directory">ایجاد دایرکتوری ناموفق بود: %s</string>
<string name="pref_vertical_navigator_height">ارتفاع ناوبری عمودی</string>
<string name="pref_display_images_description">نمایش تصاویر در توضیحات مانگا</string>
<string name="pref_filter_update_categories_details">به‌روزرسانی‌های مربوط به دسته‌های مستثنی‌شده نمایش داده نخواهند شد، حتی اگر آن موارد در دسته‌های گنجانده‌شده نیز قرار داشته باشند.</string>
<string name="label_auto">خودکار</string>
<string name="migrationConfigScreen.deepSearchModeTitle">حالت جستجوی پیشرفته</string>
<string name="ext_remove">حذف</string>
<string name="migrationConfigScreen.selectAllLabel">انتخاب همه</string>
<string name="seconds_short">%dثانیه</string>
<string name="migrationListScreen.exitDialog.cancelLabel">لغو</string>
<string name="migrationListScreen.exitDialogTitle">توقف مهاجرت؟</string>
<string name="extensionStoresScreen.addStore.title">افزودن فروشگاه افزونه‌ها</string>
<string name="onboarding_storage_info">پوشه‌ای را انتخاب کنید که %1$s فایل‌های دانلودشده‌ی چپترها، نسخه‌های پشتیبان و موارد دیگر را در آن ذخیره کند.\n\nاستفاده از یک پوشه‌ی اختصاصی توصیه می‌شود.\n\nپوشه‌ی انتخاب‌شده: %2$s</string>
<string name="onboarding_permission_analytics_description">برای بهبود قابلیت‌های برنامه، داده‌های استفاده را به‌صورت ناشناس ارسال کنید.</string>
<string name="pref_flash_duration">مدت‌زمان فلاش</string>
<string name="download_notifier_split_page_not_found">برگه %d هنگام تفکیک یافت نشد</string>
<string name="author">نویسنده</string>
<string name="donationCampaign.title">از برنامه لذت می‌برید؟</string>
<string name="action_notes">یادداشت‌ها</string>
<string name="wish_list">فهرست علاقه‌مندی‌</string>
<string name="pref_cutout_mode">حالت بریدگی نمایشگر</string>
<string name="pref_flash_with">فلاش با</string>
<string name="transition_animation_stack_left">پشته (چپ)</string>
<string name="exception_http">HTTP %d، وب‌سایت را در نمای‌وب بررسی کنید</string>
<string name="crash_screen_description">%s با خطای غیرمنتظره‌ای مواجه شد. پیشنهاد می‌کنیم گزارش‌های خرابی را در کانال پشتیبانی ما در دیسکورد به اشتراک بگذارید.</string>
<string name="label_tracker_section">ردیاب‌ها</string>
<string name="non_library_settings">همهٔ موارد خوانده‌شده</string>
<string name="donationCampaign.paragraph1">سلام! من توسعه‌دهنده اصلی «می‌هون» هستم و می‌خواهم از شما درخواست کنم که حمایت مالی از توسعه آن را مد نظر قرار دهید. با حمایت کافی، می‌توانم به‌صورت تمام‌وقت روی «می‌هون» کار کنم و بر رفع مشکلات قدیمی و همچنین پیاده‌سازی قابلیت‌هایی که بسیار مورد تقاضا بوده‌اند، تمرکز کنم.</string>
<string name="confirm_tracker_update">به‌روزرسانی ردیاب‌ها به چپتر %d؟</string>
<string name="pref_mark_duplicate_read_chapter_read">علامت‌گذاری چپتر تکراری خوانده شده به عنوان خوانده شده</string>
<string name="migrationConfigScreen.selectedHeader">انتخاب‌شده</string>
<string name="pref_cutout_mode_dual">حالت بریدگی نمایشگر (دوگانه)</string>
<string name="exception_unknown_host">امکان ارتباط با %s وجود نداشت</string>
<string name="clipboard_copy_error">رونوشت بریده‌دان شکست خورد</string>
<string name="action_edit_notes">ویرایش یادداشت‌ها</string>
<string name="label_tracked_titles">عناوین ردیابی‌شده</string>
<string name="migrationListScreen.copyActionLabel">کپی‌کردن</string>
<string name="cutout_mode_shift">تغییر</string>
<string name="donationCampaign.contactPlatform">دیسکورد</string>
<string name="exclude_scanlators">استثنا کردن ترجمه غیر رسمی</string>
<string name="migrationConfigScreen.continueButtonText">ادامه</string>
<string name="error_saving_picture">خطا در ذخیره تصویر</string>
<string name="migrationListScreenTitle">مهاجرت</string>
<string name="track_remove_date_conf_title">تاریخ حذف شود؟</string>
<string name="download_notifier_split_page_path_not_found">مسیر فایل برگه %d پیدا نشد</string>
<string name="pref_update_library_manga_titles">به‌روزرسانی عناوین مانگای کتابخانه برای مطابقت با منبع</string>
<string name="extensionStoresScreen.emptyLabel">هنوز فروشگاه افزونه‌ای اضافه نکرده‌اید</string>
<string name="crash_screen_restart_application">راه‌اندازی مجدد برنامه</string>
<string name="manga_interval_expected_update_null">این مانگا یا به پایان رسیده است و یا تاریخ انتشار مشخصی برای آن پیش‌بینی نشده است.</string>
<string name="migrationConfigScreen.enhancedOptionsWarning">این گزینه‌ها کند و خطرناک هستند و ممکن است منجر به اعمال محدودیت‌هایی از سوی منابع شوند</string>
<string name="migrationListScreen.progressDialog.cancelLabel">لغو</string>
<string name="supportUsScreen.contactForDetailsMessage">برای گزینه‌های جایگزین یا هرگونه سؤال مرتبط، به سرور دیسکورد ما بپیوندید.</string>
<string name="theme_nord">نورد</string>
</resources>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">Seurantapalvelimet joihin et ole kirjautunut sisään:</string>
<string name="pref_remove_bookmarked_chapters">Poista kirjanmerkityt luvut</string>
<string name="pref_category_delete_chapters">Poista luvut</string>
<string name="ext_nsfw_warning">Tämän laajennoksen lähteet voivat sisältää NSFW (18+) -sisältöä</string>
<string name="ext_nsfw_short">18+</string>
<string name="parental_controls_info">Tämä ei estä epävirallisia tai mahdollisesti väärin merkittyjä laajennuksia näyttämästä NSFW 18+ sisältöä sovelluksessa.</string>
<string name="no_chapters_error">Lukuja ei löytynyt</string>
<string name="chapter_settings_updated">Päivitetyt luvun oletusasetukset</string>
<string name="set_chapter_settings_as_default">Aseta oletukseksi</string>
@@ -421,8 +418,6 @@
<string name="spen_next_page">Seuraava sivu</string>
<string name="spen_previous_page">Edellinen sivu</string>
<string name="migration_help_guide">Lähteen siirto-opas</string>
<string name="pref_category_nsfw_content">NSFW (18+) lähteet</string>
<string name="pref_show_nsfw_source">Näytä lähde- ja lisäosaluettelossa</string>
<string name="file_picker_error">Tiedostonvalitsinsovellusta ei löytynyt</string>
<string name="myanimelist_relogin">Kirjaudu uudelleen MAL: iin</string>
<string name="pref_viewer_nav">Navigointiasettelu</string>
@@ -584,7 +579,6 @@
<string name="manga_cover">Kansikuva</string>
<string name="error_sharing_cover">Virhe jakaessa kansikuvaa</string>
<string name="learn_more">Napauta saadaksesi lisätietoja</string>
<string name="ext_info_age_rating">Ikäluokitus</string>
<string name="tracking_guide">Seurantaopas</string>
<string name="pref_reset_viewer_flags_success">Kaikki lukijan asetukset poistettu</string>
<string name="pref_verbose_logging">Yksityiskohtainen kirjaaminen</string>
@@ -113,7 +113,7 @@
<string name="app_not_available">Hindi magagamit ang app</string>
<string name="second_to_last">Pangalawa sa huling kabanata</string>
<string name="last_read_chapter">Huling nabasang kabanata</string>
<string name="disabled">Sarado</string>
<string name="disabled">Di-pinagana</string>
<string name="pref_remove_after_marked_as_read">Pagkatapos na manu-manong namarka bilang nabasa na</string>
<string name="pref_remove_after_read">Pagkatapos basahin, awtomatikong burahin</string>
<string name="pref_webtoon_side_padding">Kapal ng gilid</string>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">Di naka-login sa mga tracker:</string>
<string name="pref_remove_bookmarked_chapters">Payagan ang pagtanggal ng mga naka-bookmark na kabanata</string>
<string name="pref_category_delete_chapters">Magbura ng kabanata</string>
<string name="parental_controls_info">Hindi nito naiiwasan ang pagpapakita ng mga NSFW (18+) sa mga di-opisyal o posibleng maling naka-flag na extension sa loob ng app.</string>
<string name="ext_nsfw_warning">Posibleng may NSFW (18+) content ang mga source mula sa extension na ito</string>
<string name="ext_nsfw_short">18+</string>
<string name="no_chapters_error">Walang nakitang kabanata</string>
<string name="confirm_set_chapter_settings">Gusto mo bang i-save at ipagpaubaya ang pagsasaayos na ito?</string>
<string name="set_chapter_settings_as_default">I-set bilang default</string>
@@ -422,8 +419,6 @@
<string name="spen_next_page">Susunod na pahina</string>
<string name="spen_previous_page">Balik</string>
<string name="file_picker_error">Walang nakitang file picker app</string>
<string name="pref_show_nsfw_source">Ipakita sa mga listahan ng source at extension</string>
<string name="pref_category_nsfw_content">Mga source na NSFW (18+)</string>
<string name="myanimelist_relogin">Mag-login muli sa MAL</string>
<string name="track_finished_reading_date">Petsa ng pagtatapos</string>
<string name="track_started_reading_date">Petsa ng pagsisimula</string>
@@ -580,7 +575,6 @@
<string name="pref_reset_viewer_flags_summary">Rine-reset ang paraan ng pagbasa at orientation para sa lahat ng mga serye</string>
<string name="ext_info_version">Bersyon</string>
<string name="ext_info_language">Wika</string>
<string name="ext_info_age_rating">Rating sa edad</string>
<string name="on_hold_list">Listahan ng Tinigil</string>
<string name="complete_list">Listahan ng Kumpleto</string>
<string name="unfinished_list">Listahan ng Di pa Tapos</string>
@@ -907,4 +901,53 @@
<string name="pref_filter_update_categories_details">Hindi ipapakita ang mga update sa mga nakabukod na kategorya, kahit kabilang pa ang mga ito sa mga napiling kategorya.</string>
<string name="pref_filter_upcoming_categories_details">Hindi ipapakita ang mga update sa mga nakabukod na kategorya, kahit kabilang pa ang mga ito sa mga napiling kategorya.</string>
<string name="theme_tokyonight">Tokyo sa Gabi</string>
<string name="pref_category_extensions">Mga extension</string>
<string name="pref_allowed_content_warnings">Pinayagan ang mga babala sa nilalaman</string>
<string name="pref_apply_content_warnings_to_installed">Ilapat rin ito sa mga na-install na extension</string>
<string name="pref_apply_content_warnings_to_installed_summary">Kapag naka-off, ang mga extension na naka-install ay magpatuloy mag-load at mag-update kahit ano pa ang kanilang babala</string>
<string name="content_warnings_info">Ang mga naka-install na extension na hindi na-load ay mananatiling nakalista nang walang mga source; ang mga hindi mo pa na-install ay nakatago. Hindi nito pinipigilan ang mga hindi opisyal o posibleng maling na-flag na extension na lumabas ang 18+ na nilalaman na nasa loob ng app.</string>
<string name="ext_filtered">Nasala</string>
<string name="ext_not_loaded">Hindi nai-load</string>
<string name="ext_not_loaded_dialog">Hindi ma-load</string>
<string name="ext_copy_stacktrace">Kopyahin ang stack trace</string>
<string name="ext_filtered_message">Ang babala sa nilalaman ng extension na ito ay hindi ang pinili mong i-load, kaya hindi available ang mga source nito.</string>
<string name="ext_unsigned_message">Hindi nilagdaan ang extension na ito, kaya walang lagda para mapatunayan ito.</string>
<string name="ext_unsupported_message">Ang extension na ito ay ginawa para sa isang bersyon ng extension library na hindi ma-load ng bersyon ng app na ito. Maaaring makatulong ang pag-update ng app o extension.</string>
<string name="ext_malformed_message">Kulang ng impormasyon ang extension na ito na kailangan ng app para ma-load ito.</string>
<string name="ext_load_failed_message">Nagdulot ng error ang extension na ito habang naglo-load, kaya wala sa mga sources nito ang available.</string>
<string name="ext_info_warning">Babala</string>
<string name="ext_content_warning_safe">Ligtas</string>
<string name="ext_content_warning_mixed">Halo</string>
<string name="ext_content_warning_nsfw">18+</string>
<string name="ext_content_warning_mixed_description">Ang mga source mula sa extension na ito ay maaaring maglaman ng parehong ligtas at 18+ na nilalaman.</string>
<string name="ext_content_warning_nsfw_description">Ang mga source mula sa extension na ito ay pangunahing nakatuon sa nilalaman para sa mga nasa 18+ taong gulang.</string>
<string name="webgpu_viewer">WebGPU</string>
<string name="transition_animation_fade">Kupas</string>
<string name="transition_animation_flip_right">Pabuklat ng pahina (kanan)</string>
<string name="transition_animation_cube_inside">Kubo (Loob)</string>
<string name="transition_animation_cube_outside">Kubo (Labas)</string>
<string name="cutout_mode_avoid">Iwasan</string>
<string name="transition_animation_sphere">Ispera</string>
<string name="transition_animation_stack_down">Pa-stack (pababa)</string>
<string name="pref_continuous_gap">Puwang</string>
<string name="transition_animation_basic">Pinasimple</string>
<string name="pref_continuous_minwidth">Pinakamaliit na lapad</string>
<string name="transition_animation_flip_left">Pabuklat ng pahina (kaliwa)</string>
<string name="cutout_mode_ignore">Di pansinin</string>
<string name="transition_animation_stack_right">Pa-stack (kanan)</string>
<string name="transition_animation_none">Wala</string>
<string name="dual_page_view_never">Hindi kailanman</string>
<string name="transition_animation_stack_up">Pa-stack (pataas)</string>
<string name="dual_page_view_wide">Kapag malapad</string>
<string name="pref_transition_animation_dual">Animasyon ng transisyon (pa-doble)</string>
<string name="transition_animation_flip">Pagbuklat ng pahina</string>
<string name="dual_page_view_always">Palagi</string>
<string name="transition_animation_fade_white">I-fade patungong puti</string>
<string name="pref_high_quality_renderer">Gamitin ang mataas na kalidad na renderer</string>
<string name="pref_dual_page_view">View ng dalawang pahina</string>
<string name="pref_transition_animation">Animasyon ng transisyon</string>
<string name="pref_cutout_mode">Modo ng display cutout</string>
<string name="transition_animation_stack_left">Pa-stack (kaliwa)</string>
<string name="pref_cutout_mode_dual">Modo ng display cutout (pa-doble)</string>
<string name="cutout_mode_shift">Pag-usog</string>
</resources>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">Traceurs non connectés :</string>
<string name="pref_remove_bookmarked_chapters">Permettre la suppression des chapitres marqués d\'un marque-page</string>
<string name="pref_category_delete_chapters">Suppression des chapitres</string>
<string name="ext_nsfw_warning">Les sources de cette extension peuvent contenir du contenu NSFW (18+)</string>
<string name="ext_nsfw_short">18+</string>
<string name="parental_controls_info">Ceci n\'empêche pas les extensions de diffuser du contenu +18 dans l\'application.</string>
<string name="no_chapters_error">Aucun chapitre trouvé</string>
<string name="confirm_set_chapter_settings">Appliquer ce paramétrage par défaut ?</string>
<string name="chapter_settings">Paramètres du chapitre</string>
@@ -421,10 +418,8 @@
<string name="spen_next_page">Page suivante</string>
<string name="spen_previous_page">Page précédente</string>
<string name="migration_help_guide">Guide de migration des sources</string>
<string name="pref_category_nsfw_content">Contenu +18</string>
<string name="myanimelist_relogin">Veuillez vous reconnecter à MAL</string>
<string name="file_picker_error">Lapplication de sélection de fichier est introuvable</string>
<string name="pref_show_nsfw_source">Afficher dans les listes de sources et d\'extensions</string>
<string name="track_finished_reading_date">Date de fin</string>
<string name="track_started_reading_date">Date de début</string>
<string name="kindlish_nav">Style Kindle</string>
@@ -580,7 +575,6 @@
<string name="empty_screen">Eh bien, c\'est gênant</string>
<string name="ext_info_version">Version</string>
<string name="ext_info_language">Langue</string>
<string name="ext_info_age_rating">Classification par âge</string>
<string name="reading_list">Liste de lecture</string>
<string name="wish_list">Liste de souhaits</string>
<string name="on_hold_list">Liste d\'attente</string>
@@ -75,8 +75,6 @@
<string name="pref_page_transitions">Animar transición de páxinas</string>
<string name="pref_cutout_short">Mostrar contido na área recortada</string>
<string name="pref_fullscreen">Pantalla completa</string>
<string name="ext_nsfw_warning">As fontes desta extensión poden ter contido NSFW (+18)</string>
<string name="ext_nsfw_short">+18</string>
<string name="obsolete_extension_message">Esta extensión xa non está dispoñible. Pode que non funcione ben e cause problemas na aplicación. Recoméndase desinstalala.</string>
<string name="untrusted_extension_message">Unha extensión maliciosa podería ler calquera credencial de inicio de sesión gardada en Mihon ou executar calquera tipo de código. \n \nSe confías nesta extensión, aceptas estes riscos.</string>
<string name="untrusted_extension">Extensión non confiable</string>
@@ -111,7 +109,6 @@
<string name="portrait">Vertical</string>
<string name="pref_library_columns">Tamaño da reixa</string>
<string name="pref_category_display">Amosar</string>
<string name="parental_controls_info">Isto non evita que as extensións non oficiais ou mal clasificadas mostren contido NSFW (+18) dentro da aplicación.</string>
<string name="hide_notification_content">Esconder o contido das notificacións</string>
<string name="secure_screen_summary">O modo discreto esconde o contido da aplicación ao cambiar entre aplicacións e bloquea as capturas de pantalla</string>
<string name="secure_screen">Modo discreto</string>
@@ -122,8 +119,6 @@
<string name="file_picker_error">Non se atopou unha aplicación para seleccionar ficheiros</string>
<string name="myanimelist_relogin">Inicia sesión en MAL de novo</string>
<string name="requires_app_restart">Require reiniciar a aplicación para que surxa efecto</string>
<string name="pref_show_nsfw_source">Amosar na lista de fontes e extensións</string>
<string name="pref_category_nsfw_content">Fontes NSFW (+18)</string>
<string name="pref_category_security">Seguridade e privacidade</string>
<string name="pref_manage_notifications">Xestionar notificacións</string>
<string name="pref_date_format">Formato da data</string>
@@ -353,7 +348,6 @@
<string name="password">Contrasinal</string>
<string name="fdroid_warning">As versións de F-Droid non teñen soporte oficial. \nPreme para saber máis.</string>
<string name="login_success">Conectado</string>
<string name="ext_info_age_rating">Clasificación por idades</string>
<string name="enhanced_services">Servizos de seguimento mellorados</string>
<string name="enhanced_tracking_info">Estes servizos proporcionan funcións melloradas para fontes concretas. Faise un seguimento automático dos elementos ao engadilos á biblioteca.</string>
<string name="backup_in_progress">Xa se está facendo unha copia de seguridade</string>
@@ -328,9 +328,6 @@
<string name="pref_category_reader">מצב קריאה</string>
<string name="pref_category_tracking">מעקב</string>
<string name="pref_category_theme">ערכת נושא</string>
<string name="pref_category_nsfw_content">מקורות NSFW (18+)</string>
<string name="pref_show_nsfw_source">הצג ברשימות מקורות ותוספים</string>
<string name="parental_controls_info">זה לא מונע תוספים לא רשמיים או תוספים שעלולים לסמן באופן שגוי מלהציג תוכן NSFW (18+) בתוך האפליקציה.</string>
<string name="relative_time_today">היום</string>
<string name="pref_category_display">תצוגה</string>
<string name="action_enable_all">הפעלת הכול</string>
@@ -362,14 +359,12 @@
<string name="pref_update_only_started">זה עוד לא התחיל</string>
<string name="ext_update_all">עדכן הכל</string>
<string name="ext_trust">האמן</string>
<string name="ext_nsfw_warning">עלול להכיל תוכן שלא מתאים למקום העבודה (18+)</string>
<string name="ext_install_service_notif">מתקין הרחבה…</string>
<string name="include">כלול: %s</string>
<string name="pref_show_navigation_mode_summary">הראה לזמן קצר כשפותחים את מצב הקריאה</string>
<string name="pref_show_reading_mode">הראה מצב קריאה</string>
<string name="ext_app_info">פרטי האפליקצייה</string>
<string name="extension_api_error">נכשל בקבלת רשימת ההרחבות</string>
<string name="ext_nsfw_short">18+</string>
<string name="none">אין</string>
<string name="update_72hour">כל 3 ימים</string>
<string name="connected_to_wifi">רק ב Wi-Fi</string>
@@ -535,7 +530,6 @@
<string name="kindlish_nav">כמו ספר אלקטרוני</string>
<string name="ext_info_version">גרסה</string>
<string name="filter_mode_overlay">כיסוי</string>
<string name="ext_info_age_rating">הגבלת גיל</string>
<string name="ext_info_language">שפה</string>
<string name="pref_color_filter_mode">פילטר צבע משתלב</string>
<string name="filter_mode_multiply">להכפיל</string>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">ट्रैकर्स में लॉग इन नहीं किया गया:</string>
<string name="pref_remove_bookmarked_chapters">बुकमार्क किए गए अध्यायों को हटाने की अनुमति दें</string>
<string name="pref_category_delete_chapters">अध्यायों को हटाएं</string>
<string name="ext_nsfw_warning">इसमें NSFW (18+) सामग्री हो सकती है</string>
<string name="ext_nsfw_short">18+</string>
<string name="parental_controls_info">यह अनौपचारिक या संभावित रूप से फ़्लैग किए गए एक्सटेंशन को ऐप के भीतर NSFW (18+) सामग्री के सामने आने से नहीं रोकता है।</string>
<string name="chapter_settings_updated">अपडेट किए गए डिफ़ॉल्ट अध्याय सेटिंग्स</string>
<string name="no_chapters_error">कोई अध्याय नहीं मिला</string>
<string name="set_chapter_settings_as_default">डिफ़ॉल्ट के रूप में सेट करें</string>
@@ -421,8 +418,6 @@
<string name="spen_next_page">अगला पृष्ठ</string>
<string name="spen_previous_page">पिछला पृष्ठ</string>
<string name="migration_help_guide">स्रोत माइग्रेशन गाइड</string>
<string name="pref_show_nsfw_source">स्रोतों और एक्सटेंशन सूचियों में दिखाएं</string>
<string name="pref_category_nsfw_content">NSFW (18+) स्रोत</string>
<string name="file_picker_error">कोई फ़ाइल पिकर ऐप नहीं मिला</string>
<string name="myanimelist_relogin">कृपया फिर से MAL पर लॉगिन करें</string>
<string name="track_finished_reading_date">समाप्ति की तिथि</string>
@@ -575,7 +570,6 @@
<string name="empty_backup_error">बैकअप करने के लिए कोई पुस्तकालय प्रविष्टि नहीं</string>
<string name="ext_info_version">संस्करण</string>
<string name="ext_info_language">भाषा</string>
<string name="ext_info_age_rating">आयु रेटिंग</string>
<string name="reading_list">पठन सूची</string>
<string name="wish_list">विश-लिस्ट</string>
<string name="theme_lavender">लैवेंडर</string>
@@ -399,10 +399,7 @@
<string name="channel_errors">Greške</string>
<string name="pref_remove_bookmarked_chapters">Dozvoli brisanje zabilježenih poglavlja</string>
<string name="pref_category_delete_chapters">Izbriši poglavlja</string>
<string name="ext_nsfw_warning">Izvori ovog proširenja mogu sadržavati neprikladan sadržaj (18+)</string>
<string name="ext_nsfw_short">18+</string>
<string name="backup_restore_missing_trackers">Usluge praćenja bez prijave:</string>
<string name="parental_controls_info">To ne sprečava neslužbene ili potencijalno krivo označena proširenja prikazati neprikladan sadržaj (18+) unutar aplikacije.</string>
<string name="no_chapters_error">Nema poglavlja</string>
<string name="chapter_settings_updated">Standardne postavke poglavlja su aktualizirane</string>
<string name="share_page_info">%1$s: %2$s, %3$d. stranica</string>
@@ -421,10 +418,8 @@
<string name="spen_next_page">Sljedeća stranica</string>
<string name="spen_previous_page">Prethodna stranica</string>
<string name="migration_help_guide">Vodič za migraciju izvora</string>
<string name="pref_category_nsfw_content">Izvori neprikladnog sadržaja (18+)</string>
<string name="file_picker_error">Nije pronađena nijedna aplikacija za biranje datoteka</string>
<string name="myanimelist_relogin">Ponovo se prijavi na MAL</string>
<string name="pref_show_nsfw_source">Prikaži u popisu izvora i proširenja</string>
<string name="track_finished_reading_date">Datum kraja</string>
<string name="track_started_reading_date">Datum početka</string>
<string name="pref_dump_crash_logs_summary">Sprema zapise grešaka u datoteku za dijeljenje podataka s programerima</string>
@@ -585,7 +580,6 @@
<string name="ext_info_language">Jezik</string>
<string name="ext_info_version">Verzija</string>
<string name="popular">Popularni</string>
<string name="ext_info_age_rating">Dobna granica</string>
<string name="download_ahead">Preuzmi unaprijed</string>
<string name="multi_lang">Višejezičnost</string>
<string name="missing_storage_permission">Dozvole za spremište nisu odobrena</string>
@@ -160,7 +160,6 @@
<string name="information_webview_outdated">A jobb kompatibilitás érdekében kérlek, frissítsd a WebView alkalmazást</string>
<string name="update_check_notification_update_available">Új verzió érhető el!</string>
<string name="email">E-mail-cím</string>
<string name="pref_category_nsfw_content">NSFW (18+) források</string>
<string name="show_chapter_number">Fejezetszám</string>
<string name="scale_type_original_size">Eredeti méret</string>
<string name="last_read_chapter">Utoljára olvasott fejezet</string>
@@ -186,12 +185,8 @@
<string name="pref_library_update_refresh_metadata">Metaadatokat frissítse automatikusan</string>
<string name="pref_category_library_update">Minden frissítése</string>
<string name="pref_category_display">Megjelenés</string>
<string name="parental_controls_info">Ez nem akadályozza meg a nem hivatalos vagy az esetlegesen helytelenül megjelölt bővítményeket a 18+-os tartalmak megjelenítésétől az alkalmazáson belül.</string>
<string name="pref_show_nsfw_source">Megjelenítés a forrás- és kiterjesztéseklistában</string>
<string name="pref_show_reading_mode_summary">Rövid ideig mutassa a jelenlegi módot az olvasó megnyitásakor</string>
<string name="pref_show_reading_mode">Olvasó mód mutatása</string>
<string name="ext_nsfw_warning">Tartalmazhat felnőtt (18+) tartalmat</string>
<string name="ext_nsfw_short">18+</string>
<string name="obsolete_extension_message">Ez a bővítmény nem elérhető többé. Előfordulhat, hogy nem működik megfelelően, és problémákat okozhat az alkalmazásban. Az eltávolítása javasolt.</string>
<string name="add_to_library">Könyvtárhoz adás</string>
<string name="unknown_status">Ismeretlen állapot</string>
@@ -460,7 +455,6 @@
<string name="update_check_no_new_updates">Nem található új frissítés</string>
<string name="ext_info_version">Verzió</string>
<string name="ext_info_language">Nyelv</string>
<string name="ext_info_age_rating">Korhatár</string>
<string name="pref_reset_viewer_flags_success">Minden olvasói beállítás visszaállítva</string>
<string name="login_title">Jelentkezz be %1$s-ba/be</string>
<string name="track_type">Típus</string>
@@ -94,9 +94,9 @@
<string name="white_background">Putih</string>
<string name="black_background">Hitam</string>
<string name="pref_viewer_type">Mode baca bawaan</string>
<string name="left_to_right_viewer">Halaman (kiri ke kanan)</string>
<string name="right_to_left_viewer">Halaman (kanan ke kiri)</string>
<string name="vertical_viewer">Halaman (vertikal)</string>
<string name="left_to_right_viewer">Berhalaman (kiri ke kanan)</string>
<string name="right_to_left_viewer">Berhalaman (kanan ke kiri)</string>
<string name="vertical_viewer">Berhalaman (vertikal)</string>
<string name="webtoon_viewer">Strip panjang</string>
<string name="pref_image_scale_type">Tipe skala</string>
<string name="scale_type_fit_screen">Pas layar</string>
@@ -329,8 +329,8 @@
<string name="add_tracking">Tambahkan pelacak</string>
<string name="manga_info_collapse">Lebih sedikit</string>
<string name="manga_info_expand">Selengkapnya</string>
<string name="in_library">Di pustaka</string>
<string name="add_to_library">Tambahkan ke pustaka</string>
<string name="in_library">Di perpustakaan</string>
<string name="add_to_library">Tambahkan ke perpustakaan</string>
<string name="licenses">Lisensi terbuka</string>
<string name="restoring_backup_error">Gagal memulihkan data dari cadangan</string>
<string name="creating_backup_error">Pencadangan data gagal</string>
@@ -396,11 +396,8 @@
<string name="channel_progress">Kemajuan</string>
<string name="channel_errors">Masalah</string>
<string name="backup_restore_missing_trackers">Pelacakan tidak tersinkronisasi di:</string>
<string name="ext_nsfw_warning">Sumber dari ekstensi ini mungkin berisi konten DEWASA (NSFW 18+)</string>
<string name="ext_nsfw_short">18+</string>
<string name="pref_remove_bookmarked_chapters">Hapus bab yang ditandai</string>
<string name="pref_category_delete_chapters">Hapus chapter</string>
<string name="parental_controls_info">Hal ini tidak mencegah ekstensi non-resmi atau konten DEWASA (NSFW 18+) yang tidak ditandai dengan benar muncul di dalam aplikasi.</string>
<string name="no_chapters_error">Tidak ada bab yang ditemukan</string>
<string name="chapter_settings_updated">Pengaturan bab bawaan diperbarui</string>
<string name="set_chapter_settings_as_default">Terapkan sebagai bawaan</string>
@@ -416,13 +413,11 @@
<string name="pref_clear_history">Hapus riwayat</string>
<string name="clear_history_confirmation">Apakah anda yakin? Semua riwayat akan terhapus.</string>
<string name="clear_history_completed">Riwayat telah dihapus</string>
<string name="pref_category_nsfw_content">Sumber NSFW (18+)</string>
<string name="spen_next_page">Halaman selanjutnya</string>
<string name="spen_previous_page">Halaman sebelumnya</string>
<string name="migration_help_guide">Petunjuk migrasi</string>
<string name="file_picker_error">Tidak ada aplikasi pemilih berkas yang ditemukan</string>
<string name="myanimelist_relogin">Harap login kembali ke MAL</string>
<string name="pref_show_nsfw_source">Tampilkan di daftar sumber dan ekstensi</string>
<string name="edge_nav">Tepi</string>
<string name="l_nav">Bentuk L</string>
<string name="action_desc">Urutan turun</string>
@@ -580,7 +575,6 @@
<string name="pref_reset_viewer_flags_error">Tidak bisa mengatur ulang setelan pembaca</string>
<string name="ext_info_version">Versi</string>
<string name="ext_info_language">Bahasa</string>
<string name="ext_info_age_rating">Rating Umur</string>
<string name="reading_list">Daftar Bacaan</string>
<string name="unfinished_list">Daftar Yang Belum Selesai</string>
<string name="wish_list">Daftar Keinginan</string>
@@ -625,7 +619,7 @@
<string name="pref_library_summary">Kategori, pembaruan global, geser chapter</string>
<string name="pref_backup_summary">Pencadangan manual &amp; otomatis, ruang penyimpanan</string>
<string name="pref_advanced_summary">Buat log kerusakan, pengoptimalan baterai</string>
<string name="crash_screen_title">Aduh!</string>
<string name="crash_screen_title">Ups!</string>
<string name="crash_screen_restart_application">Mulai ulang aplikasi</string>
<string name="invalid_location">Lokasi tidak valid: %s</string>
<string name="unknown_title">Judul tidak diketahui</string>
@@ -790,7 +784,7 @@
<string name="onboarding_permission_analytics">Izinkan analitik</string>
<string name="onboarding_permission_analytics_description">Kirim data penggunaan anonim untuk meningkatkan fitur aplikasi.</string>
<string name="pref_firebase">Analisis dan Log Kerusakan</string>
<string name="firebase_summary">Mengirim log kerusakan dan analitik akan memungkinkan kami mengidentifikasi dan memperbaiki masalah, meningkatkan kinerja, dan membuat pembaruan mendatang lebih relevan dengan kebutuhan anda</string>
<string name="firebase_summary">Mengirimkan log kerusakan dan analitik akan memungkinkan kami mengidentifikasi serta memperbaiki masalah, meningkatkan kinerja, dan membuat pembaruan mendatang lebih relevan dengan kebutuhan Anda</string>
<string name="pref_security">Keamanan</string>
<string name="non_library_settings">Semua entri yang sudah dibaca</string>
<string name="remove_private_extension_message">Apa anda yakin ingin menghapus ekstensi \"%s\"?</string>
@@ -798,16 +792,16 @@
<string name="ext_confirm_remove">Hapus Ekstensi?</string>
<string name="pref_auto_update_manga_on_mark_read">Kemajuan pembaruan saat ditandai sebagai telah dibaca</string>
<string name="trackers_updated_summary">Pelacak diperbarui ke bab %d</string>
<string name="pref_incognito_mode_extension_summary">Jeda riwayat pembacaan untuk ekstensi</string>
<string name="pref_incognito_mode_extension_summary">Jeda riwayat membaca untuk ekstensi</string>
<string name="label_auto">Otomatis</string>
<string name="pref_mark_duplicate_read_chapter_read">Tandai bab duplikat sebagai sudah dibaca</string>
<string name="pref_mark_duplicate_read_chapter_read">Tandai bab duplikat yang telah dibaca sebagai telah dibaca</string>
<string name="theme_monochrome">Monokrom</string>
<string name="export">Ekspor</string>
<string name="library_list">Daftar pustaka</string>
<string name="library_exported">Pustaka diekspor</string>
<string name="author">Penulis</string>
<string name="artist">Seniman</string>
<string name="tracked_privately">Dilacak secara pribady</string>
<string name="tracked_privately">Dilacak secara pribadi</string>
<string name="action_toggle_private_on">Lacak secara pribadi</string>
<string name="action_toggle_private_off">Lacak secara publik</string>
<string name="action_edit_notes">Ubah catatan</string>
@@ -815,7 +809,7 @@
<string name="action_display_unread_badge">Chapter belum dibaca</string>
<string name="theme_catppuccin">Catppuccin</string>
<string name="pref_behavior">Perilaku</string>
<string name="pref_mark_duplicate_read_chapter_read_existing">Setelah membaca satu bab</string>
<string name="pref_mark_duplicate_read_chapter_read_existing">Setelah membaca bab</string>
<string name="pref_mark_duplicate_read_chapter_read_new">Setelah mengambil bab baru</string>
<string name="pref_hide_missing_chapter_indicators">Sembunyikan indikator bab yang hilang</string>
<string name="storage_failed_to_create_download_directory">Gagal membuat direktori unduhan</string>
@@ -900,4 +894,5 @@
<string name="donationCampaign.paragraph3">Bagaimanapun juga, Mihon akan terus dipelihara oleh saya dan para sukarelawan, dan akan tetap bersifat open source.</string>
<string name="donationCampaign.contactPlatform">Discord</string>
<string name="donationCampaign.dismiss">Tutup</string>
<string name="considering">Mempertimbangkan</string>
</resources>
@@ -399,11 +399,8 @@
<string name="no_pinned_sources">Non hai fonti fissate</string>
<string name="pref_remove_bookmarked_chapters">Consenti eliminazione capitoli con segnalibro</string>
<string name="pref_category_delete_chapters">Elimina capitoli</string>
<string name="ext_nsfw_warning">Fonti di questa estensione potrebbero contenere materiale NSFW (18+)</string>
<string name="ext_nsfw_short">18+</string>
<string name="backup_restore_missing_trackers">Tracker non collegati:</string>
<string name="no_chapters_error">Nessun capitolo trovato</string>
<string name="parental_controls_info">Questo non impedisce a estensioni non ufficiali oppure incorrettamente segnalate di mostrare contenuti 18+ all\'interno dell\'applicazione.</string>
<string name="chapter_settings_updated">Impostazioni predefinite dei capitoli aggiornate</string>
<string name="share_page_info">%1$s: %2$s, pagina %3$d</string>
<string name="set_chapter_settings_as_default">Imposta come predefinito</string>
@@ -421,8 +418,6 @@
<string name="spen_next_page">Pagina successiva</string>
<string name="spen_previous_page">Pagina precedente</string>
<string name="migration_help_guide">Guida alla migrazione delle fonti</string>
<string name="pref_category_nsfw_content">Fonti NSFW (18+)</string>
<string name="pref_show_nsfw_source">Mostra nelle liste delle fonti e delle estensioni</string>
<string name="myanimelist_relogin">Per favore accedi di nuovo a MAL</string>
<string name="file_picker_error">Nessuna app per la selezione dei file trovata</string>
<string name="pref_viewer_nav">Zone di tocco</string>
@@ -578,7 +573,6 @@
<string name="pref_reset_viewer_flags_success">Ripristinate tutte le opzioni del lettore</string>
<string name="pref_reset_viewer_flags_error">Impossibile ripristinare le opzioni del lettore</string>
<string name="empty_screen">Beh, questo è imbarazzante</string>
<string name="ext_info_age_rating">Classificazione per età</string>
<string name="ext_info_version">Versione</string>
<string name="ext_info_language">Lingua</string>
<string name="on_hold_list">In Pausa</string>
@@ -877,4 +871,16 @@
<string name="download_bookmarked">Segnato</string>
<string name="action_filter_excluded_scanlators">Filtrare i scanlator esclusi</string>
<string name="considering">Considerando</string>
<string name="ext_content_warning_mixed_description">La fonte per questa estensione potebbe contenere contenuto non sicuro o 18+.</string>
<string name="downloading_with_progress">Scaricamento… (%d%%)</string>
<string name="ext_info_warning">Avviso</string>
<string name="ext_content_warning_nsfw">Per maggiorenni</string>
<string name="label_support_us">Supportaci</string>
<string name="pref_allowed_content_warnings">Avvisi sui contenuti consentiti</string>
<string name="ext_content_warning_nsfw_description">Le fonti per questa estensione sono dirette principalmente a contenuti 18+.</string>
<string name="theme_tokyonight">Tokyo Notte</string>
<string name="pref_category_extensions">Estensioni</string>
<string name="ext_content_warning_mixed">Misto</string>
<string name="ext_content_warning_safe">Sicuro</string>
<string name="content_warnings_info">Per rendere effettive le modifiche è necessario riavviare l\'app. Le estensioni che non sono caricate non forniranno le loro fonti in nessuna parte dell\'app. Ciò non impedisce alle estensioni non ufficiali o potenzialmente contrassegnate in modo errato di mostrare contenuti 18+ all\'interno dell\'app.</string>
</resources>
@@ -400,9 +400,6 @@
<string name="backup_restore_missing_trackers">ログインしていないトラッカー:</string>
<string name="pref_remove_bookmarked_chapters">ブックマークした章の削除を許可</string>
<string name="pref_category_delete_chapters">章を削除</string>
<string name="ext_nsfw_warning">この拡張機能のソースには成人向けのコンテンツが含まれる可能性があります</string>
<string name="ext_nsfw_short">成人向け</string>
<string name="parental_controls_info">これは、非公式の拡張機能や誤ってフラグ付けされた可能性のある拡張機能が、アプリ内で NSFW(18 歳以上向け)コンテンツを表示することを防ぐものではありません。</string>
<string name="no_chapters_error">章が見つかりません</string>
<string name="chapter_settings_updated">デフォルトの章設定を更新しました</string>
<string name="share_page_info">%1$s: %2$s、第 %3$d ページ</string>
@@ -421,8 +418,6 @@
<string name="spen_next_page">次のページ</string>
<string name="spen_previous_page">前のページ</string>
<string name="migration_help_guide">ソース移行ガイド</string>
<string name="pref_category_nsfw_content">成人向けのソース</string>
<string name="pref_show_nsfw_source">ソース・拡張機能リストに表示</string>
<string name="file_picker_error">ファイルを選択できるアプリが見つかりません</string>
<string name="myanimelist_relogin">MAL に再度ログインしてください</string>
<string name="action_display_show_number_of_items">アイテム数を表示する</string>
@@ -580,7 +575,6 @@
<string name="empty_screen">えっと、こりゃまずいですね</string>
<string name="ext_info_version">バージョン</string>
<string name="ext_info_language">言語</string>
<string name="ext_info_age_rating">年齢制限</string>
<string name="network_not_metered">非従量制課金接続でのみ</string>
<string name="reading_list">読書中リスト</string>
<string name="wish_list">読みたいリスト</string>
@@ -173,13 +173,10 @@
<string name="pref_app_theme">Tema aplikasi</string>
<string name="theme_monet">Dinamis</string>
<string name="theme_greenapple">Ijo apel</string>
<string name="parental_controls_info">Iki ora nyegah ekstensi kang ora resmi utawa salah dilabeli kanggo nampilake konten NSFW (18+) ing aplikasi.</string>
<string name="relative_time_today">Dinten puniki</string>
<string name="update_never">Mati</string>
<string name="pref_category_security">Kaamanan</string>
<string name="lock_always">Tansah</string>
<string name="pref_category_nsfw_content">Sumber NSFW (18+)</string>
<string name="pref_show_nsfw_source">Tampilaken ing sumber lan dhaptar ekstensi</string>
<string name="pref_update_only_completely_read">Karo bab durung diwaca</string>
<string name="pref_manage_notifications">Kelola notifikasi</string>
<string name="secure_screen_summary">Amanaken layar sembunikan konten aplikasi wektu ngaleh aplikasi lan blokir tangkapan layar</string>
@@ -219,8 +216,6 @@
<string name="ext_uninstall">Ucul</string>
<string name="obsolete_extension_message">Ekstensi iki ora kasedhiya maneh.</string>
<string name="extension_api_error">Gagal kanggo entuk dhaptar ekstensi</string>
<string name="ext_nsfw_short">18+</string>
<string name="ext_nsfw_warning">Sumber saka ekstensi iki bisa ngemot konten NSFW (18+)</string>
<string name="ext_install_service_notif">Masang ekstensi…</string>
<string name="ext_installer_pref">Pemasang</string>
<string name="ext_installer_shizuku_stopped">Shizuku ora lumaku</string>
@@ -251,7 +246,6 @@
<string name="ext_pending">Ketunda</string>
<string name="ext_info_version">Versine</string>
<string name="ext_info_language">Bohoso</string>
<string name="ext_info_age_rating">Rating umur</string>
<string name="ext_app_info">Infone aplikasi</string>
<string name="pref_show_navigation_mode_summary">Tampilake kanthi ringkes nalika maca dibukak</string>
<string name="pref_dual_page_split">Pamisahan kaca kaping pindho</string>
@@ -393,13 +393,10 @@
<string name="action_disable">გამორთვა</string>
<string name="action_start">დაწყება</string>
<string name="action_sort_date_added">დამატების თარიღი</string>
<string name="ext_nsfw_warning">შეიძლება შეიჩაცდეს უცენზურო(18+) კონტენტს</string>
<string name="ext_nsfw_short">18+</string>
<string name="pref_remove_bookmarked_chapters">მონიშნული თავების წაშლა</string>
<string name="pref_category_delete_chapters">თავების წაშლა</string>
<string name="right_and_left_nav">მარჯვენა და მარცხენა</string>
<string name="pref_dual_page_split">განიერი გვერდების გაყოფა</string>
<string name="parental_controls_info">ეს არ აღმოფხვრავს პროგრამაში NSFW (18+) შიგთავსის არაოფიციალურ, ან შესაძლო არასწორად მონიშნული გაფართოებებოიდან გამოჩენას.</string>
<string name="action_display_show_number_of_items">საგნების რაოდენობის ჩვენება</string>
<string name="action_search_settings">პარამეტრების ძიება</string>
<string name="action_start_downloading_now">დაიწყე ჩამოტვირთვა</string>
@@ -79,9 +79,6 @@
<string name="charging">Қуаттандыру кездінде</string>
<string name="restrictions">Шектеулер: %s</string>
<string name="secure_screen_summary">Қауіпсіз экран қолданбаларды ауыстырған кезде қолданба мазмұнын жасырады және скриншот жасауды бұғаттайды</string>
<string name="pref_category_nsfw_content">NSFW (18+) дереккөздері</string>
<string name="pref_show_nsfw_source">Дереккөздер мен кеңейтулер тізімінде көрсету</string>
<string name="parental_controls_info">Бұл қолданбадағы NSFW (18+) мазмұнын көрсетуге бейресми немесе ықтимал қате белгіленген кеңейтулерге кедергі жасамайды.</string>
<string name="action_mark_as_read">Оқылған деп белгілеу</string>
<string name="action_mark_as_unread">Оқылмаған деп белгілеу</string>
<string name="action_mark_previous_as_read">Алдыңғысын оқылған деп белгілеу</string>
@@ -200,7 +197,6 @@
<string name="network_not_metered">Тек шексіз желіде</string>
<string name="ext_info_version">Нұсқа</string>
<string name="ext_info_language">Тіл</string>
<string name="ext_info_age_rating">Жас рейтингі</string>
<string name="action_display_comfortable_grid">Ыңғайлы кесте</string>
<string name="action_display_cover_only_grid">Атаусыз кесте</string>
<string name="pref_library_update_refresh_metadata">Метадеректерді автоматты түрде жаңарту</string>
@@ -236,8 +232,6 @@
<string name="ext_app_info">Қолданба туралы</string>
<string name="untrusted_extension">Сенімсіз кеңейту</string>
<string name="untrusted_extension_message">Бұл кеңейту сенімсіз сертификатпен қол қойылған және іске қосылмаған. \n \nЗиянды кеңейту Mihon-де сақталған кез келген кіру тіркелгі деректерін оқуы немесе еркін кодты орындауы мүмкін. \n \nОсы сертификатқа сену арқылы сіз осы қауіп-қатерді қабылдайсыз.</string>
<string name="ext_nsfw_short">18+</string>
<string name="ext_nsfw_warning">Бұл кеңейту дереккөзінде NSFW (18+) мазмұны болуы мүмкін</string>
<string name="ext_install_service_notif">Кеңейту орнатылуда…</string>
<string name="pref_fullscreen">Толық экранды</string>
<string name="pref_page_transitions">Беттің ауысуын анимациялау</string>
@@ -137,8 +137,6 @@
<string name="lock_when_idle">ចាក់សោនៅពេលមិនធ្វើអ្វី</string>
<string name="lock_always">គ្រប់ពេល</string>
<string name="secure_screen">ការពារអេក្រង់</string>
<string name="pref_category_nsfw_content">ប្រភពមិនល្អ(១៨+)</string>
<string name="pref_show_nsfw_source">បង្ហាញនៅក្នុងបញ្ចីប្រភព</string>
<string name="relative_time_today">ថ្ងៃនេះ</string>
<string name="pref_category_display">ការបង្ហាញ</string>
<string name="pref_library_columns">ធាតុក្នុងមួយជួ</string>
@@ -161,7 +159,6 @@
<string name="update_12hour">រាល់១២ម៉ោងម្ដង</string>
<string name="secure_screen_summary">ការពារអេក្រង់លាក់contentរបស់កម្មវិធីនៅពេលswitchingកម្មីធីនិងប្លុកscreenshots</string>
<string name="lock_never">មិនដែល</string>
<string name="parental_controls_info">ការសម្រេចចិត្តនេះមិនបិទការflagនៃប្រភពទៅលើរឿង(១៨+)នៅក្នុងកម្មវិធីឡើយ។</string>
<string name="update_24hour">រាល់ថ្ងៃ</string>
<string name="pref_category_library_update">ការធ្វើបច្ចុប្បន្នភាពសកល</string>
<string name="title">ចំណងជើង</string>
@@ -238,8 +238,6 @@
<string name="tapping_inverted_vertical">Stûnî</string>
<string name="tapping_inverted_both">Herdu</string>
<string name="theme_strawberrydaiquiri">Daiquiriya Çîlekan</string>
<string name="pref_category_nsfw_content">Çavkaniyên NSFW (18+)</string>
<string name="pref_show_nsfw_source">Di lîsteyên çavkanî û pêvekan de nîşan bide</string>
<string name="pref_category_display">Ekran</string>
<string name="pref_library_columns">Babetên di her rêzê de</string>
<string name="pref_category_library_update">Nûkirina giştî</string>
@@ -270,6 +268,4 @@
<string name="pref_chapter_swipe_end">Kişandina beşê ber bi rastê</string>
<string name="pref_chapter_swipe_start">Kişandina beşê ber bi çepê</string>
<string name="pref_mark_duplicate_read_chapter_read">Beşên dubare yên xwendî wekî xwendî nîşan bide</string>
<string name="ext_info_age_rating">Nirxandina temenî</string>
<string name="ext_nsfw_short">18+</string>
</resources>
@@ -396,7 +396,6 @@
<string name="no_pinned_sources">ಯಾವುದೆ ಪಿನ್ ಮಾಡಿರುವ ಸೋರ್ಸ್ ಗಳು ಇಲ್ಲ</string>
<string name="channel_complete">ಪೂರ್ಣ</string>
<string name="channel_progress">ಪ್ರಗತಿ</string>
<string name="parental_controls_info">ಇದು ಅನಧಿಕೃತ ಮತ್ತು ತಪ್ಪಾಗಿ ಚಿಹ್ನಿಸಿದ ವಿಸ್ತರಣೆಗಳಿಂದ 18+ ಮೂಲಗಳನ್ನು ಮರೆಮಾಚಲಾಗುವುದಿಲ್ಲ.</string>
<string name="channel_errors">ದೋಷಗಳು</string>
<string name="chapter_settings_updated">ಅಧ್ಯಾಯದ ಡೀಫಾಲ್ಟ್ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ</string>
<string name="share_page_info">%1$s: %2$s, ಪುಟ %3$d</string>
@@ -410,8 +409,6 @@
<string name="backup_restore_missing_trackers">ಟ್ರ್ಯಾಕರ್ ಗಳು ಲಾಗಿನ್ ಆಗಿಲ್ಲ:</string>
<string name="pref_remove_bookmarked_chapters">ಬುಕ್ ಮಾರ್ಕ್ ಮಾಡಿದ ಅಧ್ಯಾಯಗಳನ್ನು ಅಳಿಸಿ</string>
<string name="pref_category_delete_chapters">ಅಧ್ಯಾಯಗಳನ್ನು ಅಳಿಸಿ</string>
<string name="ext_nsfw_warning">NSFW (18+) ವಿಷಯವನ್ನು ಹೊಂದಿರಬಹುದು</string>
<string name="ext_nsfw_short">18+</string>
<string name="action_search_settings">ಸಂಯೋಜನೆಗಳಲ್ಲಿ ಹುಡುಕಿ</string>
<string name="spen_next_page">ಮುಂದಿನ ಪುಟ</string>
<string name="spen_previous_page">ಹಿಂದಿನ ಪುಟ</string>
@@ -423,8 +420,6 @@
<string name="pref_clear_history">ಇತಿಹಾಸವನ್ನು ತೆರವುಗೊಳಿಸಿ</string>
<string name="clear_history_confirmation">ನೀವು ಖಚಿತವಾಗಿರುವಿರಾ? ಎಲ್ಲಾ ಇತಿಹಾಸವೂ ಕಳೆದುಹೋಗುತ್ತದೆ.</string>
<string name="clear_history_completed">ಇತಿಹಾಸವನ್ನು ಅಳಿಸಲಾಗಿದೆ</string>
<string name="pref_show_nsfw_source">ಮೂಲಗಳ ಪಟ್ಟಿಯಲ್ಲಿ ತೋರಿಸಿ</string>
<string name="pref_category_nsfw_content">ವಯಸ್ಕ (18+) ಮೂಲಗಳು</string>
<string name="action_filter_tracked">ಟ್ರ್ಯಾಕ್ ಮಾಡಲಾದ</string>
<string name="action_desc">ಮೊದಲು ಚಿಕ್ಕದು</string>
<string name="action_asc">ಮೊದಲ ಸಾಣ್ಣದ್ದು</string>
@@ -473,7 +473,6 @@
<string name="pref_dump_crash_logs">오류 로그 공유</string>
<string name="chapter_not_found">회차를 찾을 수 없습니다</string>
<string name="error_no_match">결과가 없습니다</string>
<string name="ext_nsfw_warning">이 확장 앱의 소스는 성인 컨텐츠가 포함될 수 있습니다</string>
<string name="action_filter_tracked">트래커 사용</string>
<string name="action_start">시작</string>
<string name="action_start_downloading_now">지금 다운로드 시작</string>
@@ -497,7 +496,6 @@
<string name="ext_obsolete">연결된 스토어 없음</string>
<string name="no_results_found">검색 결과가 없습니다</string>
<string name="pref_dual_page_invert_summary">넓은 페이지 분할 시 배치가 읽는 방향과 다를 경우</string>
<string name="ext_nsfw_short">성인 컨텐츠</string>
<string name="action_display_show_tabs">카테고리 탭 보이기</string>
<string name="action_sort_chapter_fetch_date">챕터를 가져온 날짜순</string>
<string name="action_sort_count">전체 항목 개수순</string>
@@ -548,9 +546,6 @@
<string name="theme_tako">문어</string>
<string name="theme_yinyang">음양</string>
<string name="theme_yotsuba">요츠바</string>
<string name="pref_category_nsfw_content">성인 콘텐츠 소스</string>
<string name="pref_show_nsfw_source">소스 및 확장 기능 목록에 보이기</string>
<string name="parental_controls_info">이 옵션을 끄더라도 비공식 또는 분류가 잘못된 확장 기능으로 인하여 성인 컨텐츠가 표시될 수 있습니다.</string>
<string name="action_show_errors">터치하여 자세히 보기</string>
<string name="update_72hour">3일</string>
<string name="restrictions">제한: %s</string>
@@ -588,7 +583,6 @@
<string name="unfinished_list">미완성된 목록</string>
<string name="empty_screen">와...이거 좀 어색하네</string>
<string name="custom_cover">맞춤 표지</string>
<string name="ext_info_age_rating">연령 등급</string>
<string name="action_remove_everything">모두 제거</string>
<string name="pref_reset_viewer_flags_success">모든 뷰어 설정을 초기화했습니다</string>
<string name="pref_user_agent_string">기본 사용자 에이전트 문자열</string>
@@ -907,4 +901,22 @@
<string name="downloading_with_progress">다운로드 중… (%d%%)</string>
<string name="pref_filter_update_categories_details">제외된 카테고리 업데이트는 포함되는 카테고리가 있어도 표시되지 않습니다.</string>
<string name="pref_filter_upcoming_categories_details">제외 범주에 속한 작품은 포함 범주에 속해 있더라도 표시되지 않습니다.</string>
<string name="pref_category_extensions">확장 앱</string>
<string name="pref_allowed_content_warnings">허용 컨텐츠 등급</string>
<string name="pref_apply_content_warnings_to_installed">설치된 확장 앱에도 적용</string>
<string name="pref_apply_content_warnings_to_installed_summary">비활성화하면 이미 설치한 확장 앱은 등급과 무관하게 컨텐츠를 불러오고 업데이트합니다</string>
<string name="content_warnings_info">허용하지 않은 등급의 확장 앱은 이미 설치된 경우 목록에 남지만 소스는 표시되지 않으며, 설치하지 않은 경우 목록에서 숨겨집니다. 이 설정으로도 비공식 확장 앱이나 등급이 잘못 지정된 확장 앱이 앱에 18+ 콘텐츠를 표시하는 것을 완전히 막을 수는 없습니다.</string>
<string name="ext_filtered">필터링됨</string>
<!-- Text reader -->
<string name="pref_category_text_reader">텍스트</string>
<string name="pref_text_reader_font_size">글자 크기</string>
<string name="pref_text_reader_line_spacing">행간</string>
<string name="pref_text_reader_paragraph_spacing">문단 간격</string>
<string name="pref_text_reader_font_family">글꼴</string>
<string name="pref_text_reader_font_system">시스템</string>
<string name="pref_text_reader_font_sans">고딕</string>
<string name="pref_text_reader_font_serif">명조</string>
<string name="pref_text_reader_font_mono">고정폭</string>
<string name="pref_text_reader_pdf_note">EPUB/TXT에는 이 글꼴 설정이 적용됩니다. PDF는 원본 페이지 레이아웃을 유지합니다.</string>
</resources>
@@ -47,9 +47,6 @@
<string name="pref_library_columns">Elementai eilutėje</string>
<string name="pref_category_display">Rodinys</string>
<string name="relative_time_today">Šiandien</string>
<string name="parental_controls_info">Tai nesustabdo neoficialių ar potencialiai neteisingai pažymėtų plėtinių nuo N18+ turinio rodymo programoje.</string>
<string name="pref_show_nsfw_source">Rodyti šaltiniuose ir plėtinių sąrašuose</string>
<string name="pref_category_nsfw_content">N18+ šaltiniai</string>
<string name="hide_notification_content">Slėpti pranešimo turinį</string>
<string name="secure_screen_summary">Slėpti programos turinį, kai įjungiama kita programa ir neleisti daryti ekrano nuotraukų</string>
<string name="secure_screen">Saugus užraktas</string>
@@ -181,8 +178,6 @@
<string name="pref_dual_page_split">Perskirti plačius puslapius</string>
<string name="pref_show_navigation_mode_summary">Trumpai parodyti skaitytuvą, kai atidarytas</string>
<string name="pref_fullscreen">Pilno ekrano rėžimas</string>
<string name="ext_nsfw_warning">Šaltiniuose gali būti N18 turinio</string>
<string name="ext_nsfw_short">18+</string>
<string name="obsolete_extension_message">Šis plėtinys nebėra prieinamas. Plėtinys gali veikti netinkamai arba sukelti problemas programėlėje. Rekomenduojama išdiegti plėtinį.</string>
<string name="untrusted_extension_message">Šis plėtinys buvo pasirašytas nepatikimu sertifikatu ir nebuvo aktyvuotas. \n \nPlėtinys, su negeranoriškais ketinimais, gali perskaityti prisijungimo duomenys patalpintus Mihon arba gali paleisti kenkėjišką kodą. \n \nJūs priimate šias rizikas pasitikėdami sertifikatu.</string>
<string name="untrusted_extension">Nepatikimas plėtinys</string>
@@ -409,7 +404,6 @@
<string name="backup_restore_missing_trackers">Sekimo priemonės, neprijungtos prie:</string>
<string name="split_tall_images_summary">Pagerina skaitytuvo našumą</string>
<string name="ext_info_language">Kalba</string>
<string name="ext_info_age_rating">Amžiaus reitingas</string>
<string name="rotation_reverse_portrait">Apverstas portretas</string>
<string name="action_track">Trasa</string>
<string name="edge_nav">Kraštas</string>
@@ -172,7 +172,6 @@
<string name="ext_downloading">Lejupielādē</string>
<string name="ext_installing">Instalē</string>
<string name="ext_app_info">Aplikācijas informācija</string>
<string name="ext_nsfw_short">18+</string>
<string name="confirm_lock_change">Autentificējaties, lai apstriprinātu izmaiņas</string>
<string name="pref_update_only_non_completed">Ar \'\'Pabeigts\'\' statusu</string>
<string name="action_start_downloading_now">Sākt lejupielādi tūlīt</string>
@@ -185,11 +184,9 @@
<string name="charging">Uzlādējot</string>
<string name="ext_info_version">Versija</string>
<string name="ext_info_language">Valoda</string>
<string name="ext_info_age_rating">Vecuma grupa</string>
<string name="action_sort_last_manga_update">Pēdējā atjauninājuma pārbaude</string>
<string name="action_sort_unread_count">Neizlasītais skaits</string>
<string name="action_show_manga">Rādīt ierakstu</string>
<string name="ext_nsfw_warning">Šī paplašinājuma avoti var saturēt NSFW (18+) saturu</string>
<string name="ext_install_service_notif">Instalē paplašinājumu…</string>
<string name="ext_installer_pref">Instalētājs</string>
<string name="ext_installer_shizuku_stopped">Shizuku nedarbojas</string>
@@ -197,14 +194,11 @@
<string name="action_sort_count">Visi ieraksti</string>
<string name="cancel_all_for_series">Atcelt visu šai sērijai</string>
<string name="action_order_by_upload_date">Pēc augšupielādes datuma</string>
<string name="pref_category_nsfw_content">NSFW (18+) avoti</string>
<string name="pref_show_nsfw_source">Rādīt avotos un paplašinājumu sarakstos</string>
<string name="update_never">Izslēgts</string>
<string name="action_display_language_badge">Valoda</string>
<string name="action_sort_chapter_fetch_date">Nodaļas ieneses datums</string>
<string name="action_close">Aizvērt</string>
<string name="pref_category_library_update">Globāls atjauninājums</string>
<string name="parental_controls_info">Šis neliedz neoficiāliem vai, iespējams, nepareizi atzīmētiem paplašinājumiem parādīties NSFW (18+) saturu lietotnē.</string>
<string name="relative_time_today">Šodien</string>
<string name="pref_library_update_interval">Automātiskie atjauninājumi</string>
<string name="update_weekly">Katru nedēļu</string>

Some files were not shown because too many files have changed in this diff Show More