Merge branch 'devel' into m3

pull/84/head
Zakhar Timoshenko 4 years ago committed by GitHub
commit f2bbf5855b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,8 +13,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 31
versionCode 378 versionCode 379
versionName '2.1.2' versionName '2.1.3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -59,6 +59,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [ freeCompilerArgs += [
'-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', '-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
'-Xopt-in=kotlinx.coroutines.FlowPreview',
'-Xopt-in=kotlin.contracts.ExperimentalContracts', '-Xopt-in=kotlin.contracts.ExperimentalContracts',
] ]
} }

@ -7,5 +7,6 @@
public static void checkParameterIsNotNull(...); public static void checkParameterIsNotNull(...);
public static void checkNotNullParameter(...); public static void checkNotNullParameter(...);
} }
-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment
-keep class org.koitharu.kotatsu.core.db.entity.* { *; } -keep class org.koitharu.kotatsu.core.db.entity.* { *; }
-dontwarn okhttp3.internal.platform.ConscryptPlatform -dontwarn okhttp3.internal.platform.ConscryptPlatform

@ -11,6 +11,7 @@ import org.koin.core.component.get
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.medianOrNull import org.koitharu.kotatsu.utils.ext.medianOrNull
@ -28,7 +29,7 @@ object MangaUtils : KoinComponent {
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? { suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
try { try {
val page = pages.medianOrNull() ?: return null val page = pages.medianOrNull() ?: return null
val url = page.source.repository.getPageUrl(page) val url = MangaRepository(page.source).getPageUrl(page)
val uri = Uri.parse(url) val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") { val size = if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart) val zip = ZipFile(uri.schemeSpecificPart)

@ -2,51 +2,38 @@ package org.koitharu.kotatsu.core.model
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.koin.core.context.GlobalContext
import org.koin.core.error.NoBeanDefFoundException
import org.koin.core.qualifier.named
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.site.*
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@Parcelize @Parcelize
enum class MangaSource( enum class MangaSource(
val title: String, val title: String,
val locale: String?, val locale: String?,
val cls: Class<out MangaRepository>,
) : Parcelable { ) : Parcelable {
LOCAL("Local", null, LocalMangaRepository::class.java), LOCAL("Local", null),
READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java), READMANGA_RU("ReadManga", "ru"),
MINTMANGA("MintManga", "ru", MintMangaRepository::class.java), MINTMANGA("MintManga", "ru"),
SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java), SELFMANGA("SelfManga", "ru"),
MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java), MANGACHAN("Манга-тян", "ru"),
DESUME("Desu.me", "ru", DesuMeRepository::class.java), DESUME("Desu.me", "ru"),
HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java), HENCHAN("Хентай-тян", "ru"),
YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java), YAOICHAN("Яой-тян", "ru"),
MANGATOWN("MangaTown", "en", MangaTownRepository::class.java), MANGATOWN("MangaTown", "en"),
MANGALIB("MangaLib", "ru", MangaLibRepository::class.java), MANGALIB("MangaLib", "ru"),
// NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java), // NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java),
MANGAREAD("MangaRead", "en", MangareadRepository::class.java), MANGAREAD("MangaRead", "en"),
REMANGA("Remanga", "ru", RemangaRepository::class.java), REMANGA("Remanga", "ru"),
HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java), HENTAILIB("HentaiLib", "ru"),
ANIBEL("Anibel", "be", AnibelRepository::class.java), ANIBEL("Anibel", "be"),
NINEMANGA_EN("NineManga English", "en", NineMangaRepository.English::class.java), NINEMANGA_EN("NineManga English", "en"),
NINEMANGA_ES("NineManga Español", "es", NineMangaRepository.Spanish::class.java), NINEMANGA_ES("NineManga Español", "es"),
NINEMANGA_RU("NineManga Русский", "ru", NineMangaRepository.Russian::class.java), NINEMANGA_RU("NineManga Русский", "ru"),
NINEMANGA_DE("NineManga Deutsch", "de", NineMangaRepository.Deutsch::class.java), NINEMANGA_DE("NineManga Deutsch", "de"),
NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java), NINEMANGA_IT("NineManga Italiano", "it"),
NINEMANGA_BR("NineManga Brasil", "pt", NineMangaRepository.Brazil::class.java), NINEMANGA_BR("NineManga Brasil", "pt"),
NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java), NINEMANGA_FR("NineManga Français", "fr"),
EXHENTAI("ExHentai", null, ExHentaiRepository::class.java), EXHENTAI("ExHentai", null),
MANGAOWL("MangaOwl", "en", MangaOwlRepository::class.java), MANGAOWL("MangaOwl", "en"),
MANGADEX("MangaDex", null, MangaDexRepository::class.java), MANGADEX("MangaDex", null),
; ;
@get:Throws(NoBeanDefFoundException::class)
@Deprecated("", ReplaceWith("MangaRepository(this)",
"org.koitharu.kotatsu.core.parser.MangaRepository"))
val repository: MangaRepository
get() = GlobalContext.get().get(named(this))
} }

@ -7,6 +7,7 @@ import org.koin.core.qualifier.named
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.DownloadManagerHelper import org.koitharu.kotatsu.utils.DownloadManagerHelper
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -30,4 +31,5 @@ val networkModule
}.build() }.build()
} }
factory { DownloadManagerHelper(get(), get()) } factory { DownloadManagerHelper(get(), get()) }
single { MangaLoaderContext(get(), get()) }
} }

@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.parser
import android.net.Uri
import coil.map.Mapper
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.core.model.MangaSource
class FaviconMapper() : Mapper<Uri, HttpUrl> {
override fun map(data: Uri): HttpUrl {
val mangaSource = MangaSource.valueOf(data.schemeSpecificPart)
val repo = MangaRepository(mangaSource) as RemoteMangaRepository
return repo.getFaviconUrl().toHttpUrl()
}
override fun handles(data: Uri) = data.scheme == "favicon"
}

