From 8ef758009727f2ea4e971c61ae786d1df0da2a0c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 Jan 2022 07:49:03 +0200 Subject: [PATCH 01/15] Fix MangaRead parse #87 --- app/build.gradle | 4 ++-- .../kotatsu/core/parser/site/MangareadRepository.kt | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 68bb3d879..9f01df521 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" 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, From fb815abad022cea186f02f15007153de98a5973b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 Jan 2022 08:05:07 +0200 Subject: [PATCH 02/15] Fix widgets #86 --- .../java/org/koitharu/kotatsu/utils/PendingIntentCompat.kt | 6 ++++++ .../koitharu/kotatsu/widget/recent/RecentWidgetProvider.kt | 2 +- .../koitharu/kotatsu/widget/shelf/ShelfWidgetProvider.kt | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) 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) From cb6bf91dd3c0d615812ccf670299e08ff63e5c32 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 Jan 2022 08:14:56 +0200 Subject: [PATCH 03/15] Fix missing fragment crash #91 --- app/proguard-rules.pro | 1 + 1 file changed, 1 insertion(+) 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 From 66dc5a9597fac0174fbb64d9c04d0276fe9a9d60 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 Jan 2022 09:46:26 +0200 Subject: [PATCH 04/15] Fix MangaTown licensed chapters --- .../core/parser/RemoteMangaRepository.kt | 4 ++- .../core/parser/site/MangaTownRepository.kt | 28 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) 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..0062bc1d0 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 @@ -53,8 +53,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/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 { From e3a80b5a6df9d336fc4c1b1d1a71efdf7653c571 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 23 Jan 2022 10:13:32 +0200 Subject: [PATCH 05/15] Enhance download cancellation in blocking io tasks #90 --- .../download/domain/DownloadManager.kt | 2 +- .../koitharu/kotatsu/local/data/MangaZip.kt | 8 +-- .../kotatsu/local/data/WritableCbzFile.kt | 58 +++++++++---------- 3 files changed, 34 insertions(+), 34 deletions(-) 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/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) } } } From cbcf98e1d46f61942fda212859b0b5611915273e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 26 Jan 2022 19:24:27 +0200 Subject: [PATCH 06/15] Fix blocking calls in coroutines --- app/build.gradle | 24 +++---- .../kotatsu/base/domain/MangaUtils.kt | 20 +++--- .../kotatsu/core/backup/BackupArchive.kt | 4 +- .../kotatsu/local/data/WritableCbzFile.kt | 7 +- .../local/domain/LocalMangaRepository.kt | 72 +++++++++---------- .../kotatsu/local/ui/LocalListViewModel.kt | 10 +-- .../settings/backup/RestoreViewModel.kt | 4 +- .../koitharu/kotatsu/utils/MutableZipFile.kt | 10 +-- 8 files changed, 77 insertions(+), 74 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9f01df521..5089c1088 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,10 +24,6 @@ android { } } } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } buildTypes { debug { applicationIdSuffix = '.debug' @@ -45,6 +41,17 @@ android { sourceSets { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + freeCompilerArgs += [ + '-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', + '-Xopt-in=kotlin.contracts.ExperimentalContracts', + ] + } lintOptions { disable 'MissingTranslation' abortOnError false @@ -54,15 +61,6 @@ android { unitTests.returnDefaultValues = false } } -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - freeCompilerArgs += [ - '-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', - '-Xopt-in=kotlin.contracts.ExperimentalContracts', - ] - } -} dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0' 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..9e14b0d40 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 @@ -3,7 +3,9 @@ package org.koitharu.kotatsu.base.domain import android.graphics.BitmapFactory import android.net.Uri import android.util.Size -import androidx.annotation.WorkerThread +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import org.koin.core.component.KoinComponent @@ -23,18 +25,18 @@ object MangaUtils : KoinComponent { * Automatic determine type of manga by page size * @return ReaderMode.WEBTOON if page is wide */ - @WorkerThread - @Suppress("BlockingMethodInNonBlockingContext") suspend fun determineMangaIsWebtoon(pages: List): Boolean? { try { val page = pages.medianOrNull() ?: return null val url = page.source.repository.getPageUrl(page) val uri = Uri.parse(url) val size = if (uri.scheme == "cbz") { - val zip = ZipFile(uri.schemeSpecificPart) - val entry = zip.getEntry(uri.fragment) - zip.getInputStream(entry).use { - getBitmapSize(it) + runInterruptible(Dispatchers.IO) { + val zip = ZipFile(uri.schemeSpecificPart) + val entry = zip.getEntry(uri.fragment) + zip.getInputStream(entry).use { + getBitmapSize(it) + } } } else { val client = get() @@ -45,7 +47,9 @@ object MangaUtils : KoinComponent { .cacheControl(CacheUtils.CONTROL_DISABLED) .build() client.newCall(request).await().use { - getBitmapSize(it.body?.byteStream()) + withContext(Dispatchers.IO) { + getBitmapSize(it.body?.byteStream()) + } } } return size.width * 2 < size.height diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupArchive.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupArchive.kt index c0af8d788..d0ec8a8cb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupArchive.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupArchive.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.backup import android.content.Context import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import org.json.JSONArray import org.koitharu.kotatsu.R @@ -33,8 +34,7 @@ class BackupArchive(file: File) : MutableZipFile(file) { private const val DIR_BACKUPS = "backups" - @Suppress("BlockingMethodInNonBlockingContext") - suspend fun createNew(context: Context): BackupArchive = withContext(Dispatchers.IO) { + suspend fun createNew(context: Context): BackupArchive = runInterruptible(Dispatchers.IO) { val dir = context.run { getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS) } 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 b7c5f7b9f..fbc2637aa 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 @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.local.data import androidx.annotation.CheckResult import kotlinx.coroutines.* +import org.koitharu.kotatsu.utils.ext.deleteAwait import java.io.File import java.io.FileInputStream import java.io.FileOutputStream @@ -13,7 +14,6 @@ class WritableCbzFile(private val file: File) { private val dir = File(file.parentFile, file.nameWithoutExtension) - @Suppress("BlockingMethodInNonBlockingContext") suspend fun prepare() = withContext(Dispatchers.IO) { check(dir.list().isNullOrEmpty()) { "Dir ${dir.name} is not empty" @@ -45,11 +45,10 @@ class WritableCbzFile(private val file: File) { } @CheckResult - @Suppress("BlockingMethodInNonBlockingContext") suspend fun flush() = withContext(Dispatchers.IO) { val tempFile = File(file.path + ".tmp") if (tempFile.exists()) { - tempFile.delete() + tempFile.deleteAwait() } try { runInterruptible { @@ -63,7 +62,7 @@ class WritableCbzFile(private val file: File) { tempFile.renameTo(file) } finally { if (tempFile.exists()) { - tempFile.delete() + tempFile.deleteAwait() } } } 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..822da1c24 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 @@ -8,6 +8,7 @@ import androidx.collection.ArraySet import androidx.core.net.toFile import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.parser.MangaRepository @@ -42,38 +43,39 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { getFromFile(Uri.parse(manga.url).toFile()) } else manga - @Suppress("BlockingMethodInNonBlockingContext") override suspend fun getPages(chapter: MangaChapter): List { - val uri = Uri.parse(chapter.url) - val file = uri.toFile() - val zip = ZipFile(file) - val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex) - var entries = zip.entries().asSequence() - entries = if (index != null) { - val pattern = index.getChapterNamesPattern(chapter) - entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) } - } else { - val parent = uri.fragment.orEmpty() - entries.filter { x -> - !x.isDirectory && x.name.substringBeforeLast( - File.separatorChar, - "" - ) == parent + return runInterruptible(Dispatchers.IO){ + val uri = Uri.parse(chapter.url) + val file = uri.toFile() + val zip = ZipFile(file) + val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex) + var entries = zip.entries().asSequence() + entries = if (index != null) { + val pattern = index.getChapterNamesPattern(chapter) + entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) } + } else { + val parent = uri.fragment.orEmpty() + entries.filter { x -> + !x.isDirectory && x.name.substringBeforeLast( + File.separatorChar, + "" + ) == parent + } } + entries + .toList() + .sortedWith(compareBy(AlphanumComparator()) { x -> x.name }) + .map { x -> + val entryUri = zipUri(file, x.name) + MangaPage( + id = entryUri.longHashCode(), + url = entryUri, + preview = null, + referer = chapter.url, + source = MangaSource.LOCAL, + ) + } } - return entries - .toList() - .sortedWith(compareBy(AlphanumComparator()) { x -> x.name }) - .map { x -> - val entryUri = zipUri(file, x.name) - MangaPage( - id = entryUri.longHashCode(), - url = entryUri, - preview = null, - referer = chapter.url, - source = MangaSource.LOCAL, - ) - } } suspend fun delete(manga: Manga): Boolean { @@ -137,20 +139,18 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { val file = runCatching { Uri.parse(localManga.url).toFile() }.getOrNull() ?: return null - return withContext(Dispatchers.IO) { - @Suppress("BlockingMethodInNonBlockingContext") + return runInterruptible(Dispatchers.IO) { ZipFile(file).use { zip -> val entry = zip.getEntry(MangaZip.INDEX_ENTRY) - val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null - index.getMangaInfo() + val index = entry?.let(zip::readText)?.let(::MangaIndex) + index?.getMangaInfo() } } } - suspend fun findSavedManga(remoteManga: Manga): Manga? = withContext(Dispatchers.IO) { + suspend fun findSavedManga(remoteManga: Manga): Manga? = runInterruptible(Dispatchers.IO) { val files = getAllFiles() for (file in files) { - @Suppress("BlockingMethodInNonBlockingContext") val index = ZipFile(file).use { zip -> val entry = zip.getEntry(MangaZip.INDEX_ENTRY) entry?.let(zip::readText)?.let(::MangaIndex) @@ -158,7 +158,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository { val info = index.getMangaInfo() ?: continue if (info.id == remoteManga.id) { val fileUri = file.toUri().toString() - return@withContext info.copy( + return@runInterruptible info.copy( source = MangaSource.LOCAL, url = fileUri, chapters = info.chapters?.map { c -> c.copy(url = fileUri) } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index 8e40277c9..b1fc2493e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException @@ -81,10 +82,11 @@ class LocalListViewModel( } val dest = settings.getStorageDir(context)?.let { File(it, name) } ?: throw IOException("External files dir unavailable") - @Suppress("BlockingMethodInNonBlockingContext") - contentResolver.openInputStream(uri)?.use { source -> - dest.outputStream().use { output -> - source.copyTo(output) + runInterruptible { + contentResolver.openInputStream(uri)?.use { source -> + dest.outputStream().use { output -> + source.copyTo(output) + } } } ?: throw IOException("Cannot open input stream: $uri") } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt index 1948b6585..ed2613fb8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt @@ -5,6 +5,7 @@ import android.net.Uri import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.backup.BackupArchive @@ -32,8 +33,7 @@ class RestoreViewModel( } val contentResolver = context.contentResolver - @Suppress("BlockingMethodInNonBlockingContext") - val backup = withContext(Dispatchers.IO) { + val backup = runInterruptible(Dispatchers.IO) { val tempFile = File.createTempFile("backup_", ".tmp") (contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input -> diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt b/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt index a57bae29c..b785a62d2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/MutableZipFile.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils import androidx.annotation.CheckResult import androidx.annotation.WorkerThread import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import java.io.File import java.io.FileInputStream @@ -11,12 +12,11 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -@Suppress("BlockingMethodInNonBlockingContext") open class MutableZipFile(val file: File) { protected val dir = File(file.parentFile, file.nameWithoutExtension) - suspend fun unpack(): Unit = withContext(Dispatchers.IO) { + suspend fun unpack(): Unit = runInterruptible(Dispatchers.IO) { check(dir.list().isNullOrEmpty()) { "Dir ${dir.name} is not empty" } @@ -24,7 +24,7 @@ open class MutableZipFile(val file: File) { dir.mkdir() } if (!file.exists()) { - return@withContext + return@runInterruptible } ZipInputStream(FileInputStream(file)).use { zip -> var entry = zip.nextEntry @@ -45,7 +45,7 @@ open class MutableZipFile(val file: File) { } @CheckResult - suspend fun flush(): Boolean = withContext(Dispatchers.IO) { + suspend fun flush(): Boolean = runInterruptible(Dispatchers.IO) { val tempFile = File(file.path + ".tmp") if (tempFile.exists()) { tempFile.delete() @@ -57,7 +57,7 @@ open class MutableZipFile(val file: File) { } zip.flush() } - return@withContext tempFile.renameTo(file) + tempFile.renameTo(file) } finally { if (tempFile.exists()) { tempFile.delete() From 03590f4b822e567a96c6eb5780998e533cafab8d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 11 Feb 2022 19:23:41 +0200 Subject: [PATCH 07/15] Fix widgets context leak --- .../org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt | 2 +- .../org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt b/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt index 74e24e159..ccad1811d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/recent/RecentWidgetService.kt @@ -7,6 +7,6 @@ import org.koin.android.ext.android.get class RecentWidgetService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { - return RecentListFactory(this, get(), get()) + return RecentListFactory(applicationContext, get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt index 3f590235a..89d0a8862 100644 --- a/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/widget/shelf/ShelfWidgetService.kt @@ -12,6 +12,6 @@ class ShelfWidgetService : RemoteViewsService() { AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID ) - return ShelfListFactory(this, get(), get(), widgetId) + return ShelfListFactory(applicationContext, get(), get(), widgetId) } } \ No newline at end of file From 33190ae3eae15e623783007805e493f5fc5ff50f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 11 Feb 2022 19:32:38 +0200 Subject: [PATCH 08/15] Fix DownloadBinder leak --- .../koitharu/kotatsu/download/ui/service/DownloadService.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt index d44204533..03712d889 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt @@ -16,7 +16,6 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -79,6 +78,11 @@ class DownloadService : BaseService() { return binder ?: DownloadBinder(this).also { binder = it } } + override fun onUnbind(intent: Intent?): Boolean { + binder = null + return super.onUnbind(intent) + } + override fun onDestroy() { unregisterReceiver(controlReceiver) binder = null From c7c23b9768f209a008710bf37719ee33691dd08c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 11 Feb 2022 19:41:49 +0200 Subject: [PATCH 09/15] Fix ItemTouchHelper leak --- .../settings/sources/SourcesSettingsFragment.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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..0d68448d1 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 @@ -21,12 +21,11 @@ import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem class SourcesSettingsFragment : BaseFragment(), SourceConfigListener { - private lateinit var reorderHelper: ItemTouchHelper + private var reorderHelper: ItemTouchHelper? = null private val viewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - reorderHelper = ItemTouchHelper(SourcesReorderCallback()) setHasOptionsMenu(true) } @@ -47,7 +46,9 @@ class SourcesSettingsFragment : BaseFragment(), setHasFixedSize(true) addItemDecoration(SourceConfigItemDecoration(view.context)) adapter = sourcesAdapter - reorderHelper.attachToRecyclerView(this) + reorderHelper = ItemTouchHelper(SourcesReorderCallback()).also { + it.attachToRecyclerView(this) + } } viewModel.items.observe(viewLifecycleOwner) { sourcesAdapter.items = it @@ -55,7 +56,7 @@ class SourcesSettingsFragment : BaseFragment(), } override fun onDestroyView() { - reorderHelper.attachToRecyclerView(null) + reorderHelper = null super.onDestroyView() } @@ -76,7 +77,7 @@ class SourcesSettingsFragment : BaseFragment(), } override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) { - reorderHelper.startDrag(holder) + reorderHelper?.startDrag(holder) } override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) { From 8b0b375dfeb1cbf0983613b5f97ef9faad1a0d54 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 12 Feb 2022 10:31:26 +0200 Subject: [PATCH 10/15] Fix ActivityNotFoundException --- app/src/main/AndroidManifest.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2cc8e53e2..de52fe2fb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,7 +32,7 @@ + android:value="org.koitharu.kotatsu.ui.search.SearchActivity" /> - + Date: Sat, 12 Feb 2022 14:58:03 +0200 Subject: [PATCH 11/15] Increase version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5089c1088..932b8736d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdkVersion 21 targetSdkVersion 31 - versionCode 379 - versionName '2.1.3' + versionCode 380 + versionName '2.1.4' generatedDensities = [] testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" From e2f3ba19b8f550bf5c038dbdeaf0fc5a9e88c821 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 12 Feb 2022 15:05:58 +0200 Subject: [PATCH 12/15] Update dependencies minor versions --- app/build.gradle | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 932b8736d..1f5456819 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,13 +68,13 @@ dependencies { implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.activity:activity-ktx:1.4.0' - implementation 'androidx.fragment:fragment-ktx:1.4.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0' - implementation 'androidx.lifecycle:lifecycle-service:2.4.0' - implementation 'androidx.lifecycle:lifecycle-process:2.4.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + implementation 'androidx.fragment:fragment-ktx:1.4.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' + implementation 'androidx.lifecycle:lifecycle-service:2.4.1' + implementation 'androidx.lifecycle:lifecycle-process:2.4.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' @@ -82,11 +82,11 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'com.google.android.material:material:1.4.0' //noinspection LifecycleAnnotationProcessorWithJava8 - kapt 'androidx.lifecycle:lifecycle-compiler:2.4.0' + kapt 'androidx.lifecycle:lifecycle-compiler:2.4.1' - implementation 'androidx.room:room-runtime:2.4.0' - implementation 'androidx.room:room-ktx:2.4.0' - kapt 'androidx.room:room-compiler:2.4.0' + implementation 'androidx.room:room-runtime:2.4.1' + implementation 'androidx.room:room-ktx:2.4.1' + kapt 'androidx.room:room-compiler:2.4.1' implementation 'com.squareup.okhttp3:okhttp:4.9.1' implementation 'com.squareup.okio:okio:2.10.0' @@ -98,7 +98,7 @@ dependencies { implementation 'io.insert-koin:koin-android:3.1.4' implementation 'io.coil-kt:coil-base:1.4.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' - implementation 'com.github.solkin:disk-lru-cache:1.3' + implementation 'com.github.solkin:disk-lru-cache:1.4' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7' @@ -112,6 +112,6 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3' - androidTestImplementation 'androidx.room:room-testing:2.4.0' + androidTestImplementation 'androidx.room:room-testing:2.4.1' androidTestImplementation 'com.google.truth:truth:1.1.3' } \ No newline at end of file From 78aa4d76db0c74a0e164864e9ddd503c9b4130a1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 25 Jan 2022 19:56:37 +0200 Subject: [PATCH 13/15] Show favicons in sources list --- .../kotatsu/core/parser/FaviconMapper.kt | 18 ++++++ .../core/parser/RemoteMangaRepository.kt | 2 + .../core/parser/site/AnibelRepository.kt | 4 ++ .../org/koitharu/kotatsu/core/ui/uiModule.kt | 2 + .../koitharu/kotatsu/local/data/CbzFetcher.kt | 7 ++- .../sources/SourcesSettingsFragment.kt | 3 +- .../sources/SourcesSettingsViewModel.kt | 2 + .../sources/adapter/SourceConfigAdapter.kt | 7 ++- .../adapter/SourceConfigAdapterDelegates.kt | 55 +++++++++++++++++-- .../sources/model/SourceConfigItem.kt | 7 +++ .../main/res/drawable/ic_favicon_fallback.xml | 16 ++++++ .../main/res/layout/item_source_config.xml | 14 +++-- .../layout/item_source_config_draggable.xml | 46 ++++++++++++++++ 13 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/parser/FaviconMapper.kt create mode 100644 app/src/main/res/drawable/ic_favicon_fallback.xml create mode 100644 app/src/main/res/layout/item_source_config_draggable.xml 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/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 0062bc1d0..293bed94b 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 @@ -29,6 +29,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 } 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/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/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/settings/sources/SourcesSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/sources/SourcesSettingsFragment.kt index 0d68448d1..443077ce8 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 @@ -8,6 +8,7 @@ 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 @@ -41,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)) 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..1bff594c9 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 @@ -81,6 +81,7 @@ class SourcesSettingsViewModel( SourceConfigItem.SourceItem( source = it, isEnabled = true, + isDraggable = true, ) } } @@ -102,6 +103,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..79cab4f39 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,18 @@ 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), ) \ 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..df2b734b9 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 @@ -6,12 +6,18 @@ 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.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 +44,54 @@ 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 } +) { + + val eventListener = object : View.OnClickListener, CompoundButton.OnCheckedChangeListener { + override fun onClick(v: View?) = listener.onItemSettingsClick(item) + + override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { + listener.onItemEnabledChanged(item, isChecked) + } + } + var imageRequest: Disposable? = null + + binding.imageViewConfig.setOnClickListener(eventListener) + binding.switchToggle.setOnCheckedChangeListener(eventListener) + + bind { + binding.textViewTitle.text = item.source.title + binding.switchToggle.isChecked = item.isEnabled + binding.imageViewConfig.isVisible = item.isEnabled + binding.root.updatePaddingRelative( + end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd, + ) + 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,10 +119,8 @@ 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, ) } 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..a3e398a03 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,6 +72,7 @@ sealed interface SourceConfigItem { override fun hashCode(): Int { var result = source.hashCode() result = 31 * result + isEnabled.hashCode() + result = 31 * result + isDraggable.hashCode() return result } } 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 ffa9a68e5..2d225fea2 100644 --- a/app/src/main/res/layout/item_source_config.xml +++ b/app/src/main/res/layout/item_source_config.xml @@ -9,12 +9,14 @@ 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="6dp" + android:scaleType="fitCenter" + tools:src="@tools:sample/avatars" /> + + + + + + + + + + + \ No newline at end of file From 79c2bf17fd0eb57641dfda628631ef46ab79b38c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 26 Jan 2022 08:21:53 +0200 Subject: [PATCH 14/15] Quick search across manga sources --- .../sources/SourcesSettingsFragment.kt | 32 ++++++++++++++++--- .../sources/SourcesSettingsViewModel.kt | 22 +++++++++++++ .../sources/adapter/SourceConfigAdapter.kt | 1 + .../adapter/SourceConfigAdapterDelegates.kt | 29 ++++++----------- .../adapter/SourceConfigDiffCallback.kt | 10 ++++-- .../sources/model/SourceConfigItem.kt | 2 ++ .../main/res/layout/item_source_config.xml | 16 ++-------- .../main/res/layout/item_sources_empty.xml | 9 ++++++ app/src/main/res/menu/opt_sources.xml | 9 +++--- 9 files changed, 86 insertions(+), 44 deletions(-) create mode 100644 app/src/main/res/layout/item_sources_empty.xml 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 443077ce8..dd9c4235a 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,9 +1,8 @@ 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 @@ -20,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 var reorderHelper: ItemTouchHelper? = null private val viewModel by viewModel() @@ -61,6 +60,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, @@ -85,6 +95,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 1bff594c9..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 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 79cab4f39..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 @@ -15,4 +15,5 @@ class SourceConfigAdapter( sourceConfigGroupDelegate(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 df2b734b9..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,12 +4,11 @@ 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 @@ -53,25 +52,15 @@ fun sourceConfigItemDelegate( on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable } ) { - val eventListener = object : View.OnClickListener, CompoundButton.OnCheckedChangeListener { - override fun onClick(v: View?) = listener.onItemSettingsClick(item) - - override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) { - listener.onItemEnabledChanged(item, isChecked) - } - } var imageRequest: Disposable? = null - binding.imageViewConfig.setOnClickListener(eventListener) - binding.switchToggle.setOnCheckedChangeListener(eventListener) + binding.switchToggle.setOnCheckedChangeListener { _, isChecked -> + listener.onItemEnabledChanged(item, isChecked) + } bind { binding.textViewTitle.text = item.source.title binding.switchToggle.isChecked = item.isEnabled - binding.imageViewConfig.isVisible = item.isEnabled - binding.root.updatePaddingRelative( - end = if (item.isEnabled) 0 else binding.imageViewConfig.paddingEnd, - ) imageRequest = ImageRequest.Builder(context) .data(item.faviconUrl) .error(R.drawable.ic_favicon_fallback) @@ -119,9 +108,9 @@ fun sourceConfigDraggableItemDelegate( bind { binding.textViewTitle.text = item.source.title binding.switchToggle.isChecked = item.isEnabled - binding.imageViewConfig.isVisible = item.isEnabled - binding.root.updatePaddingRelative( - 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 a3e398a03..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 @@ -76,4 +76,6 @@ sealed interface SourceConfigItem { return result } } + + object EmptySearchResult : SourceConfigItem } \ 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 2d225fea2..a13432590 100644 --- a/app/src/main/res/layout/item_source_config.xml +++ b/app/src/main/res/layout/item_source_config.xml @@ -4,7 +4,6 @@ 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"> @@ -14,7 +13,7 @@ android:layout_height="?android:listPreferredItemHeightSmall" android:layout_marginHorizontal="?listPreferredItemPaddingStart" android:labelFor="@id/textView_title" - android:padding="6dp" + android:padding="8dp" android:scaleType="fitCenter" tools:src="@tools:sample/avatars" /> @@ -33,16 +32,7 @@ - - + android:layout_height="wrap_content" + android:layout_marginEnd="?listPreferredItemPaddingEnd" /> \ 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 From 5820b2f5114adfb394a7848d25bdbc7b584fa579 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 12 Feb 2022 20:31:30 +0200 Subject: [PATCH 15/15] Fix saved page sharing --- .../java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt | 6 +++--- app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index c745d2071..4ed70ba02 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -238,8 +238,8 @@ class ReaderActivity : BaseFullscreenActivity(), ) { false } else { - val targets = binding.root.hitTest(rawX, rawY) - targets.none { it.hasOnClickListeners() } + val touchables = window.peekDecorView()?.touchables + touchables?.none { it.hasGlobalPoint(rawX, rawY) } ?: true } } @@ -281,7 +281,7 @@ class ReaderActivity : BaseFullscreenActivity(), private fun onPageSaved(uri: Uri?) { if (uri != null) { - Snackbar.make(binding.container, R.string.page_saved, Snackbar.LENGTH_LONG) + Snackbar.make(binding.container, R.string.page_saved, Snackbar.LENGTH_INDEFINITE) .setAnchorView(binding.appbarBottom) .setAction(R.string.share) { ShareHelper(this).shareImage(uri) diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt index fdfb8f20d..b06b18d16 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt @@ -46,7 +46,7 @@ class ShareHelper(private val context: Context) { fun shareImage(uri: Uri) { val intent = Intent(Intent.ACTION_SEND) - intent.setDataAndType(uri, context.contentResolver.getType(uri)) + intent.setDataAndType(uri, context.contentResolver.getType(uri) ?: "image/*") intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_image)) context.startActivity(shareIntent)