diff --git a/app/build.gradle b/app/build.gradle index c342da9ae..cc9dc482d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 31 - versionCode 378 - versionName '2.1.2' + versionCode 379 + versionName '2.1.3' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -59,6 +59,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { jvmTarget = JavaVersion.VERSION_1_8.toString() freeCompilerArgs += [ '-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', + '-Xopt-in=kotlinx.coroutines.FlowPreview', '-Xopt-in=kotlin.contracts.ExperimentalContracts', ] } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 74c093891..5d74551b7 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -7,5 +7,6 @@ public static void checkParameterIsNotNull(...); public static void checkNotNullParameter(...); } +-keep public class ** extends org.koitharu.kotatsu.base.ui.BaseFragment -keep class org.koitharu.kotatsu.core.db.entity.* { *; } -dontwarn okhttp3.internal.platform.ConscryptPlatform \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt index 83bd5dc25..d9f845c65 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/MangaUtils.kt @@ -11,6 +11,7 @@ import org.koin.core.component.get import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.model.MangaPage 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.ext.await import org.koitharu.kotatsu.utils.ext.medianOrNull @@ -28,7 +29,7 @@ object MangaUtils : KoinComponent { suspend fun determineMangaIsWebtoon(pages: List): Boolean? { try { 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 size = if (uri.scheme == "cbz") { val zip = ZipFile(uri.schemeSpecificPart) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index 4ef49374c..a9fc84da5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -2,51 +2,38 @@ package org.koitharu.kotatsu.core.model import android.os.Parcelable 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") @Parcelize enum class MangaSource( val title: String, val locale: String?, - val cls: Class, ) : Parcelable { - LOCAL("Local", null, LocalMangaRepository::class.java), - READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java), - MINTMANGA("MintManga", "ru", MintMangaRepository::class.java), - SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java), - MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java), - DESUME("Desu.me", "ru", DesuMeRepository::class.java), - HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java), - YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java), - MANGATOWN("MangaTown", "en", MangaTownRepository::class.java), - MANGALIB("MangaLib", "ru", MangaLibRepository::class.java), + LOCAL("Local", null), + READMANGA_RU("ReadManga", "ru"), + MINTMANGA("MintManga", "ru"), + SELFMANGA("SelfManga", "ru"), + MANGACHAN("Манга-тян", "ru"), + DESUME("Desu.me", "ru"), + HENCHAN("Хентай-тян", "ru"), + YAOICHAN("Яой-тян", "ru"), + MANGATOWN("MangaTown", "en"), + MANGALIB("MangaLib", "ru"), // NUDEMOON("Nude-Moon", "ru", NudeMoonRepository::class.java), - MANGAREAD("MangaRead", "en", MangareadRepository::class.java), - REMANGA("Remanga", "ru", RemangaRepository::class.java), - HENTAILIB("HentaiLib", "ru", HentaiLibRepository::class.java), - ANIBEL("Anibel", "be", AnibelRepository::class.java), - NINEMANGA_EN("NineManga English", "en", NineMangaRepository.English::class.java), - NINEMANGA_ES("NineManga Español", "es", NineMangaRepository.Spanish::class.java), - NINEMANGA_RU("NineManga Русский", "ru", NineMangaRepository.Russian::class.java), - NINEMANGA_DE("NineManga Deutsch", "de", NineMangaRepository.Deutsch::class.java), - NINEMANGA_IT("NineManga Italiano", "it", NineMangaRepository.Italiano::class.java), - NINEMANGA_BR("NineManga Brasil", "pt", NineMangaRepository.Brazil::class.java), - NINEMANGA_FR("NineManga Français", "fr", NineMangaRepository.Francais::class.java), - EXHENTAI("ExHentai", null, ExHentaiRepository::class.java), - MANGAOWL("MangaOwl", "en", MangaOwlRepository::class.java), - MANGADEX("MangaDex", null, MangaDexRepository::class.java), + MANGAREAD("MangaRead", "en"), + REMANGA("Remanga", "ru"), + HENTAILIB("HentaiLib", "ru"), + ANIBEL("Anibel", "be"), + NINEMANGA_EN("NineManga English", "en"), + NINEMANGA_ES("NineManga Español", "es"), + NINEMANGA_RU("NineManga Русский", "ru"), + NINEMANGA_DE("NineManga Deutsch", "de"), + NINEMANGA_IT("NineManga Italiano", "it"), + NINEMANGA_BR("NineManga Brasil", "pt"), + NINEMANGA_FR("NineManga Français", "fr"), + EXHENTAI("ExHentai", null), + MANGAOWL("MangaOwl", "en"), + 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)) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt index 6c41a5291..5627e8637 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/network/NetworkModule.kt @@ -7,6 +7,7 @@ import org.koin.core.qualifier.named import org.koin.dsl.bind import org.koin.dsl.module import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.DownloadManagerHelper import java.util.concurrent.TimeUnit @@ -30,4 +31,5 @@ val networkModule }.build() } factory { DownloadManagerHelper(get(), get()) } + single { MangaLoaderContext(get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt new file mode 100644 index 000000000..44f1ca040 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt @@ -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 { + + 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" +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt index c8904b2a8..45103954f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -7,6 +7,8 @@ import org.koitharu.kotatsu.core.model.* interface MangaRepository { + val source: MangaSource + val sortOrders: Set suspend fun getList2( diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt index c97f7a4eb..944bd1ac6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/ParserModule.kt @@ -2,15 +2,12 @@ package org.koitharu.kotatsu.core.parser import org.koin.core.qualifier.named import org.koin.dsl.module -import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.parser.site.* val parserModule get() = module { - single { MangaLoaderContext(get(), get()) } - factory(named(MangaSource.READMANGA_RU)) { ReadmangaRepository(get()) } factory(named(MangaSource.MINTMANGA)) { MintMangaRepository(get()) } factory(named(MangaSource.SELFMANGA)) { SelfMangaRepository(get()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index ef4c55817..3505c53b8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.parser import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.exceptions.ParseException 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.SortOrder import org.koitharu.kotatsu.core.prefs.SourceSettings @@ -12,8 +11,6 @@ abstract class RemoteMangaRepository( protected val loaderContext: MangaLoaderContext ) : MangaRepository { - protected abstract val source: MangaSource - protected abstract val defaultDomain: String private val conf by lazy { @@ -29,6 +26,8 @@ abstract class RemoteMangaRepository( override suspend fun getTags(): Set = emptySet() + open fun getFaviconUrl() = "https://${getDomain()}/favicon.ico" + open fun onCreatePreferences(map: MutableMap) { map[SourceSettings.KEY_DOMAIN] = defaultDomain } @@ -53,8 +52,10 @@ abstract class RemoteMangaRepository( if (subdomain != null) { append(subdomain) append('.') + append(conf.getDomain(defaultDomain).removePrefix("www.")) + } else { + append(conf.getDomain(defaultDomain)) } - append(conf.getDomain(defaultDomain)) append(this@withDomain) } else -> this diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt index 03853cd1f..e71378eec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/AnibelRepository.kt @@ -21,6 +21,10 @@ class AnibelRepository(loaderContext: MangaLoaderContext) : RemoteMangaRepositor SortOrder.NEWEST ) + override fun getFaviconUrl(): String { + return "https://cdn.${getDomain()}/favicons/favicon.png" + } + override suspend fun getList2( offset: Int, query: String?, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt index 7f1285369..afe3750c3 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt @@ -128,7 +128,7 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : scanlator = null, branch = null, ) - } + } ?: bypassLicensedChapters(manga) ) } @@ -191,6 +191,32 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : map[SourceSettings.KEY_USE_SSL] = true } + private suspend fun bypassLicensedChapters(manga: Manga): List { + 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 companion object { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt index 7999fb7f2..2b2f9b8cd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangareadRepository.kt @@ -151,8 +151,10 @@ class MangareadRepository( ?.selectFirst("div.reading-content") ?: throw ParseException("Root not found") return root.select("div.page-break").map { div -> - val img = div.selectFirst("img") - val url = img?.relUrl("src") ?: parseFailed("Page image not found") + val img = div.selectFirst("img") ?: parseFailed("Page image not found") + val url = img.relUrl("data-src").ifEmpty { + img.relUrl("src") + } MangaPage( id = generateUid(url), url = url, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt index 8032d9783..148fa409d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt @@ -5,6 +5,7 @@ import coil.ImageLoader import okhttp3.OkHttpClient import org.koin.android.ext.koin.androidContext import org.koin.dsl.module +import org.koitharu.kotatsu.core.parser.FaviconMapper import org.koitharu.kotatsu.local.data.CbzFetcher val uiModule @@ -15,6 +16,7 @@ val uiModule .componentRegistry( ComponentRegistry.Builder() .add(CbzFetcher()) + .add(FaviconMapper()) .build() ).build() } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index 8b7b241e6..3f8ee5a8f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -123,7 +123,7 @@ class DetailsViewModel( var manga = mangaDataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") mangaData.value = manga - manga = manga.source.repository.getDetails(manga) + manga = MangaRepository(manga.source).getDetails(manga) // find default branch val hist = historyRepository.getOne(manga) selectedBranch.value = if (hist != null) { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt index 75905aa51..1a36ec7fc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -145,7 +145,7 @@ class DownloadManager( while (true) { try { val response = call.clone().await() - withContext(Dispatchers.IO) { + runInterruptible(Dispatchers.IO) { file.outputStream().use { out -> checkNotNull(response.body).byteStream().copyTo(out) } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt index cd4f5ea83..4e2746cec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/CbzFetcher.kt @@ -9,23 +9,24 @@ import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.fetch.SourceResult import coil.size.Size +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible import okio.buffer import okio.source import java.util.zip.ZipFile class CbzFetcher : Fetcher { - @Suppress("BlockingMethodInNonBlockingContext") override suspend fun fetch( pool: BitmapPool, data: Uri, size: Size, options: Options, - ): FetchResult { + ): FetchResult = runInterruptible(Dispatchers.IO) { val zip = ZipFile(data.schemeSpecificPart) val entry = zip.getEntry(data.fragment) val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name) - return SourceResult( + SourceResult( source = ExtraCloseableBufferedSource( zip.getInputStream(entry).source().buffer(), zip, diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaZip.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaZip.kt index 2904910d6..c9d93f147 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/MangaZip.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/MangaZip.kt @@ -31,7 +31,7 @@ class MangaZip(val file: File) { return writableCbz.flush() } - fun addCover(file: File, ext: String) { + suspend fun addCover(file: File, ext: String) { val name = buildString { append(FILENAME_PATTERN.format(0, 0)) if (ext.isNotEmpty() && ext.length <= 4) { @@ -39,11 +39,11 @@ class MangaZip(val file: File) { append(ext) } } - writableCbz[name] = file + writableCbz.put(name, file) 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 { append(FILENAME_PATTERN.format(chapter.number, pageNumber)) if (ext.isNotEmpty() && ext.length <= 4) { @@ -51,7 +51,7 @@ class MangaZip(val file: File) { append(ext) } } - writableCbz[name] = file + writableCbz.put(name, file) index.addChapter(chapter) } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt index 5a591740f..b7c5f7b9f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/data/WritableCbzFile.kt @@ -1,8 +1,7 @@ package org.koitharu.kotatsu.local.data import androidx.annotation.CheckResult -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -27,11 +26,13 @@ class WritableCbzFile(private val file: File) { } ZipInputStream(FileInputStream(file)).use { zip -> var entry = zip.nextEntry - while (entry != null) { + while (entry != null && currentCoroutineContext().isActive) { val target = File(dir.path + File.separator + entry.name) - target.parentFile?.mkdirs() - target.outputStream().use { out -> - zip.copyTo(out) + runInterruptible { + target.parentFile?.mkdirs() + target.outputStream().use { out -> + zip.copyTo(out) + } } zip.closeEntry() entry = zip.nextEntry @@ -51,11 +52,13 @@ class WritableCbzFile(private val file: File) { tempFile.delete() } try { - ZipOutputStream(FileOutputStream(tempFile)).use { zip -> - dir.listFiles()?.forEach { - zipFile(it, it.name, zip) + runInterruptible { + ZipOutputStream(FileOutputStream(tempFile)).use { zip -> + dir.listFiles()?.forEach { + zipFile(it, it.name, zip) + } + zip.flush() } - zip.flush() } tempFile.renameTo(file) } finally { @@ -67,29 +70,26 @@ class WritableCbzFile(private val file: File) { 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) } - companion object { - - private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) { - if (fileToZip.isDirectory) { - if (fileName.endsWith("/")) { - zipOut.putNextEntry(ZipEntry(fileName)) - } else { - zipOut.putNextEntry(ZipEntry("$fileName/")) - } - zipOut.closeEntry() - fileToZip.listFiles()?.forEach { childFile -> - zipFile(childFile, "$fileName/${childFile.name}", zipOut) - } + private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) { + if (fileToZip.isDirectory) { + if (fileName.endsWith("/")) { + zipOut.putNextEntry(ZipEntry(fileName)) } else { - FileInputStream(fileToZip).use { fis -> - val zipEntry = ZipEntry(fileName) - zipOut.putNextEntry(zipEntry) - fis.copyTo(zipOut) - } + zipOut.putNextEntry(ZipEntry("$fileName/")) + } + zipOut.closeEntry() + 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) } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index 3742f007e..cedfe10a9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -23,6 +23,7 @@ import java.util.zip.ZipFile class LocalMangaRepository(private val context: Context) : MangaRepository { + override val source = MangaSource.LOCAL private val filenameFilter = CbzFilter() override suspend fun getList2( diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 38cfe450e..8e4e8316d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -50,7 +50,7 @@ class PageLoader( private fun loadAsync(page: MangaPage): Deferred { var repo = repository - if (repo?.javaClass != page.source.cls) { + if (repo?.source != page.source) { repo = mangaRepositoryOf(page.source) repository = repo } diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index cb4a0e38e..fa6203f55 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.reader.ui -import android.content.ContentResolver import android.net.Uri import android.util.LongSparseArray import androidx.lifecycle.MutableLiveData @@ -77,7 +76,7 @@ class ReaderViewModel( var manga = dataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") mangaData.value = manga - val repo = manga.source.repository + val repo = MangaRepository(manga.source) manga = repo.getDetails(manga) manga.chapters?.forEach { chapters.put(it.id, it) @@ -206,7 +205,7 @@ class ReaderViewModel( private suspend fun loadChapter(chapterId: Long): List { val manga = checkNotNull(mangaData.value) { "Manga is null" } 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 -> ReaderPage.from(page, index, chapterId) } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index d099f54c5..efb736d7b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -14,6 +14,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaSource 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.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.utils.ext.levenshteinDistance @@ -29,7 +30,7 @@ class MangaSearchRepository( MangaProviderFactory.getSources(settings, includeHidden = false).asFlow() .flatMapMerge(concurrency) { source -> runCatching { - source.repository.getList2( + MangaRepository(source).getList2( offset = 0, query = query, sortOrder = SortOrder.POPULARITY diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index 25893f670..5cbb3ac41 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt @@ -1,13 +1,13 @@ package org.koitharu.kotatsu.settings.sources import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import android.view.* +import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets import androidx.core.view.updatePadding import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R 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 class SourcesSettingsFragment : BaseFragment(), - SourceConfigListener { + SourceConfigListener, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener { private lateinit var reorderHelper: ItemTouchHelper private val viewModel by viewModel() @@ -42,7 +42,7 @@ class SourcesSettingsFragment : BaseFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val sourcesAdapter = SourceConfigAdapter(this) + val sourcesAdapter = SourceConfigAdapter(this, get(), viewLifecycleOwner) with(binding.recyclerView) { setHasFixedSize(true) addItemDecoration(SourceConfigItemDecoration(view.context)) @@ -59,6 +59,17 @@ class SourcesSettingsFragment : BaseFragment(), 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) { binding.recyclerView.updatePadding( bottom = insets.bottom, @@ -83,6 +94,20 @@ class SourcesSettingsFragment : BaseFragment(), 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( ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0, diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt index 52125df63..a908ccf4e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsViewModel.kt @@ -21,6 +21,7 @@ class SourcesSettingsViewModel( val items = MutableLiveData>(emptyList()) private val expandedGroups = HashSet() + private var searchQuery: String? = null init { buildList() @@ -63,9 +64,30 @@ class SourcesSettingsViewModel( buildList() } + fun performSearch(query: String?) { + searchQuery = query?.trim() + buildList() + } + private fun buildList() { val sources = MangaProviderFactory.getSources(settings, includeHidden = true) 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())) { if (it.name !in hiddenSources) { KEY_ENABLED @@ -81,6 +103,7 @@ class SourcesSettingsViewModel( SourceConfigItem.SourceItem( source = it, isEnabled = true, + isDraggable = true, ) } } @@ -102,6 +125,7 @@ class SourcesSettingsViewModel( SourceConfigItem.SourceItem( source = it, isEnabled = false, + isDraggable = false, ) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt index d04d22fcc..d580684be 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapter.kt @@ -1,13 +1,19 @@ package org.koitharu.kotatsu.settings.sources.adapter +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem class SourceConfigAdapter( listener: SourceConfigListener, + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, ) : AsyncListDifferDelegationAdapter( SourceConfigDiffCallback(), sourceConfigHeaderDelegate(), sourceConfigGroupDelegate(listener), - sourceConfigItemDelegate(listener), + sourceConfigItemDelegate(listener, coil, lifecycleOwner), + sourceConfigDraggableItemDelegate(listener), + sourceConfigEmptySearchDelegate(), ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt index df7435bac..aa8c9fb35 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt @@ -4,14 +4,19 @@ import android.annotation.SuppressLint import android.view.MotionEvent import android.view.View import android.widget.CompoundButton -import androidx.core.view.isVisible -import androidx.core.view.updatePaddingRelative +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import coil.request.Disposable +import coil.request.ImageRequest +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.databinding.ItemExpandableBinding import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding +import org.koitharu.kotatsu.databinding.ItemSourceConfigDraggableBinding import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem +import org.koitharu.kotatsu.utils.ext.enqueueWith fun sourceConfigHeaderDelegate() = adapterDelegateViewBinding( { layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) } @@ -38,11 +43,44 @@ fun sourceConfigGroupDelegate( } } -@SuppressLint("ClickableViewAccessibility") fun sourceConfigItemDelegate( listener: SourceConfigListener, + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, ) = adapterDelegateViewBinding( - { 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( + { layoutInflater, parent -> ItemSourceConfigDraggableBinding.inflate(layoutInflater, parent, false) }, + on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable } ) { val eventListener = object : View.OnClickListener, View.OnTouchListener, @@ -70,11 +108,9 @@ fun sourceConfigItemDelegate( bind { binding.textViewTitle.text = item.source.title 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, - ) } -} \ No newline at end of file +} + +fun sourceConfigEmptySearchDelegate() = adapterDelegate( + R.layout.item_sources_empty +) { } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt index 370cca88d..8bab50c2a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigDiffCallback.kt @@ -2,21 +2,25 @@ package org.koitharu.kotatsu.settings.sources.adapter import androidx.recyclerview.widget.DiffUtil import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem +import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem.* class SourceConfigDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean { return when { oldItem.javaClass != newItem.javaClass -> false - oldItem is SourceConfigItem.LocaleGroup && newItem is SourceConfigItem.LocaleGroup -> { + oldItem is LocaleGroup && newItem is LocaleGroup -> { oldItem.localeId == newItem.localeId } - oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> { + oldItem is SourceItem && newItem is SourceItem -> { oldItem.source == newItem.source } - oldItem is SourceConfigItem.Header && newItem is SourceConfigItem.Header -> { + oldItem is Header && newItem is Header -> { oldItem.titleResId == newItem.titleResId } + oldItem == EmptySearchResult && newItem == EmptySearchResult -> { + true + } else -> false } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt index 965ea1171..dd998ddac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.settings.sources.model +import android.net.Uri import androidx.annotation.StringRes import org.koitharu.kotatsu.core.model.MangaSource @@ -49,8 +50,12 @@ sealed interface SourceConfigItem { class SourceItem( val source: MangaSource, val isEnabled: Boolean, + val isDraggable: Boolean, ) : SourceConfigItem { + val faviconUrl: Uri + get() = Uri.fromParts("favicon", source.name, null) + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -59,6 +64,7 @@ sealed interface SourceConfigItem { if (source != other.source) return false if (isEnabled != other.isEnabled) return false + if (isDraggable != other.isDraggable) return false return true } @@ -66,7 +72,10 @@ sealed interface SourceConfigItem { override fun hashCode(): Int { var result = source.hashCode() result = 31 * result + isEnabled.hashCode() + result = 31 * result + isDraggable.hashCode() return result } } + + object EmptySearchResult : SourceConfigItem } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/PendingIntentCompat.kt b/app/src/main/java/org/koitharu/kotatsu/utils/PendingIntentCompat.kt index 4169a571d..c99050819 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/PendingIntentCompat.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/PendingIntentCompat.kt @@ -10,4 +10,10 @@ object PendingIntentCompat { } else { 0 } + + val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE + } else { + 0 + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt b/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt index 63a2816ae..a5c6bb748 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt @@ -31,7 +31,7 @@ class RecentWidgetProvider : AppWidgetProvider() { context, 0, 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) diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt index 334941d51..7b3ba2059 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt @@ -31,7 +31,7 @@ class ShelfWidgetProvider : AppWidgetProvider() { context, 0, 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) diff --git a/app/src/main/res/drawable/ic_favicon_fallback.xml b/app/src/main/res/drawable/ic_favicon_fallback.xml new file mode 100644 index 000000000..24996b554 --- /dev/null +++ b/app/src/main/res/drawable/ic_favicon_fallback.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_source_config.xml b/app/src/main/res/layout/item_source_config.xml index efe0fbbf0..a0620433a 100644 --- a/app/src/main/res/layout/item_source_config.xml +++ b/app/src/main/res/layout/item_source_config.xml @@ -4,17 +4,18 @@ 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"> + android:id="@+id/imageView_icon" + android:layout_width="?android:listPreferredItemHeightSmall" + android:layout_height="?android:listPreferredItemHeightSmall" + android:layout_marginHorizontal="?listPreferredItemPaddingStart" + android:labelFor="@id/textView_title" + android:padding="8dp" + android:scaleType="fitCenter" + tools:src="@tools:sample/avatars" /> - - + android:layout_height="wrap_content" + android:layout_marginEnd="?listPreferredItemPaddingEnd" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_source_config_draggable.xml b/app/src/main/res/layout/item_source_config_draggable.xml new file mode 100644 index 000000000..ffa9a68e5 --- /dev/null +++ b/app/src/main/res/layout/item_source_config_draggable.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_sources_empty.xml b/app/src/main/res/layout/item_sources_empty.xml new file mode 100644 index 000000000..3aad1bbe4 --- /dev/null +++ b/app/src/main/res/layout/item_sources_empty.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_sources.xml b/app/src/main/res/menu/opt_sources.xml index 35c7034be..5128fbe66 100644 --- a/app/src/main/res/menu/opt_sources.xml +++ b/app/src/main/res/menu/opt_sources.xml @@ -4,9 +4,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + android:id="@+id/action_search" + android:icon="@drawable/ic_search" + android:title="@string/search" + app:actionViewClass="androidx.appcompat.widget.SearchView" + app:showAsAction="ifRoom|collapseActionView" /> \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 505eb3643..fb9e9af25 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -246,4 +246,7 @@ Па змаўчанні Не паказваць NSFW мангу з гісторыі Імя не можа быць пустым + Паказваць нумары старонак + Уключаныя крыніцы + Даступныя крыніцы \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index cb4c26f34..5deb17d71 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -242,8 +242,11 @@ Formato de la fecha Por defecto Algunos fabricantes pueden cambiar el comportamiento del sistema, lo que podría interrumpir las tareas en segundo plano. - Nombre no debe estar vacío + El nombre no debe estar vacío Autorización en %s no es compatible Se cerrará la sesión de todas las fuentes en las que esté autorizado Excluye manga NSFW del historial + Mostrar los números de páginas + Fuentes activadas + Fuentes disponibles \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml new file mode 100644 index 000000000..54bac4ed8 --- /dev/null +++ b/app/src/main/res/values-fa/strings.xml @@ -0,0 +1,10 @@ + + + بستن منو + بازکردن منو + محل ذخیره سازی + موارد دلخواه + تاریخچه + خطایی رخ داده است + خطای اتصال به شبکه + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a6b3daec9..17fe50b16 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,2 +1,252 @@ - \ No newline at end of file + + Abrir menu + Armazenamento local + Favoritos + Ocorreu um erro + Erro de conexão de rede + Detalhes + Lista + Lista detalhada + Grade + Modo lista + Configurações + Carregando… + Capítulo %1$d de %2$d + Tente novamente + Limpar histórico + Nada encontrado + Histórico vazio + Add marca páginas + Você não tem favoritos ainda + Adicionar aos favoritos + Add + Nomeie a categoria + Salvar + Compartilhar + Criar atalho… + Compartilhar %s + Pesquisar + Pesquisar mangá + Baixando mangá… + Download completo + Downloads + Por nome + Populares + Por avaliação + Todos + Ordem de classificação + Gênero + Filtro + Escuro + Automático + Páginas + Limpar + Você realmente quer limpar todo o seu histórico de leitura\? Essa ação não pode ser desfeita. + Remover + \"%s\" removido do histórico + \"%s\" deletado do armazenamento local + Aguarde o carregamento para finalizar + Salvar página + Página salva com sucesso + Compartilhar imagem + Importar + Utualizado + Deletar + Essa operação não é suportada + Histórico e cache + Limpar cache de páginas + Cache + B|kB|MB|GB|TB + Padrão + Webtoon + Modo leitura + Tamanho de grade + Pesquisar em %s + Deletar mangá + Configurações de leitura + Mudar páginas + Cliques na borda + Botões de volume + Essa operação pode consumir muito tráfego de rede + Não pergunte novamente + Cancelando… + Erro + Limpar cache de thumbnails + Histórico de pesquisa limpo + Apenas gestos + Armazenamento interno + Armazenamento externo + Domínio + Verificar automaticamente se há actualizações + Actualização da aplicação está disponível + Mostrar notificação se a actualização estiver disponível + Aberto no navegador + Esta manga tem %s. Quer salvar tudo isto\? + Salvar mangá + Notificações + Novos capítulos + Notifique sobre atualizações do mangá que está lendo + Download + Ler desde o início + Reiniciar + Configurações das notificações + Indicador luminoso + Fechar menu + Fontes remotas + Fechar + Brilho + Histórico + Ler + Processando… + Novos + Tema + Sem descrição + Continuar + Capítulos + Add nova categoria + Aviso + Você realmente quer deletar \"%s\" do armazenamento local de seu celular\? +\nEssa operação não pode ser desfeita. + Arquivo inválido. Apenas ZIP e CBZ são suportados. + Limpar histórico de pesquisa + Activado %1$d de %2$d + Som de notificação + Mostrar números de páginas + Concluído + em andamento + Categorias… + Renomear + Remover categoria + Está meio vazio aqui… + Prateleira de manga + Feito + Relacionado + Manter no início + Limpar feed de atualizações + Feed de atualizações limpo + Atualizar + A atualização do feed começará em breve + Confira as atualizações do mangá + Não verifique + Digite a senha + Senha incorreta + Repita a senha + As senhas não coincidem + Cerca de + Versão %s + Verifique se há atualizações + Verificando atualizações… + Nenhuma atualização disponível + Direita para esquerda + Nova categoria + Criar problema no GitHub + Modo de escala + Centro de ajuste + Ajustar à largura + É necessário reiniciar + Restauração de backup + Criar backup de dados + Restaurar do backup + Dados restaurados + Preparando… + Arquivo não encontrado + Todos os dados restaurados com sucesso + Os dados foram restaurados, mas há erros + Agora mesmo + Ontem + Muito tempo atrás + Grupo + Hoje + Toque para tentar novamente + Silencioso + O CAPTCHA é obrigatório + Resolver + Todos os cookies foram removidos + Verificando novos capítulos: %1$d de %2$d + Limpar feed + Todo o histórico de atualizações será apagado e esta ação não poderá ser desfeita. Tem certeza\? + Verificação de novos capítulos + Reverter + Entrar + Padrão: %s + …e %1$d mais + Próximo + Digite a senha que será necessária quando o aplicativo for iniciado + Confirme + A senha deve ter pelo menos 4 caracteres + Descrição + Cópias de segurança salvas com sucesso + Alguns fabricantes podem alterar o comportamento do sistema, o que pode quebrar as tarefas de fundo. + Leia mais + Ocultar a barra de ferramentas ao rolar + Pesquise apenas em %s + Outros + Línguas + Bem vindo + Fontes disponíveis + Fontes ativadas + Enfileirado + No momento, não há downloads ativos + O nome não deve estar vazio + Traduzir esta aplicação + Autor + Comentar + Tópico no 4PDA + Apoiar o desenvolvedor + Se gostar desta aplicação, pode ajudar financeiramente através de Yoomoney (ex. Yandex.Money) + agradecimento + Estas pessoas fazem o Kotatsu tornar-se melhor! + Direitos de autor e licenças + Licença + Este capítulo está em falta no seu dispositivo. Descarregue ou leia-o online. + Falta um capítulo + Autorização completa + A autorização em %s não é suportada + Géneros + Tradução + Será desconectado de todas as fontes em que estiver autorizado + Vibração + Não é possível encontrar nenhum armazenamento disponível + Categorias favoritas + Quer realmente remover a categoria \"%s\" dos seus favoritos\? +\nSerá perdido todos os mangas contidos. + Pode encontrar o que ler no menu lateral. + Pode salvá-lo a partir de fontes online ou importá-lo a partir de ficheiro. + Manga recente + Outro armazenamento + Tente reformular a consulta. + Prefira o leitor da direita para a esquerda + Não disponível + Tamanho: %s + Mangá que está a ler será afixada aqui + Ainda não tem nenhuma mangá salvo + Animação de páginas + Esta categoria está vazia + Leia mais tarde + atualizações + Todos os favoritos + À espera de rede… + Utilizar ligação segura (HTTPS) + Resultados da pesquisa + Aqui verá os novos capítulos do mangá que está a ler + Nova versão: %s + Girar a tela + Falha na verificação de atualização + Proteger aplicativo + Pedir senha no início do aplicativo + Ajustar à altura + Tema Black dark + Útil para telas AMOLED + A configuração escolhida será lembrada para este mangá + Você pode criar backup de seu histórico e favoritos e restaurá-lo + Limpar cookies + Você realmente deseja remover todas as consultas de pesquisa recentes\? Essa ação não pode ser desfeita. + Você deve autorizar a visualização deste conteúdo + Pode usar categorias para organizar seu mangá favorito. Pressione <<+>> para criar uma categoria + Você pode configurar o modo de leitura para cada mangá separadamente + Local onde serão armazenados os mangás baixados + Excluir manga NSFW da história + Formato da data + Padrão + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d909289b..4ef71fe27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,8 +7,8 @@ Local storage Favourites History - An error has occurred - Network connection error + An error occurred + Could not connect to the Internet Details Chapters List @@ -23,12 +23,12 @@ Try again Clear history Nothing found - History is empty + No history yet Read Add bookmark - You have not favourites yet - Add to favourites - Add new category + No favourites yet + Favourite this + New category Add Enter category name Save @@ -37,40 +37,40 @@ Share %s Search Search manga - Manga downloading… + Downloading… Processing… - Download complete + Downloaded Downloads - By name + Name Popular Updated Newest - By rating + Rating All - Sort order + Sorting order Genre Filter Theme Light Dark - Automatic + Follow system Pages Clear - Do you really want to clear all your reading history? This action cannot be undone. + Clear all reading history permanently? Remove \"%s\" removed from history \"%s\" deleted from local storage - Wait for the load to finish + Wait for loading to finish… Save page - Page saved successful + Saved Share image Import Delete This operation is not supported - Invalid file. Only ZIP and CBZ are supported. + Either pick a ZIP or CBZ file. No description History and cache - Clear pages cache + Clear page cache Cache B|kB|MB|GB|TB Standard @@ -79,32 +79,32 @@ Grid size Search on %s Delete manga - Do you really want to delete \"%s\" from your phone\'s local storage? \nThis operation cannot be undone. + Delete \"%s\" from device permanently? Reader settings Switch pages - Taps on edges + Edge taps Volume buttons Continue Warning - This operation may consume a lot of network traffic + This may transfer a lot of data Don\'t ask again Cancelling… Error Clear thumbnails cache Clear search history - Search history cleared + Cleared Gestures only Internal storage External storage Domain - Check for updates automatically - Application update is available - Show notification if update is available - Open in browser - This manga has %s. Do you want to save all of it? - Save manga + Check for new versions of the app + A new version of the app is available + Show notification if a new version is available + Open in web browser + This manga has %s. Save all of it? + Save Notifications - Enabled %1$d of %2$d + Turned on %1$d of %2$d New chapters Notify about updates of manga you are reading Download @@ -112,61 +112,61 @@ Restart Notifications settings Notification sound - Light indicator + LED indicator Vibration - Favourites categories + Favourite categories Categories… Rename - Do you really want to remove category \"%s\" from your favourites? \nAll containing manga will be lost. - Remove category + Remove the \"%s\" category from your favourites? \nAll manga in it will be lost. + Remove It\'s kind of empty here… - You can use categories to organize your favourite manga. Press «+» to create a category + You can use categories to organize your favourites. Press «+» to create a category Try to reformulate the query. - Manga you are reading will be displayed here - You can find what to read in side menu. - You have not any saved manga yet - You can save it from online sources or import from file. - Manga shelf - Recent manga - Pages animation - Manga download location + What you read will be displayed here + Find what to read in side menu. + Save something first + Save it from online sources or import files. + Shelf + Recent + Page animation + Folder for downloads Not available - Cannot find any available storage + No available storage Other storage - Use secure connection (HTTPS) + Use secure (HTTPS) connection Done All favourites - This category is empty + Empty category Read later Updates - Here you will see the new chapters of the manga you are reading + New chapters of what you are reading is shown here Search results Related New version: %s Size: %s Waiting for network… Clear updates feed - Updates feed cleared + Cleared Rotate screen Update Feed update will start soon - Check updates for manga - Don`t check + Look for updates + Don\'t check Enter password Wrong password - Protect application - Ask for password on application start - Repeat password - Passwords do not match + Protect the app + Ask for password when starting Kotatsu + Repeat the password + Mismatching passwords About Version %s Check for updates Checking for updates… - Update check failed + Could not look for updates No updates available - Right to left - Prefer Right to left reader - You can set up the reading mode for each manga separately + Right-to-left (←) + Prefer right-to-left (→) reader + Reading mode can be set up separately for each series New category Create issue on GitHub Scale mode @@ -174,17 +174,17 @@ Fit to height Fit to width Keep at start - Black dark theme - Useful for AMOLED screens + Black + Uses less power on AMOLED screens Restart required - + Create data backup Restore from backup - Data restored + Restored Preparing… File not found - All data restored successfully - The data restored, but there are errors + All data was restored + The data was restored, but there are errors You can create backup of your history and favourites and restore it Just now Yesterday @@ -192,61 +192,62 @@ Group Today Tap to try again - Chosen configuration will be remembered for this manga + The chosen configuration will be remembered for this manga Silent - CAPTCHA is required + CAPTCHA required Solve Clear cookies - All cookies was removed + All cookies were removed Checking for new chapters: %1$d of %2$d Clear feed - All updates history will be cleared and this action cannot be undone. Are you sure? - New chapters checking + Clear all update history permanently? + Looking for new chapters… Reverse Sign in - You should authorize to view this content + Sign in to view this content Default: %s …and %1$d more Next - Enter password that will be required when the application starts + Enter a password to start the app with Confirm - Password must be at least 4 characters + The password must be 4 characters or more Hide toolbar when scrolling Search only on %s - Do you really want to remove all recent search queries? This action cannot be undone. + Remove all recent search queries permanently? Other Languages Welcome - Backup saved successfully - Some manufacturers can change the system behavior, which may breaks background tasks. + Description + Backup saved + Some devices have different system behavior, which may break background tasks. Read more Queued - There are currently no active downloads - This chapter is missing on your device. Download or read it online. - Chapter is missing + No active downloads + Download or read this missing chapter online. + The chapter is missing Translate this app Translation Author Feedback Topic on 4PDA Support the developer - If you like this app, you can help financially through Yoomoney (ex. Yandex.Money) + If you like this app, you can send money through Yoomoney (ex. Yandex.Money) Gratitudes - These people make Kotatsu become better! + These people all made Kotatsu better Copyright and Licenses License - Authorization complete - Authorization on %s is not supported - You will be logged out from all sources that you are authorized in + Authorized + Logging in on %s is not supported + You will be logged out from all sources Genres Finished Ongoing Date format Default Exclude NSFW manga from history - Name should not be empty - Show pages numbers - Enabled sources + You must enter a name + Numbered pages + Used sources Available sources Dynamic theme Applies a theme created on the color scheme of your wallpaper diff --git a/app/src/test/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepositoryTest.kt b/app/src/test/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepositoryTest.kt index bdfa3f735..6cf57a268 100644 --- a/app/src/test/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepositoryTest.kt +++ b/app/src/test/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepositoryTest.kt @@ -31,7 +31,7 @@ class RemoteMangaRepositoryTest(private val source: MangaSource) : KoinTest { @get:Rule val koinTestRule = KoinTestRule.create { printLogger(Level.ERROR) - modules(repositoryTestModule) + modules(repositoryTestModule, parserModule) } @get:Rule diff --git a/app/src/test/java/org/koitharu/kotatsu/core/parser/RepositoryTestModule.kt b/app/src/test/java/org/koitharu/kotatsu/core/parser/RepositoryTestModule.kt index c28ccdec8..81d536fea 100644 --- a/app/src/test/java/org/koitharu/kotatsu/core/parser/RepositoryTestModule.kt +++ b/app/src/test/java/org/koitharu/kotatsu/core/parser/RepositoryTestModule.kt @@ -7,7 +7,6 @@ import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.network.TestCookieJar 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.prefs.SourceSettings import java.util.concurrent.TimeUnit @@ -31,12 +30,4 @@ val repositoryTestModule } } } - factory { (source: MangaSource) -> - runCatching { - source.cls.getDeclaredConstructor(MangaLoaderContext::class.java) - .newInstance(get()) - }.recoverCatching { - source.cls.newInstance() - }.getOrThrow() as RemoteMangaRepository - } } \ No newline at end of file