@ -7,6 +7,8 @@ import org.koitharu.kotatsu.core.model.*
interface MangaRepository { interface MangaRepository {
val source: MangaSource
val sortOrders: Set<SortOrder> val sortOrders: Set<SortOrder>
suspend fun getList2( suspend fun getList2(

@ -2,15 +2,12 @@ package org.koitharu.kotatsu.core.parser
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.site.* import org.koitharu.kotatsu.core.parser.site.*
val parserModule val parserModule
get() = module { get() = module {
single { MangaLoaderContext(get(), get()) }
factory<MangaRepository>(named(MangaSource.READMANGA_RU)) { ReadmangaRepository(get()) } factory<MangaRepository>(named(MangaSource.READMANGA_RU)) { ReadmangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.MINTMANGA)) { MintMangaRepository(get()) } factory<MangaRepository>(named(MangaSource.MINTMANGA)) { MintMangaRepository(get()) }
factory<MangaRepository>(named(MangaSource.SELFMANGA)) { SelfMangaRepository(get()) } factory<MangaRepository>(named(MangaSource.SELFMANGA)) { SelfMangaRepository(get()) }

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
@ -12,8 +11,6 @@ abstract class RemoteMangaRepository(
protected val loaderContext: MangaLoaderContext protected val loaderContext: MangaLoaderContext
) : MangaRepository { ) : MangaRepository {
protected abstract val source: MangaSource
protected abstract val defaultDomain: String protected abstract val defaultDomain: String
private val conf by lazy { private val conf by lazy {
@ -29,6 +26,8 @@ abstract class RemoteMangaRepository(
override suspend fun getTags(): Set<MangaTag> = emptySet() override suspend fun getTags(): Set<MangaTag> = emptySet()
open fun getFaviconUrl() = "https://${getDomain()}/favicon.ico"
open fun onCreatePreferences(map: MutableMap<String, Any>) { open fun onCreatePreferences(map: MutableMap<String, Any>) {
map[SourceSettings.KEY_DOMAIN] = defaultDomain map[SourceSettings.KEY_DOMAIN] = defaultDomain
} }
@ -53,8 +52,10 @@ abstract class RemoteMangaRepository(
if (subdomain != null) { if (subdomain != null) {
append(subdomain) append(subdomain)
append('.') append('.')
append(conf.getDomain(defaultDomain).removePrefix("www."))
} else {
append(conf.getDomain(defaultDomain))
} }
append(conf.getDomain(defaultDomain))
append(this@withDomain) append(this@withDomain)
} }
else -> this else -> this

@ -21,6 +21,10 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor
SortOrder.NEWEST SortOrder.NEWEST
) )
override fun getFaviconUrl(): String {
return "https://cdn.${getDomain()}/favicons/favicon.png"
}
override suspend fun getList2( override suspend fun getList2(
offset: Int, offset: Int,
query: String?, query: String?,

@ -128,7 +128,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
scanlator = null, scanlator = null,
branch = null, branch = null,
) )
} } ?: bypassLicensedChapters(manga)
) )
} }
@ -191,6 +191,32 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) :
map[SourceSettings.KEY_USE_SSL] = true map[SourceSettings.KEY_USE_SSL] = true
} }
private suspend fun bypassLicensedChapters(manga: Manga): List<MangaChapter> {
val doc = loaderContext.httpGet(manga.url.withDomain("m")).parseHtml()
val list = doc.body().selectFirst("ul.detail-ch-list") ?: return emptyList()
val dateFormat = SimpleDateFormat("MMM dd,yyyy", Locale.US)
return list.select("li").asReversed().mapIndexedNotNull { i, li ->
val a = li.selectFirst("a") ?: return@mapIndexedNotNull null
val href = a.relUrl("href")
val name = a.selectFirst("span.vol")?.text().orEmpty().ifEmpty {
a.ownText()
}
MangaChapter(
id = generateUid(href),
url = href,
source = MangaSource.MANGATOWN,
number = i + 1,
uploadDate = parseChapterDate(
dateFormat,
li.selectFirst("span.time")?.text()
),
name = name.ifEmpty { "${manga.title} - ${i + 1}" },
scanlator = null,
branch = null,
)
}
}
private fun String.parseTagKey() = split('/').findLast { TAG_REGEX matches it } private fun String.parseTagKey() = split('/').findLast { TAG_REGEX matches it }
private companion object { private companion object {

@ -151,8 +151,10 @@ class MangareadRepository(
?.selectFirst("div.reading-content") ?.selectFirst("div.reading-content")
?: throw ParseException("Root not found") ?: throw ParseException("Root not found")
return root.select("div.page-break").map { div -> return root.select("div.page-break").map { div ->
val img = div.selectFirst("img") val img = div.selectFirst("img") ?: parseFailed("Page image not found")
val url = img?.relUrl("src") ?: parseFailed("Page image not found") val url = img.relUrl("data-src").ifEmpty {
img.relUrl("src")
}
MangaPage( MangaPage(
id = generateUid(url), id = generateUid(url),
url = url, url = url,

@ -5,6 +5,7 @@ import coil.ImageLoader
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.FaviconMapper
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule val uiModule
@ -15,6 +16,7 @@ val uiModule
.componentRegistry( .componentRegistry(
ComponentRegistry.Builder() ComponentRegistry.Builder()
.add(CbzFetcher()) .add(CbzFetcher())
.add(FaviconMapper())
.build() .build()
).build() ).build()
} }

@ -123,7 +123,7 @@ class DetailsViewModel(
var manga = mangaDataRepository.resolveIntent(intent) var manga = mangaDataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga") ?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga mangaData.value = manga
manga = manga.source.repository.getDetails(manga) manga = MangaRepository(manga.source).getDetails(manga)
// find default branch // find default branch
val hist = historyRepository.getOne(manga) val hist = historyRepository.getOne(manga)
selectedBranch.value = if (hist != null) { selectedBranch.value = if (hist != null) {

@ -145,7 +145,7 @@ class DownloadManager(
while (true) { while (true) {
try { try {
val response = call.clone().await() val response = call.clone().await()
withContext(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
file.outputStream().use { out -> file.outputStream().use { out ->
checkNotNull(response.body).byteStream().copyTo(out) checkNotNull(response.body).byteStream().copyTo(out)
} }

@ -9,23 +9,24 @@ import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.size.Size import coil.size.Size
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okio.buffer import okio.buffer
import okio.source import okio.source
import java.util.zip.ZipFile import java.util.zip.ZipFile
class CbzFetcher : Fetcher<Uri> { class CbzFetcher : Fetcher<Uri> {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun fetch( override suspend fun fetch(
pool: BitmapPool, pool: BitmapPool,
data: Uri, data: Uri,
size: Size, size: Size,
options: Options, options: Options,
): FetchResult { ): FetchResult = runInterruptible(Dispatchers.IO) {
val zip = ZipFile(data.schemeSpecificPart) val zip = ZipFile(data.schemeSpecificPart)
val entry = zip.getEntry(data.fragment) val entry = zip.getEntry(data.fragment)
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name) val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
return SourceResult( SourceResult(
source = ExtraCloseableBufferedSource( source = ExtraCloseableBufferedSource(
zip.getInputStream(entry).source().buffer(), zip.getInputStream(entry).source().buffer(),
zip, zip,

@ -31,7 +31,7 @@ class MangaZip(val file: File) {
return writableCbz.flush() return writableCbz.flush()
} }
fun addCover(file: File, ext: String) { suspend fun addCover(file: File, ext: String) {
val name = buildString { val name = buildString {
append(FILENAME_PATTERN.format(0, 0)) append(FILENAME_PATTERN.format(0, 0))
if (ext.isNotEmpty() && ext.length <= 4) { if (ext.isNotEmpty() && ext.length <= 4) {
@ -39,11 +39,11 @@ class MangaZip(val file: File) {
append(ext) append(ext)
} }
} }
writableCbz[name] = file writableCbz.put(name, file)
index.setCoverEntry(name) index.setCoverEntry(name)
} }
fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) { suspend fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) {
val name = buildString { val name = buildString {
append(FILENAME_PATTERN.format(chapter.number, pageNumber)) append(FILENAME_PATTERN.format(chapter.number, pageNumber))
if (ext.isNotEmpty() && ext.length <= 4) { if (ext.isNotEmpty() && ext.length <= 4) {
@ -51,7 +51,7 @@ class MangaZip(val file: File) {
append(ext) append(ext)
} }
} }
writableCbz[name] = file writableCbz.put(name, file)
index.addChapter(chapter) index.addChapter(chapter)
} }

@ -1,8 +1,7 @@
package org.koitharu.kotatsu.local.data package org.koitharu.kotatsu.local.data
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
@ -27,11 +26,13 @@ class WritableCbzFile(private val file: File) {
} }
ZipInputStream(FileInputStream(file)).use { zip -> ZipInputStream(FileInputStream(file)).use { zip ->
var entry = zip.nextEntry var entry = zip.nextEntry
while (entry != null) { while (entry != null && currentCoroutineContext().isActive) {
val target = File(dir.path + File.separator + entry.name) val target = File(dir.path + File.separator + entry.name)
target.parentFile?.mkdirs() runInterruptible {
target.outputStream().use { out -> target.parentFile?.mkdirs()
zip.copyTo(out) target.outputStream().use { out ->
zip.copyTo(out)
}
} }
zip.closeEntry() zip.closeEntry()
entry = zip.nextEntry entry = zip.nextEntry
@ -51,11 +52,13 @@ class WritableCbzFile(private val file: File) {
tempFile.delete() tempFile.delete()
} }
try { try {
ZipOutputStream(FileOutputStream(tempFile)).use { zip -> runInterruptible {
dir.listFiles()?.forEach { ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
zipFile(it, it.name, zip) dir.listFiles()?.forEach {
zipFile(it, it.name, zip)
}
zip.flush()
} }
zip.flush()
} }
tempFile.renameTo(file) tempFile.renameTo(file)
} finally { } finally {
@ -67,29 +70,26 @@ class WritableCbzFile(private val file: File) {
operator fun get(name: String) = File(dir, name) operator fun get(name: String) = File(dir, name)
operator fun set(name: String, file: File) { suspend fun put(name: String, file: File) = runInterruptible(Dispatchers.IO) {
file.copyTo(this[name], overwrite = true) file.copyTo(this[name], overwrite = true)
} }
companion object { private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) {
if (fileToZip.isDirectory) {
private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) { if (fileName.endsWith("/")) {
if (fileToZip.isDirectory) { zipOut.putNextEntry(ZipEntry(fileName))
if (fileName.endsWith("/")) {
zipOut.putNextEntry(ZipEntry(fileName))
} else {
zipOut.putNextEntry(ZipEntry("$fileName/"))
}
zipOut.closeEntry()
fileToZip.listFiles()?.forEach { childFile ->
zipFile(childFile, "$fileName/${childFile.name}", zipOut)
}
} else { } else {
FileInputStream(fileToZip).use { fis -> zipOut.putNextEntry(ZipEntry("$fileName/"))
val zipEntry = ZipEntry(fileName) }
zipOut.putNextEntry(zipEntry) zipOut.closeEntry()
fis.copyTo(zipOut) fileToZip.listFiles()?.forEach { childFile ->
} zipFile(childFile, "$fileName/${childFile.name}", zipOut)
}
} else {
FileInputStream(fileToZip).use { fis ->
val zipEntry = ZipEntry(fileName)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
} }
} }
} }

@ -23,6 +23,7 @@ import java.util.zip.ZipFile
class LocalMangaRepository(private val context: Context) : MangaRepository { class LocalMangaRepository(private val context: Context) : MangaRepository {
override val source = MangaSource.LOCAL
private val filenameFilter = CbzFilter() private val filenameFilter = CbzFilter()
override suspend fun getList2( override suspend fun getList2(

@ -50,7 +50,7 @@ class PageLoader(
private fun loadAsync(page: MangaPage): Deferred<File> { private fun loadAsync(page: MangaPage): Deferred<File> {
var repo = repository var repo = repository
if (repo?.javaClass != page.source.cls) { if (repo?.source != page.source) {
repo = mangaRepositoryOf(page.source) repo = mangaRepositoryOf(page.source)
repository = repo repository = repo
} }

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.reader.ui package org.koitharu.kotatsu.reader.ui
import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.util.LongSparseArray import android.util.LongSparseArray
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -77,7 +76,7 @@ class ReaderViewModel(
var manga = dataRepository.resolveIntent(intent) var manga = dataRepository.resolveIntent(intent)
?: throw MangaNotFoundException("Cannot find manga") ?: throw MangaNotFoundException("Cannot find manga")
mangaData.value = manga mangaData.value = manga
val repo = manga.source.repository val repo = MangaRepository(manga.source)
manga = repo.getDetails(manga) manga = repo.getDetails(manga)
manga.chapters?.forEach { manga.chapters?.forEach {
chapters.put(it.id, it) chapters.put(it.id, it)
@ -206,7 +205,7 @@ class ReaderViewModel(
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> { private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
val manga = checkNotNull(mangaData.value) { "Manga is null" } val manga = checkNotNull(mangaData.value) { "Manga is null" }
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" } val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
val repo = manga.source.repository val repo = MangaRepository(manga.source)
return repo.getPages(chapter).mapIndexed { index, page -> return repo.getPages(chapter).mapIndexed { index, page ->
ReaderPage.from(page, index, chapterId) ReaderPage.from(page, index, chapterId)
} }

@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.model.SortOrder
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.ext.levenshteinDistance import org.koitharu.kotatsu.utils.ext.levenshteinDistance
@ -29,7 +30,7 @@ class MangaSearchRepository(
MangaProviderFactory.getSources(settings, includeHidden = false).asFlow() MangaProviderFactory.getSources(settings, includeHidden = false).asFlow()
.flatMapMerge(concurrency) { source -> .flatMapMerge(concurrency) { source ->
runCatching { runCatching {
source.repository.getList2( MangaRepository(source).getList2(
offset = 0, offset = 0,
query = query, query = query,
sortOrder = SortOrder.POPULARITY sortOrder = SortOrder.POPULARITY

@ -1,13 +1,13 @@
package org.koitharu.kotatsu.settings.sources package org.koitharu.kotatsu.settings.sources
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.*
import android.view.View import androidx.appcompat.widget.SearchView
import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
@ -19,7 +19,7 @@ import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(), class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
SourceConfigListener { SourceConfigListener, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
private lateinit var reorderHelper: ItemTouchHelper private lateinit var reorderHelper: ItemTouchHelper
private val viewModel by viewModel<SourcesSettingsViewModel>() private val viewModel by viewModel<SourcesSettingsViewModel>()
@ -42,7 +42,7 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val sourcesAdapter = SourceConfigAdapter(this) val sourcesAdapter = SourceConfigAdapter(this, get(), viewLifecycleOwner)
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
addItemDecoration(SourceConfigItemDecoration(view.context)) addItemDecoration(SourceConfigItemDecoration(view.context))
@ -59,6 +59,17 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
super.onDestroyView() super.onDestroyView()
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.opt_sources, menu)
val searchMenuItem = menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
}
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding( binding.recyclerView.updatePadding(
bottom = insets.bottom, bottom = insets.bottom,
@ -83,6 +94,20 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
viewModel.expandOrCollapse(header.localeId) viewModel.expandOrCollapse(header.localeId)
} }
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.performSearch(newText)
return true
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
(item.actionView as SearchView).setQuery("", false)
return true
}
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback( private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP, ItemTouchHelper.DOWN or ItemTouchHelper.UP,
0, 0,

@ -21,6 +21,7 @@ class SourcesSettingsViewModel(
val items = MutableLiveData<List<SourceConfigItem>>(emptyList()) val items = MutableLiveData<List<SourceConfigItem>>(emptyList())
private val expandedGroups = HashSet<String?>() private val expandedGroups = HashSet<String?>()
private var searchQuery: String? = null
init { init {
buildList() buildList()
@ -63,9 +64,30 @@ class SourcesSettingsViewModel(
buildList() buildList()
} }
fun performSearch(query: String?) {
searchQuery = query?.trim()
buildList()
}
private fun buildList() { private fun buildList() {
val sources = MangaProviderFactory.getSources(settings, includeHidden = true) val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
val hiddenSources = settings.hiddenSources val hiddenSources = settings.hiddenSources
val query = searchQuery
if (!query.isNullOrEmpty()) {
items.value = sources.mapNotNull {
if (!it.title.contains(query, ignoreCase = true)) {
return@mapNotNull null
}
SourceConfigItem.SourceItem(
source = it,
isEnabled = it.name !in hiddenSources,
isDraggable = false,
)
}.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult)
}
return
}
val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) { val map = sources.groupByTo(TreeMap(LocaleKeyComparator())) {
if (it.name !in hiddenSources) { if (it.name !in hiddenSources) {
KEY_ENABLED KEY_ENABLED
@ -81,6 +103,7 @@ class SourcesSettingsViewModel(
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = it, source = it,
isEnabled = true, isEnabled = true,
isDraggable = true,
) )
} }
} }
@ -102,6 +125,7 @@ class SourcesSettingsViewModel(
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = it, source = it,
isEnabled = false, isEnabled = false,
isDraggable = false,
) )
} }
} }

@ -1,13 +1,19 @@
package org.koitharu.kotatsu.settings.sources.adapter package org.koitharu.kotatsu.settings.sources.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
class SourceConfigAdapter( class SourceConfigAdapter(
listener: SourceConfigListener, listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : AsyncListDifferDelegationAdapter<SourceConfigItem>( ) : AsyncListDifferDelegationAdapter<SourceConfigItem>(
SourceConfigDiffCallback(), SourceConfigDiffCallback(),
sourceConfigHeaderDelegate(), sourceConfigHeaderDelegate(),
sourceConfigGroupDelegate(listener), sourceConfigGroupDelegate(listener),
sourceConfigItemDelegate(listener), sourceConfigItemDelegate(listener, coil, lifecycleOwner),
sourceConfigDraggableItemDelegate(listener),
sourceConfigEmptySearchDelegate(),
) )

@ -4,14 +4,19 @@ import android.annotation.SuppressLint
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner
import androidx.core.view.updatePaddingRelative import coil.ImageLoader
import coil.request.Disposable
import coil.request.ImageRequest
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemExpandableBinding import org.koitharu.kotatsu.databinding.ItemExpandableBinding
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.utils.ext.enqueueWith
fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>( fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
@ -38,11 +43,44 @@ fun sourceConfigGroupDelegate(
} }
} }
@SuppressLint("ClickableViewAccessibility")
fun sourceConfigItemDelegate( fun sourceConfigItemDelegate(
listener: SourceConfigListener, listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>( ) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) } { layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable }
) {
var imageRequest: Disposable? = null
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
listener.onItemEnabledChanged(item, isChecked)
}
bind {
binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled
imageRequest = ImageRequest.Builder(context)
.data(item.faviconUrl)
.error(R.drawable.ic_favicon_fallback)
.target(binding.imageViewIcon)
.lifecycle(lifecycleOwner)
.enqueueWith(coil)
}
onViewRecycled {
imageRequest?.dispose()
imageRequest = null
}
}
@SuppressLint("ClickableViewAccessibility")
fun sourceConfigDraggableItemDelegate(
listener: SourceConfigListener,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigDraggableBinding>(
{ layoutInflater, parent -> ItemSourceConfigDraggableBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable }
) { ) {
val eventListener = object : View.OnClickListener, View.OnTouchListener, val eventListener = object : View.OnClickListener, View.OnTouchListener,
@ -70,11 +108,9 @@ fun sourceConfigItemDelegate(
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled binding.switchToggle.isChecked = item.isEnabled
binding.imageViewHandle.isVisible = item.isEnabled
binding.imageViewConfig.isVisible = item.isEnabled
binding.root.updatePaddingRelative(
start = if (item.isEnabled) 0 else binding.imageViewHandle.paddingStart * 2,
end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd,
)
} }
} }
fun sourceConfigEmptySearchDelegate() = adapterDelegate<SourceConfigItem.EmptySearchResult, SourceConfigItem>(
R.layout.item_sources_empty
) { }

@ -2,21 +2,25 @@ package org.koitharu.kotatsu.settings.sources.adapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.*
class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() { class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() {
override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean { override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
return when { return when {
oldItem.javaClass != newItem.javaClass -> false oldItem.javaClass != newItem.javaClass -> false
oldItem is SourceConfigItem.LocaleGroup && newItem is SourceConfigItem.LocaleGroup -> { oldItem is LocaleGroup && newItem is LocaleGroup -> {
oldItem.localeId == newItem.localeId oldItem.localeId == newItem.localeId
} }
oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> { oldItem is SourceItem && newItem is SourceItem -> {
oldItem.source == newItem.source oldItem.source == newItem.source
} }
oldItem is SourceConfigItem.Header && newItem is SourceConfigItem.Header -> { oldItem is Header && newItem is Header -> {
oldItem.titleResId == newItem.titleResId oldItem.titleResId == newItem.titleResId
} }
oldItem == EmptySearchResult && newItem == EmptySearchResult -> {
true
}
else -> false else -> false
} }
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources.model package org.koitharu.kotatsu.settings.sources.model
import android.net.Uri
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
@ -49,8 +50,12 @@ sealed interface SourceConfigItem {
class SourceItem( class SourceItem(
val source: MangaSource, val source: MangaSource,
val isEnabled: Boolean, val isEnabled: Boolean,
val isDraggable: Boolean,
) : SourceConfigItem { ) : SourceConfigItem {
val faviconUrl: Uri
get() = Uri.fromParts("favicon", source.name, null)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@ -59,6 +64,7 @@ sealed interface SourceConfigItem {
if (source != other.source) return false if (source != other.source) return false
if (isEnabled != other.isEnabled) return false if (isEnabled != other.isEnabled) return false
if (isDraggable != other.isDraggable) return false
return true return true
} }
@ -66,7 +72,10 @@ sealed interface SourceConfigItem {
override fun hashCode(): Int { override fun hashCode(): Int {
var result = source.hashCode() var result = source.hashCode()
result = 31 * result + isEnabled.hashCode() result = 31 * result + isEnabled.hashCode()
result = 31 * result + isDraggable.hashCode()
return result return result
} }
} }
object EmptySearchResult : SourceConfigItem
} }

@ -10,4 +10,10 @@ object PendingIntentCompat {
} else { } else {
0 0
} }
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
0
}
} }

@ -31,7 +31,7 @@ class RecentWidgetProvider : AppWidgetProvider() {
context, context,
0, 0,
intent, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE
) )
) )
views.setEmptyView(R.id.stackView, R.id.textView_holder) views.setEmptyView(R.id.stackView, R.id.textView_holder)

@ -31,7 +31,7 @@ class ShelfWidgetProvider : AppWidgetProvider() {
context, context,
0, 0,
intent, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE
) )
) )
views.setEmptyView(R.id.gridView, R.id.textView_holder) views.setEmptyView(R.id.gridView, R.id.textView_holder)

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="?colorControlLight" />
</shape>
</item>
<item
android:bottom="4dp"
android:drawable="@drawable/ic_web"
android:left="4dp"
android:right="4dp"
android:top="4dp" />
</layer-list>

@ -4,17 +4,18 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall" android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:windowBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView <ImageView
android:id="@+id/imageView_handle" android:id="@+id/imageView_icon"
android:layout_width="wrap_content" android:layout_width="?android:listPreferredItemHeightSmall"
android:layout_height="match_parent" android:layout_height="?android:listPreferredItemHeightSmall"
android:paddingHorizontal="?listPreferredItemPaddingStart" android:layout_marginHorizontal="?listPreferredItemPaddingStart"
android:scaleType="center" android:labelFor="@id/textView_title"
android:src="@drawable/ic_reorder_handle" /> android:padding="8dp"
android:scaleType="fitCenter"
tools:src="@tools:sample/avatars" />
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
@ -30,16 +31,7 @@
<com.google.android.material.switchmaterial.SwitchMaterial <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_toggle" android:id="@+id/switch_toggle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
android:layout_marginEnd="?listPreferredItemPaddingEnd" />
<ImageView
android:id="@+id/imageView_config"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/settings"
android:paddingHorizontal="?listPreferredItemPaddingEnd"
android:scaleType="center"
android:src="@drawable/ic_settings" />
</LinearLayout> </LinearLayout>

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:windowBackground"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/imageView_handle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingHorizontal="?listPreferredItemPaddingStart"
android:scaleType="center"
android:src="@drawable/ic_reorder_handle" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
tools:text="@tools:sample/lorem[1]" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/imageView_config"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/settings"
android:paddingHorizontal="?listPreferredItemPaddingEnd"
android:scaleType="center"
android:src="@drawable/ic_settings" />
</LinearLayout>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/nothing_found"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorSecondary" />

@ -4,9 +4,10 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/action_languages" android:id="@+id/action_search"
android:icon="@drawable/ic_locale" android:icon="@drawable/ic_search"
android:title="@string/languages" android:title="@string/search"
app:showAsAction="ifRoom" /> app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
</menu> </menu>

@ -246,4 +246,7 @@
<string name="system_default">Па змаўчанні</string> <string name="system_default">Па змаўчанні</string>
<string name="exclude_nsfw_from_history">Не паказваць NSFW мангу з гісторыі</string> <string name="exclude_nsfw_from_history">Не паказваць NSFW мангу з гісторыі</string>
<string name="error_empty_name">Імя не можа быць пустым</string> <string name="error_empty_name">Імя не можа быць пустым</string>
<string name="show_pages_numbers">Паказваць нумары старонак</string>
<string name="enabled_sources">Уключаныя крыніцы</string>
<string name="available_sources">Даступныя крыніцы</string>
</resources> </resources>

@ -242,8 +242,11 @@
<string name="date_format">Formato de la fecha</string> <string name="date_format">Formato de la fecha</string>
<string name="system_default">Por defecto</string> <string name="system_default">Por defecto</string>
<string name="tracker_warning">Algunos fabricantes pueden cambiar el comportamiento del sistema, lo que podría interrumpir las tareas en segundo plano.</string> <string name="tracker_warning">Algunos fabricantes pueden cambiar el comportamiento del sistema, lo que podría interrumpir las tareas en segundo plano.</string>
<string name="error_empty_name">Nombre no debe estar vacío</string> <string name="error_empty_name">El nombre no debe estar vacío</string>
<string name="auth_not_supported_by">Autorización en %s no es compatible</string> <string name="auth_not_supported_by">Autorización en %s no es compatible</string>
<string name="text_clear_cookies_prompt">Se cerrará la sesión de todas las fuentes en las que esté autorizado</string> <string name="text_clear_cookies_prompt">Se cerrará la sesión de todas las fuentes en las que esté autorizado</string>
<string name="exclude_nsfw_from_history">Excluye manga NSFW del historial</string> <string name="exclude_nsfw_from_history">Excluye manga NSFW del historial</string>
<string name="show_pages_numbers">Mostrar los números de páginas</string>
<string name="enabled_sources">Fuentes activadas</string>
<string name="available_sources">Fuentes disponibles</string>
</resources> </resources>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="close_menu">بستن منو</string>
<string name="open_menu">بازکردن منو</string>
<string name="local_storage">محل ذخیره سازی</string>
<string name="favourites">موارد دلخواه</string>
<string name="history">تاریخچه</string>
<string name="error_occurred">خطایی رخ داده است</string>
<string name="network_error">خطای اتصال به شبکه</string>
</resources>

@ -1,2 +1,252 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources></resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="open_menu">Abrir menu</string>
<string name="local_storage">Armazenamento local</string>
<string name="favourites">Favoritos</string>
<string name="error_occurred">Ocorreu um erro</string>
<string name="network_error">Erro de conexão de rede</string>
<string name="details">Detalhes</string>
<string name="list">Lista</string>
<string name="detailed_list">Lista detalhada</string>
<string name="grid">Grade</string>
<string name="list_mode">Modo lista</string>
<string name="settings">Configurações</string>
<string name="loading_">Carregando…</string>
<string name="chapter_d_of_d">Capítulo %1$d de %2$d</string>
<string name="try_again">Tente novamente</string>
<string name="clear_history">Limpar histórico</string>
<string name="nothing_found">Nada encontrado</string>
<string name="history_is_empty">Histórico vazio</string>
<string name="add_bookmark">Add marca páginas</string>
<string name="you_have_not_favourites_yet">Você não tem favoritos ainda</string>
<string name="add_to_favourites">Adicionar aos favoritos</string>
<string name="add">Add</string>
<string name="enter_category_name">Nomeie a categoria</string>
<string name="save">Salvar</string>
<string name="share">Compartilhar</string>
<string name="create_shortcut">Criar atalho…</string>
<string name="share_s">Compartilhar %s</string>
<string name="search">Pesquisar</string>
<string name="search_manga">Pesquisar mangá</string>
<string name="manga_downloading_">Baixando mangá…</string>
<string name="download_complete">Download completo</string>
<string name="downloads">Downloads</string>
<string name="by_name">Por nome</string>
<string name="popular">Populares</string>
<string name="by_rating">Por avaliação</string>
<string name="all">Todos</string>
<string name="sort_order">Ordem de classificação</string>
<string name="genre">Gênero</string>
<string name="filter">Filtro</string>
<string name="dark">Escuro</string>
<string name="automatic">Automático</string>
<string name="pages">Páginas</string>
<string name="clear">Limpar</string>
<string name="text_clear_history_prompt">Você realmente quer limpar todo o seu histórico de leitura\? Essa ação não pode ser desfeita.</string>
<string name="remove">Remover</string>
<string name="_s_removed_from_history">\"%s\" removido do histórico</string>
<string name="_s_deleted_from_local_storage">\"%s\" deletado do armazenamento local</string>
<string name="wait_for_loading_finish">Aguarde o carregamento para finalizar</string>
<string name="save_page">Salvar página</string>
<string name="page_saved">Página salva com sucesso</string>
<string name="share_image">Compartilhar imagem</string>
<string name="_import">Importar</string>
<string name="updated">Utualizado</string>
<string name="delete">Deletar</string>
<string name="operation_not_supported">Essa operação não é suportada</string>
<string name="history_and_cache">Histórico e cache</string>
<string name="clear_pages_cache">Limpar cache de páginas</string>
<string name="cache">Cache</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">Padrão</string>
<string name="webtoon">Webtoon</string>
<string name="read_mode">Modo leitura</string>
<string name="grid_size">Tamanho de grade</string>
<string name="search_on_s">Pesquisar em %s</string>
<string name="delete_manga">Deletar mangá</string>
<string name="reader_settings">Configurações de leitura</string>
<string name="switch_pages">Mudar páginas</string>
<string name="taps_on_edges">Cliques na borda</string>
<string name="volume_buttons">Botões de volume</string>
<string name="network_consumption_warning">Essa operação pode consumir muito tráfego de rede</string>
<string name="dont_ask_again">Não pergunte novamente</string>
<string name="cancelling_">Cancelando…</string>
<string name="error">Erro</string>
<string name="clear_thumbs_cache">Limpar cache de thumbnails</string>
<string name="search_history_cleared">Histórico de pesquisa limpo</string>
<string name="gestures_only">Apenas gestos</string>
<string name="internal_storage">Armazenamento interno</string>
<string name="external_storage">Armazenamento externo</string>
<string name="domain">Domínio</string>
<string name="application_update">Verificar automaticamente se há actualizações</string>
<string name="app_update_available">Actualização da aplicação está disponível</string>
<string name="show_notification_app_update">Mostrar notificação se a actualização estiver disponível</string>
<string name="open_in_browser">Aberto no navegador</string>
<string name="large_manga_save_confirm">Esta manga tem %s. Quer salvar tudo isto\?</string>
<string name="save_manga">Salvar mangá</string>
<string name="notifications">Notificações</string>
<string name="new_chapters">Novos capítulos</string>
<string name="show_notification_new_chapters">Notifique sobre atualizações do mangá que está lendo</string>
<string name="download">Download</string>
<string name="read_from_start">Ler desde o início</string>
<string name="restart">Reiniciar</string>
<string name="notifications_settings">Configurações das notificações</string>
<string name="light_indicator">Indicador luminoso</string>
<string name="close_menu">Fechar menu</string>
<string name="remote_sources">Fontes remotas</string>
<string name="close">Fechar</string>
<string name="light">Brilho</string>
<string name="history">Histórico</string>
<string name="read">Ler</string>
<string name="processing_">Processando…</string>
<string name="newest">Novos</string>
<string name="theme">Tema</string>
<string name="no_description">Sem descrição</string>
<string name="_continue">Continuar</string>
<string name="chapters">Capítulos</string>
<string name="add_new_category">Add nova categoria</string>
<string name="warning">Aviso</string>
<string name="text_delete_local_manga">Você realmente quer deletar \"%s\" do armazenamento local de seu celular\?
\nEssa operação não pode ser desfeita.</string>
<string name="text_file_not_supported">Arquivo inválido. Apenas ZIP e CBZ são suportados.</string>
<string name="clear_search_history">Limpar histórico de pesquisa</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">Activado %1$d de %2$d</string>
<string name="notification_sound">Som de notificação</string>
<string name="show_pages_numbers">Mostrar números de páginas</string>
<string name="state_finished">Concluído</string>
<string name="state_ongoing">em andamento</string>
<string name="categories_">Categorias…</string>
<string name="rename">Renomear</string>
<string name="remove_category">Remover categoria</string>
<string name="text_empty_holder_primary">Está meio vazio aqui…</string>
<string name="manga_shelf">Prateleira de manga</string>
<string name="done">Feito</string>
<string name="related">Relacionado</string>
<string name="zoom_mode_keep_start">Manter no início</string>
<string name="clear_updates_feed">Limpar feed de atualizações</string>
<string name="updates_feed_cleared">Feed de atualizações limpo</string>
<string name="update">Atualizar</string>
<string name="feed_will_update_soon">A atualização do feed começará em breve</string>
<string name="track_sources">Confira as atualizações do mangá</string>
<string name="dont_check">Não verifique</string>
<string name="enter_password">Digite a senha</string>
<string name="wrong_password">Senha incorreta</string>
<string name="repeat_password">Repita a senha</string>
<string name="passwords_mismatch">As senhas não coincidem</string>
<string name="about">Cerca de</string>
<string name="app_version">Versão %s</string>
<string name="check_for_updates">Verifique se há atualizações</string>
<string name="checking_for_updates">Verificando atualizações…</string>
<string name="no_update_available">Nenhuma atualização disponível</string>
<string name="right_to_left">Direita para esquerda</string>
<string name="create_category">Nova categoria</string>
<string name="report_github">Criar problema no GitHub</string>
<string name="scale_mode">Modo de escala</string>
<string name="zoom_mode_fit_center">Centro de ajuste</string>
<string name="zoom_mode_fit_width">Ajustar à largura</string>
<string name="restart_required">É necessário reiniciar</string>
<string name="backup_restore">Restauração de backup</string>
<string name="create_backup">Criar backup de dados</string>
<string name="restore_backup">Restaurar do backup</string>
<string name="data_restored">Dados restaurados</string>
<string name="preparing_">Preparando…</string>
<string name="file_not_found">Arquivo não encontrado</string>
<string name="data_restored_success">Todos os dados restaurados com sucesso</string>
<string name="data_restored_with_errors">Os dados foram restaurados, mas há erros</string>
<string name="just_now">Agora mesmo</string>
<string name="yesterday">Ontem</string>
<string name="long_ago">Muito tempo atrás</string>
<string name="group">Grupo</string>
<string name="today">Hoje</string>
<string name="tap_to_try_again">Toque para tentar novamente</string>
<string name="silent">Silencioso</string>
<string name="captcha_required">O CAPTCHA é obrigatório</string>
<string name="captcha_solve">Resolver</string>
<string name="cookies_cleared">Todos os cookies foram removidos</string>
<string name="chapters_checking_progress">Verificando novos capítulos: %1$d de %2$d</string>
<string name="clear_feed">Limpar feed</string>
<string name="text_clear_updates_feed_prompt">Todo o histórico de atualizações será apagado e esta ação não poderá ser desfeita. Tem certeza\?</string>
<string name="new_chapters_checking">Verificação de novos capítulos</string>
<string name="reverse">Reverter</string>
<string name="sign_in">Entrar</string>
<string name="default_s">Padrão: %s</string>
<string name="_and_x_more">…e %1$d mais</string>
<string name="next">Próximo</string>
<string name="protect_application_subtitle">Digite a senha que será necessária quando o aplicativo for iniciado</string>
<string name="confirm">Confirme</string>
<string name="password_length_hint">A senha deve ter pelo menos 4 caracteres</string>
<string name="description">Descrição</string>
<string name="backup_saved">Cópias de segurança salvas com sucesso</string>
<string name="tracker_warning">Alguns fabricantes podem alterar o comportamento do sistema, o que pode quebrar as tarefas de fundo.</string>
<string name="read_more">Leia mais</string>
<string name="hide_toolbar">Ocultar a barra de ferramentas ao rolar</string>
<string name="search_only_on_s">Pesquise apenas em %s</string>
<string name="other">Outros</string>
<string name="languages">Línguas</string>
<string name="welcome">Bem vindo</string>
<string name="available_sources">Fontes disponíveis</string>
<string name="enabled_sources">Fontes ativadas</string>
<string name="queued">Enfileirado</string>
<string name="text_downloads_holder">No momento, não há downloads ativos</string>
<string name="error_empty_name">O nome não deve estar vazio</string>
<string name="about_app_translation_summary">Traduzir esta aplicação</string>
<string name="about_author">Autor</string>
<string name="about_feedback">Comentar</string>
<string name="about_feedback_4pda">Tópico no 4PDA</string>
<string name="about_support_developer">Apoiar o desenvolvedor</string>
<string name="about_support_developer_summary">Se gostar desta aplicação, pode ajudar financeiramente através de Yoomoney (ex. Yandex.Money)</string>
<string name="about_gratitudes">agradecimento</string>
<string name="about_gratitudes_summary">Estas pessoas fazem o Kotatsu tornar-se melhor!</string>
<string name="about_copyright_and_licenses">Direitos de autor e licenças</string>
<string name="about_license">Licença</string>
<string name="chapter_is_missing_text">Este capítulo está em falta no seu dispositivo. Descarregue ou leia-o online.</string>
<string name="chapter_is_missing">Falta um capítulo</string>
<string name="auth_complete">Autorização completa</string>
<string name="auth_not_supported_by">A autorização em %s não é suportada</string>
<string name="genres">Géneros</string>
<string name="about_app_translation">Tradução</string>
<string name="text_clear_cookies_prompt">Será desconectado de todas as fontes em que estiver autorizado</string>
<string name="vibration">Vibração</string>
<string name="cannot_find_available_storage">Não é possível encontrar nenhum armazenamento disponível</string>
<string name="favourites_categories">Categorias favoritas</string>
<string name="category_delete_confirm">Quer realmente remover a categoria \"%s\" dos seus favoritos\?
\nSerá perdido todos os mangas contidos.</string>
<string name="text_history_holder_secondary">Pode encontrar o que ler no menu lateral.</string>
<string name="text_local_holder_secondary">Pode salvá-lo a partir de fontes online ou importá-lo a partir de ficheiro.</string>
<string name="recent_manga">Manga recente</string>
<string name="other_storage">Outro armazenamento</string>
<string name="text_search_holder_secondary">Tente reformular a consulta.</string>
<string name="prefer_rtl_reader">Prefira o leitor da direita para a esquerda</string>
<string name="not_available">Não disponível</string>
<string name="size_s">Tamanho: %s</string>
<string name="text_history_holder_primary">Mangá que está a ler será afixada aqui</string>
<string name="text_local_holder_primary">Ainda não tem nenhuma mangá salvo</string>
<string name="pages_animation">Animação de páginas</string>
<string name="favourites_category_empty">Esta categoria está vazia</string>
<string name="read_later">Leia mais tarde</string>
<string name="updates">atualizações</string>
<string name="all_favourites">Todos os favoritos</string>
<string name="waiting_for_network">À espera de rede…</string>
<string name="use_ssl">Utilizar ligação segura (HTTPS)</string>
<string name="search_results">Resultados da pesquisa</string>
<string name="text_feed_holder">Aqui verá os novos capítulos do mangá que está a ler</string>
<string name="new_version_s">Nova versão: %s</string>
<string name="rotate_screen">Girar a tela</string>
<string name="update_check_failed">Falha na verificação de atualização</string>
<string name="protect_application">Proteger aplicativo</string>
<string name="protect_application_summary">Pedir senha no início do aplicativo</string>
<string name="zoom_mode_fit_height">Ajustar à altura</string>
<string name="black_dark_theme">Tema Black dark</string>
<string name="black_dark_theme_summary">Útil para telas AMOLED</string>
<string name="reader_mode_hint">A configuração escolhida será lembrada para este mangá</string>
<string name="backup_information">Você pode criar backup de seu histórico e favoritos e restaurá-lo</string>
<string name="clear_cookies">Limpar cookies</string>
<string name="text_clear_search_history_prompt">Você realmente deseja remover todas as consultas de pesquisa recentes\? Essa ação não pode ser desfeita.</string>
<string name="auth_required">Você deve autorizar a visualização deste conteúdo</string>
<string name="text_categories_holder">Pode usar categorias para organizar seu mangá favorito. Pressione &lt;&lt;+&gt;&gt; para criar uma categoria</string>
<string name="prefer_rtl_reader_summary">Você pode configurar o modo de leitura para cada mangá separadamente</string>
<string name="manga_save_location">Local onde serão armazenados os mangás baixados</string>
<string name="exclude_nsfw_from_history">Excluir manga NSFW da história</string>
<string name="date_format">Formato da data</string>
<string name="system_default">Padrão</string>
</resources>

@ -7,8 +7,8 @@
<string name="local_storage">Local storage</string> <string name="local_storage">Local storage</string>
<string name="favourites">Favourites</string> <string name="favourites">Favourites</string>
<string name="history">History</string> <string name="history">History</string>
<string name="error_occurred">An error has occurred</string> <string name="error_occurred">An error occurred</string>
<string name="network_error">Network connection error</string> <string name="network_error">Could not connect to the Internet</string>
<string name="details">Details</string> <string name="details">Details</string>
<string name="chapters">Chapters</string> <string name="chapters">Chapters</string>
<string name="list">List</string> <string name="list">List</string>
@ -23,12 +23,12 @@
<string name="try_again">Try again</string> <string name="try_again">Try again</string>
<string name="clear_history">Clear history</string> <string name="clear_history">Clear history</string>
<string name="nothing_found">Nothing found</string> <string name="nothing_found">Nothing found</string>
<string name="history_is_empty">History is empty</string> <string name="history_is_empty">No history yet</string>
<string name="read">Read</string> <string name="read">Read</string>
<string name="add_bookmark">Add bookmark</string> <string name="add_bookmark">Add bookmark</string>
<string name="you_have_not_favourites_yet">You have not favourites yet</string> <string name="you_have_not_favourites_yet">No favourites yet</string>
<string name="add_to_favourites">Add to favourites</string> <string name="add_to_favourites">Favourite this</string>
<string name="add_new_category">Add new category</string> <string name="add_new_category">New category</string>
<string name="add">Add</string> <string name="add">Add</string>
<string name="enter_category_name">Enter category name</string> <string name="enter_category_name">Enter category name</string>
<string name="save">Save</string> <string name="save">Save</string>
@ -37,40 +37,40 @@
<string name="share_s">Share %s</string> <string name="share_s">Share %s</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="search_manga">Search manga</string> <string name="search_manga">Search manga</string>
<string name="manga_downloading_">Manga downloading…</string> <string name="manga_downloading_">Downloading…</string>
<string name="processing_">Processing…</string> <string name="processing_">Processing…</string>
<string name="download_complete">Download complete</string> <string name="download_complete">Downloaded</string>
<string name="downloads">Downloads</string> <string name="downloads">Downloads</string>
<string name="by_name">By name</string> <string name="by_name">Name</string>
<string name="popular">Popular</string> <string name="popular">Popular</string>
<string name="updated">Updated</string> <string name="updated">Updated</string>
<string name="newest">Newest</string> <string name="newest">Newest</string>
<string name="by_rating">By rating</string> <string name="by_rating">Rating</string>
<string name="all">All</string> <string name="all">All</string>
<string name="sort_order">Sort order</string> <string name="sort_order">Sorting order</string>
<string name="genre">Genre</string> <string name="genre">Genre</string>
<string name="filter">Filter</string> <string name="filter">Filter</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="automatic">Automatic</string> <string name="automatic">Follow system</string>
<string name="pages">Pages</string> <string name="pages">Pages</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="text_clear_history_prompt">Do you really want to clear all your reading history? This action cannot be undone.</string> <string name="text_clear_history_prompt">Clear all reading history permanently?</string>
<string name="remove">Remove</string> <string name="remove">Remove</string>
<string name="_s_removed_from_history">\"%s\" removed from history</string> <string name="_s_removed_from_history">\"%s\" removed from history</string>
<string name="_s_deleted_from_local_storage">\"%s\" deleted from local storage</string> <string name="_s_deleted_from_local_storage">\"%s\" deleted from local storage</string>
<string name="wait_for_loading_finish">Wait for the load to finish</string> <string name="wait_for_loading_finish">Wait for loading to finish</string>
<string name="save_page">Save page</string> <string name="save_page">Save page</string>
<string name="page_saved">Page saved successful</string> <string name="page_saved">Saved</string>
<string name="share_image">Share image</string> <string name="share_image">Share image</string>
<string name="_import">Import</string> <string name="_import">Import</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="operation_not_supported">This operation is not supported</string> <string name="operation_not_supported">This operation is not supported</string>
<string name="text_file_not_supported">Invalid file. Only ZIP and CBZ are supported.</string> <string name="text_file_not_supported">Either pick a ZIP or CBZ file.</string>
<string name="no_description">No description</string> <string name="no_description">No description</string>
<string name="history_and_cache">History and cache</string> <string name="history_and_cache">History and cache</string>
<string name="clear_pages_cache">Clear pages cache</string> <string name="clear_pages_cache">Clear page cache</string>
<string name="cache">Cache</string> <string name="cache">Cache</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string> <string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">Standard</string> <string name="standard">Standard</string>
@ -79,32 +79,32 @@
<string name="grid_size">Grid size</string> <string name="grid_size">Grid size</string>
<string name="search_on_s">Search on %s</string> <string name="search_on_s">Search on %s</string>
<string name="delete_manga">Delete manga</string> <string name="delete_manga">Delete manga</string>
<string name="text_delete_local_manga">Do you really want to delete \"%s\" from your phone\'s local storage? \nThis operation cannot be undone.</string> <string name="text_delete_local_manga">Delete \"%s\" from device permanently?</string>
<string name="reader_settings">Reader settings</string> <string name="reader_settings">Reader settings</string>
<string name="switch_pages">Switch pages</string> <string name="switch_pages">Switch pages</string>
<string name="taps_on_edges">Taps on edges</string> <string name="taps_on_edges">Edge taps</string>
<string name="volume_buttons">Volume buttons</string> <string name="volume_buttons">Volume buttons</string>
<string name="_continue">Continue</string> <string name="_continue">Continue</string>
<string name="warning">Warning</string> <string name="warning">Warning</string>
<string name="network_consumption_warning">This operation may consume a lot of network traffic</string> <string name="network_consumption_warning">This may transfer a lot of data</string>
<string name="dont_ask_again">Don\'t ask again</string> <string name="dont_ask_again">Don\'t ask again</string>
<string name="cancelling_">Cancelling…</string> <string name="cancelling_">Cancelling…</string>
<string name="error">Error</string> <string name="error">Error</string>
<string name="clear_thumbs_cache">Clear thumbnails cache</string> <string name="clear_thumbs_cache">Clear thumbnails cache</string>
<string name="clear_search_history">Clear search history</string> <string name="clear_search_history">Clear search history</string>
<string name="search_history_cleared">Search history cleared</string> <string name="search_history_cleared">Cleared</string>
<string name="gestures_only">Gestures only</string> <string name="gestures_only">Gestures only</string>
<string name="internal_storage">Internal storage</string> <string name="internal_storage">Internal storage</string>
<string name="external_storage">External storage</string> <string name="external_storage">External storage</string>
<string name="domain">Domain</string> <string name="domain">Domain</string>
<string name="application_update">Check for updates automatically</string> <string name="application_update">Check for new versions of the app</string>
<string name="app_update_available">Application update is available</string> <string name="app_update_available">A new version of the app is available</string>
<string name="show_notification_app_update">Show notification if update is available</string> <string name="show_notification_app_update">Show notification if a new version is available</string>
<string name="open_in_browser">Open in browser</string> <string name="open_in_browser">Open in web browser</string>
<string name="large_manga_save_confirm">This manga has %s. Do you want to save all of it?</string> <string name="large_manga_save_confirm">This manga has %s. Save all of it?</string>
<string name="save_manga">Save manga</string> <string name="save_manga">Save</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">Enabled %1$d of %2$d</string> <string name="enabled_d_of_d" tools:ignore="PluralsCandidate">Turned on %1$d of %2$d</string>
<string name="new_chapters">New chapters</string> <string name="new_chapters">New chapters</string>
<string name="show_notification_new_chapters">Notify about updates of manga you are reading</string> <string name="show_notification_new_chapters">Notify about updates of manga you are reading</string>
<string name="download">Download</string> <string name="download">Download</string>
@ -112,61 +112,61 @@
<string name="restart">Restart</string> <string name="restart">Restart</string>
<string name="notifications_settings">Notifications settings</string> <string name="notifications_settings">Notifications settings</string>
<string name="notification_sound">Notification sound</string> <string name="notification_sound">Notification sound</string>
<string name="light_indicator">Light indicator</string> <string name="light_indicator">LED indicator</string>
<string name="vibration">Vibration</string> <string name="vibration">Vibration</string>
<string name="favourites_categories">Favourites categories</string> <string name="favourites_categories">Favourite categories</string>
<string name="categories_">Categories…</string> <string name="categories_">Categories…</string>
<string name="rename">Rename</string> <string name="rename">Rename</string>
<string name="category_delete_confirm">Do you really want to remove category \"%s\" from your favourites? \nAll containing manga will be lost.</string> <string name="category_delete_confirm">Remove the \"%s\" category from your favourites? \nAll manga in it will be lost.</string>
<string name="remove_category">Remove category</string> <string name="remove_category">Remove</string>
<string name="text_empty_holder_primary">It\'s kind of empty here…</string> <string name="text_empty_holder_primary">It\'s kind of empty here…</string>
<string name="text_categories_holder">You can use categories to organize your favourite manga. Press «+» to create a category</string> <string name="text_categories_holder">You can use categories to organize your favourites. Press «+» to create a category</string>
<string name="text_search_holder_secondary">Try to reformulate the query.</string> <string name="text_search_holder_secondary">Try to reformulate the query.</string>
<string name="text_history_holder_primary">Manga you are reading will be displayed here</string> <string name="text_history_holder_primary">What you read will be displayed here</string>
<string name="text_history_holder_secondary">You can find what to read in side menu.</string> <string name="text_history_holder_secondary">Find what to read in side menu.</string>
<string name="text_local_holder_primary">You have not any saved manga yet</string> <string name="text_local_holder_primary">Save something first</string>
<string name="text_local_holder_secondary">You can save it from online sources or import from file.</string> <string name="text_local_holder_secondary">Save it from online sources or import files.</string>
<string name="manga_shelf">Manga shelf</string> <string name="manga_shelf">Shelf</string>
<string name="recent_manga">Recent manga</string> <string name="recent_manga">Recent</string>
<string name="pages_animation">Pages animation</string> <string name="pages_animation">Page animation</string>
<string name="manga_save_location">Manga download location</string> <string name="manga_save_location">Folder for downloads</string>
<string name="not_available">Not available</string> <string name="not_available">Not available</string>
<string name="cannot_find_available_storage">Cannot find any available storage</string> <string name="cannot_find_available_storage">No available storage</string>
<string name="other_storage">Other storage</string> <string name="other_storage">Other storage</string>
<string name="use_ssl">Use secure connection (HTTPS)</string> <string name="use_ssl">Use secure (HTTPS) connection</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="all_favourites">All favourites</string> <string name="all_favourites">All favourites</string>
<string name="favourites_category_empty">This category is empty</string> <string name="favourites_category_empty">Empty category</string>
<string name="read_later">Read later</string> <string name="read_later">Read later</string>
<string name="updates">Updates</string> <string name="updates">Updates</string>
<string name="text_feed_holder">Here you will see the new chapters of the manga you are reading</string> <string name="text_feed_holder">New chapters of what you are reading is shown here</string>
<string name="search_results">Search results</string> <string name="search_results">Search results</string>
<string name="related">Related</string> <string name="related">Related</string>
<string name="new_version_s">New version: %s</string> <string name="new_version_s">New version: %s</string>
<string name="size_s">Size: %s</string> <string name="size_s">Size: %s</string>
<string name="waiting_for_network">Waiting for network…</string> <string name="waiting_for_network">Waiting for network…</string>
<string name="clear_updates_feed">Clear updates feed</string> <string name="clear_updates_feed">Clear updates feed</string>
<string name="updates_feed_cleared">Updates feed cleared</string> <string name="updates_feed_cleared">Cleared</string>
<string name="rotate_screen">Rotate screen</string> <string name="rotate_screen">Rotate screen</string>
<string name="update">Update</string> <string name="update">Update</string>
<string name="feed_will_update_soon">Feed update will start soon</string> <string name="feed_will_update_soon">Feed update will start soon</string>
<string name="track_sources">Check updates for manga</string> <string name="track_sources">Look for updates</string>
<string name="dont_check">Don`t check</string> <string name="dont_check">Don\'t check</string>
<string name="enter_password">Enter password</string> <string name="enter_password">Enter password</string>
<string name="wrong_password">Wrong password</string> <string name="wrong_password">Wrong password</string>
<string name="protect_application">Protect application</string> <string name="protect_application">Protect the app</string>
<string name="protect_application_summary">Ask for password on application start</string> <string name="protect_application_summary">Ask for password when starting Kotatsu</string>
<string name="repeat_password">Repeat password</string> <string name="repeat_password">Repeat the password</string>
<string name="passwords_mismatch">Passwords do not match</string> <string name="passwords_mismatch">Mismatching passwords</string>
<string name="about">About</string> <string name="about">About</string>
<string name="app_version">Version %s</string> <string name="app_version">Version %s</string>
<string name="check_for_updates">Check for updates</string> <string name="check_for_updates">Check for updates</string>
<string name="checking_for_updates">Checking for updates…</string> <string name="checking_for_updates">Checking for updates…</string>
<string name="update_check_failed">Update check failed</string> <string name="update_check_failed">Could not look for updates</string>
<string name="no_update_available">No updates available</string> <string name="no_update_available">No updates available</string>
<string name="right_to_left">Right to left</string> <string name="right_to_left">Right-to-left (←)</string>
<string name="prefer_rtl_reader">Prefer Right to left reader</string> <string name="prefer_rtl_reader">Prefer right-to-left (→) reader</string>
<string name="prefer_rtl_reader_summary">You can set up the reading mode for each manga separately</string> <string name="prefer_rtl_reader_summary">Reading mode can be set up separately for each series</string>
<string name="create_category">New category</string> <string name="create_category">New category</string>
<string name="report_github">Create issue on GitHub</string> <string name="report_github">Create issue on GitHub</string>
<string name="scale_mode">Scale mode</string> <string name="scale_mode">Scale mode</string>
@ -174,17 +174,17 @@
<string name="zoom_mode_fit_height">Fit to height</string> <string name="zoom_mode_fit_height">Fit to height</string>
<string name="zoom_mode_fit_width">Fit to width</string> <string name="zoom_mode_fit_width">Fit to width</string>
<string name="zoom_mode_keep_start">Keep at start</string> <string name="zoom_mode_keep_start">Keep at start</string>
<string name="black_dark_theme">Black dark theme</string> <string name="black_dark_theme">Black</string>
<string name="black_dark_theme_summary">Useful for AMOLED screens</string> <string name="black_dark_theme_summary">Uses less power on AMOLED screens</string>
<string name="restart_required">Restart required</string> <string name="restart_required">Restart required</string>
<string name="backup_restore"><![CDATA[Backup & Restore]]></string> <string name="backup_restore"><![CDATA[Backup and restore]]></string>
<string name="create_backup">Create data backup</string> <string name="create_backup">Create data backup</string>
<string name="restore_backup">Restore from backup</string> <string name="restore_backup">Restore from backup</string>
<string name="data_restored">Data restored</string> <string name="data_restored">Restored</string>
<string name="preparing_">Preparing…</string> <string name="preparing_">Preparing…</string>
<string name="file_not_found">File not found</string> <string name="file_not_found">File not found</string>
<string name="data_restored_success">All data restored successfully</string> <string name="data_restored_success">All data was restored</string>
<string name="data_restored_with_errors">The data restored, but there are errors</string> <string name="data_restored_with_errors">The data was restored, but there are errors</string>
<string name="backup_information">You can create backup of your history and favourites and restore it</string> <string name="backup_information">You can create backup of your history and favourites and restore it</string>
<string name="just_now">Just now</string> <string name="just_now">Just now</string>
<string name="yesterday">Yesterday</string> <string name="yesterday">Yesterday</string>
@ -192,61 +192,62 @@
<string name="group">Group</string> <string name="group">Group</string>
<string name="today">Today</string> <string name="today">Today</string>
<string name="tap_to_try_again">Tap to try again</string> <string name="tap_to_try_again">Tap to try again</string>
<string name="reader_mode_hint">Chosen configuration will be remembered for this manga</string> <string name="reader_mode_hint">The chosen configuration will be remembered for this manga</string>
<string name="silent">Silent</string> <string name="silent">Silent</string>
<string name="captcha_required">CAPTCHA is required</string> <string name="captcha_required">CAPTCHA required</string>
<string name="captcha_solve">Solve</string> <string name="captcha_solve">Solve</string>
<string name="clear_cookies">Clear cookies</string> <string name="clear_cookies">Clear cookies</string>
<string name="cookies_cleared">All cookies was removed</string> <string name="cookies_cleared">All cookies were removed</string>
<string name="chapters_checking_progress">Checking for new chapters: %1$d of %2$d</string> <string name="chapters_checking_progress">Checking for new chapters: %1$d of %2$d</string>
<string name="clear_feed">Clear feed</string> <string name="clear_feed">Clear feed</string>
<string name="text_clear_updates_feed_prompt">All updates history will be cleared and this action cannot be undone. Are you sure?</string> <string name="text_clear_updates_feed_prompt">Clear all update history permanently?</string>
<string name="new_chapters_checking">New chapters checking</string> <string name="new_chapters_checking">Looking for new chapters…</string>
<string name="reverse">Reverse</string> <string name="reverse">Reverse</string>
<string name="sign_in">Sign in</string> <string name="sign_in">Sign in</string>
<string name="auth_required">You should authorize to view this content</string> <string name="auth_required">Sign in to view this content</string>
<string name="default_s">Default: %s</string> <string name="default_s">Default: %s</string>
<string name="_and_x_more">…and %1$d more</string> <string name="_and_x_more">…and %1$d more</string>
<string name="next">Next</string> <string name="next">Next</string>
<string name="protect_application_subtitle">Enter password that will be required when the application starts</string> <string name="protect_application_subtitle">Enter a password to start the app with</string>
<string name="confirm">Confirm</string> <string name="confirm">Confirm</string>
<string name="password_length_hint">Password must be at least 4 characters</string> <string name="password_length_hint">The password must be 4 characters or more</string>
<string name="hide_toolbar">Hide toolbar when scrolling</string> <string name="hide_toolbar">Hide toolbar when scrolling</string>
<string name="search_only_on_s">Search only on %s</string> <string name="search_only_on_s">Search only on %s</string>
<string name="text_clear_search_history_prompt">Do you really want to remove all recent search queries? This action cannot be undone.</string> <string name="text_clear_search_history_prompt">Remove all recent search queries permanently?</string>
<string name="other">Other</string> <string name="other">Other</string>
<string name="languages">Languages</string> <string name="languages">Languages</string>
<string name="welcome">Welcome</string> <string name="welcome">Welcome</string>
<string name="backup_saved">Backup saved successfully</string> <string name="description">Description</string>
<string name="tracker_warning">Some manufacturers can change the system behavior, which may breaks background tasks.</string> <string name="backup_saved">Backup saved</string>
<string name="tracker_warning">Some devices have different system behavior, which may break background tasks.</string>
<string name="read_more">Read more</string> <string name="read_more">Read more</string>
<string name="queued">Queued</string> <string name="queued">Queued</string>
<string name="text_downloads_holder">There are currently no active downloads</string> <string name="text_downloads_holder">No active downloads</string>
<string name="chapter_is_missing_text">This chapter is missing on your device. Download or read it online.</string> <string name="chapter_is_missing_text">Download or read this missing chapter online.</string>
<string name="chapter_is_missing">Chapter is missing</string> <string name="chapter_is_missing">The chapter is missing</string>
<string name="about_app_translation_summary">Translate this app</string> <string name="about_app_translation_summary">Translate this app</string>
<string name="about_app_translation">Translation</string> <string name="about_app_translation">Translation</string>
<string name="about_author">Author</string> <string name="about_author">Author</string>
<string name="about_feedback">Feedback</string> <string name="about_feedback">Feedback</string>
<string name="about_feedback_4pda">Topic on 4PDA</string> <string name="about_feedback_4pda">Topic on 4PDA</string>
<string name="about_support_developer">Support the developer</string> <string name="about_support_developer">Support the developer</string>
<string name="about_support_developer_summary">If you like this app, you can help financially through Yoomoney (ex. Yandex.Money)</string> <string name="about_support_developer_summary">If you like this app, you can send money through Yoomoney (ex. Yandex.Money)</string>
<string name="about_gratitudes">Gratitudes</string> <string name="about_gratitudes">Gratitudes</string>
<string name="about_gratitudes_summary">These people make Kotatsu become better!</string> <string name="about_gratitudes_summary">These people all made Kotatsu better</string>
<string name="about_copyright_and_licenses">Copyright and Licenses</string> <string name="about_copyright_and_licenses">Copyright and Licenses</string>
<string name="about_license">License</string> <string name="about_license">License</string>
<string name="auth_complete">Authorization complete</string> <string name="auth_complete">Authorized</string>
<string name="auth_not_supported_by">Authorization on %s is not supported</string> <string name="auth_not_supported_by">Logging in on %s is not supported</string>
<string name="text_clear_cookies_prompt">You will be logged out from all sources that you are authorized in</string> <string name="text_clear_cookies_prompt">You will be logged out from all sources</string>
<string name="genres">Genres</string> <string name="genres">Genres</string>
<string name="state_finished">Finished</string> <string name="state_finished">Finished</string>
<string name="state_ongoing">Ongoing</string> <string name="state_ongoing">Ongoing</string>
<string name="date_format">Date format</string> <string name="date_format">Date format</string>
<string name="system_default">Default</string> <string name="system_default">Default</string>
<string name="exclude_nsfw_from_history">Exclude NSFW manga from history</string> <string name="exclude_nsfw_from_history">Exclude NSFW manga from history</string>
<string name="error_empty_name">Name should not be empty</string> <string name="error_empty_name">You must enter a name</string>
<string name="show_pages_numbers">Show pages numbers</string> <string name="show_pages_numbers">Numbered pages</string>
<string name="enabled_sources">Enabled sources</string> <string name="enabled_sources">Used sources</string>
<string name="available_sources">Available sources</string> <string name="available_sources">Available sources</string>
<string name="dynamic_theme">Dynamic theme</string> <string name="dynamic_theme">Dynamic theme</string>
<string name="dynamic_theme_summary">Applies a theme created on the color scheme of your wallpaper</string> <string name="dynamic_theme_summary">Applies a theme created on the color scheme of your wallpaper</string>

@ -31,7 +31,7 @@ class RemoteMangaRepositoryTest(private val source: MangaSource) : KoinTest {
@get:Rule @get:Rule
val koinTestRule = KoinTestRule.create { val koinTestRule = KoinTestRule.create {
printLogger(Level.ERROR) printLogger(Level.ERROR)
modules(repositoryTestModule) modules(repositoryTestModule, parserModule)
} }
@get:Rule @get:Rule

@ -7,7 +7,6 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.TestCookieJar import org.koitharu.kotatsu.core.network.TestCookieJar
import org.koitharu.kotatsu.core.network.UserAgentInterceptor import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.parser.SourceSettingsStub import org.koitharu.kotatsu.core.parser.SourceSettingsStub
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -31,12 +30,4 @@ val repositoryTestModule
} }
} }
} }
factory { (source: MangaSource) ->
runCatching {
source.cls.getDeclaredConstructor(MangaLoaderContext::class.java)
.newInstance(get<MangaLoaderContext>())
}.recoverCatching {
source.cls.newInstance()
}.getOrThrow() as RemoteMangaRepository
}
} }
Loading…
Cancel
Save