From a4345a40bf751e81122ba27b3ace4c4e0794a778 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 2 Oct 2024 11:34:51 +0300 Subject: [PATCH 001/109] Update dependencies --- app/build.gradle | 4 ++-- .../kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt | 6 +++++- app/src/main/res/values/strings.xml | 1 + build.gradle | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0ba441866..7bad9b52c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,7 +83,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:1.1') { + implementation('com.github.KotatsuApp:kotatsu-parsers:a8df8665ae') { exclude group: 'org.json', module: 'json' } @@ -137,7 +137,7 @@ dependencies { implementation 'io.coil-kt:coil-base:2.7.0' implementation 'io.coil-kt:coil-svg:2.7.0' - implementation 'com.github.KotatsuApp:subsampling-scale-image-view:b2c5a6d5ca' + implementation 'com.github.KotatsuApp:subsampling-scale-image-view:e04098de68' implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'io.noties.markwon:core:4.6.2' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt index 93c447518..85afb772c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt @@ -81,7 +81,11 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { is UnknownHostException, is SocketTimeoutException -> resources.getString(R.string.network_error) - is ImageDecodeException -> resources.getString(R.string.error_corrupted_file) + is ImageDecodeException -> resources.getString( + R.string.error_image_format, + format.ifNullOrEmpty { resources.getString(R.string.unknown) }, + ) + is NoDataReceivedException -> resources.getString(R.string.error_no_data_received) is IncompatiblePluginException -> resources.getString(R.string.plugin_incompatible) is WrongPasswordException -> resources.getString(R.string.wrong_password) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e0cca68e8..a5bcf862c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -737,4 +737,5 @@ Source code User manual Telegram group + Unsupported image format: %s diff --git a/build.gradle b/build.gradle index fadb8b78b..48081cc88 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.6.0' + classpath 'com.android.tools.build:gradle:8.7.0' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.52' classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:2.0.20-1.0.25' From ac96c49b605b09afd6b3e7c048b6a92bac76373f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 2 Oct 2024 15:13:37 +0300 Subject: [PATCH 002/109] Strict mode notificaiton for debug build --- .../kotlin/org/koitharu/kotatsu/KotatsuApp.kt | 36 +++++++++-- .../koitharu/kotatsu/StrictModeNotifier.kt | 64 +++++++++++++++++++ app/src/debug/res/values/strings.xml | 3 +- 3 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 app/src/debug/kotlin/org/koitharu/kotatsu/StrictModeNotifier.kt diff --git a/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index eec41000c..187a7b3d8 100644 --- a/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu import android.content.Context +import android.os.Build import android.os.StrictMode import androidx.fragment.app.strictmode.FragmentStrictMode import org.koitharu.kotatsu.core.BaseApp @@ -18,22 +19,42 @@ class KotatsuApp : BaseApp() { } private fun enableStrictMode() { + val notifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + StrictModeNotifier(this) + } else { + null + } StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() - .build(), + .run { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { + penaltyListener(notifier.executor, notifier) + } else { + this + } + }.build(), ) StrictMode.setVmPolicy( StrictMode.VmPolicy.Builder() - .detectAll() + .detectActivityLeaks() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() .setClassInstanceLimit(LocalMangaRepository::class.java, 1) .setClassInstanceLimit(PagesCache::class.java, 1) .setClassInstanceLimit(MangaLoaderContext::class.java, 1) .setClassInstanceLimit(PageLoader::class.java, 1) .setClassInstanceLimit(ReaderViewModel::class.java, 1) .penaltyLog() - .build(), + .run { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { + penaltyListener(notifier.executor, notifier) + } else { + this + } + }.build(), ) FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder() .penaltyDeath() @@ -42,6 +63,13 @@ class KotatsuApp : BaseApp() { .detectRetainInstanceUsage() .detectSetUserVisibleHint() .detectFragmentTagUsage() - .build() + .penaltyLog() + .run { + if (notifier != null) { + penaltyListener(notifier) + } else { + this + } + }.build() } } diff --git a/app/src/debug/kotlin/org/koitharu/kotatsu/StrictModeNotifier.kt b/app/src/debug/kotlin/org/koitharu/kotatsu/StrictModeNotifier.kt new file mode 100644 index 000000000..8847cb601 --- /dev/null +++ b/app/src/debug/kotlin/org/koitharu/kotatsu/StrictModeNotifier.kt @@ -0,0 +1,64 @@ +package org.koitharu.kotatsu + +import android.app.Notification +import android.app.Notification.BigTextStyle +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import android.os.StrictMode +import android.os.strictmode.Violation +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import androidx.fragment.app.strictmode.FragmentStrictMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import org.koitharu.kotatsu.core.ErrorReporterReceiver +import kotlin.math.absoluteValue +import androidx.fragment.app.strictmode.Violation as FragmentViolation + +@RequiresApi(Build.VERSION_CODES.P) +class StrictModeNotifier( + private val context: Context, +) : StrictMode.OnVmViolationListener, StrictMode.OnThreadViolationListener, FragmentStrictMode.OnViolationListener { + + val executor = Dispatchers.Default.asExecutor() + + private val notificationManager by lazy { + val nm = checkNotNull(context.getSystemService()) + val channel = NotificationChannel( + CHANNEL_ID, + context.getString(R.string.strict_mode), + NotificationManager.IMPORTANCE_LOW, + ) + nm.createNotificationChannel(channel) + nm + } + + override fun onVmViolation(v: Violation) = showNotification(v) + + override fun onThreadViolation(v: Violation) = showNotification(v) + + override fun onViolation(violation: FragmentViolation) = showNotification(violation) + + private fun showNotification(violation: Throwable) = Notification.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_notify_error) + .setContentTitle(context.getString(R.string.strict_mode)) + .setContentText(violation.message) + .setStyle( + BigTextStyle() + .setBigContentTitle(context.getString(R.string.strict_mode)) + .setSummaryText(violation.message) + .bigText(violation.stackTraceToString()), + ).setShowWhen(true) + .setContentIntent(ErrorReporterReceiver.getPendingIntent(context, violation)) + .setAutoCancel(true) + .setGroup(CHANNEL_ID) + .build() + .let { notificationManager.notify(CHANNEL_ID, violation.hashCode().absoluteValue, it) } + + private companion object { + + const val CHANNEL_ID = "strict_mode" + } +} diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index 5a9c5bfd8..dfda02b0e 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -1,3 +1,4 @@ Kotatsu Dev - \ No newline at end of file + Strict mode + From e5b69475869ece06e832e234b55c420592e08337 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 2 Oct 2024 16:39:21 +0300 Subject: [PATCH 003/109] Fix StrictMode errors --- .../kotlin/org/koitharu/kotatsu/KotatsuApp.kt | 83 +++++++++---------- .../koitharu/kotatsu/core/fs/FileSequence.kt | 29 +++++-- .../kotatsu/core/util/CloseableSequence.kt | 3 + .../koitharu/kotatsu/core/util/ext/File.kt | 9 +- .../core/util/iterator/CloseableIterator.kt | 36 -------- .../koitharu/kotatsu/core/zip/ZipOutput.kt | 8 +- .../ui/pager/pages/MangaPageFetcher.kt | 5 +- .../download/ui/worker/DownloadWorker.kt | 3 +- .../local/data/LocalMangaRepository.kt | 18 ++-- .../local/data/input/LocalMangaDirInput.kt | 19 +++-- .../local/data/output/LocalMangaDirOutput.kt | 2 +- .../kotatsu/reader/domain/PageLoader.kt | 4 +- .../koitharu/kotatsu/sync/data/SyncAuthApi.kt | 3 +- 13 files changed, 105 insertions(+), 117 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/CloseableSequence.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt diff --git a/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index 187a7b3d8..7426b5575 100644 --- a/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -25,51 +25,50 @@ class KotatsuApp : BaseApp() { null } StrictMode.setThreadPolicy( - StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyLog() - .run { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { - penaltyListener(notifier.executor, notifier) - } else { - this - } - }.build(), + StrictMode.ThreadPolicy.Builder().apply { + detectNetwork() + detectDiskWrites() + detectCustomSlowCalls() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectUnbufferedIo() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) detectResourceMismatches() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) detectExplicitGc() + penaltyLog() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { + penaltyListener(notifier.executor, notifier) + } + }.build(), ) StrictMode.setVmPolicy( - StrictMode.VmPolicy.Builder() - .detectActivityLeaks() - .detectLeakedSqlLiteObjects() - .detectLeakedClosableObjects() - .detectLeakedRegistrationObjects() - .setClassInstanceLimit(LocalMangaRepository::class.java, 1) - .setClassInstanceLimit(PagesCache::class.java, 1) - .setClassInstanceLimit(MangaLoaderContext::class.java, 1) - .setClassInstanceLimit(PageLoader::class.java, 1) - .setClassInstanceLimit(ReaderViewModel::class.java, 1) - .penaltyLog() - .run { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { - penaltyListener(notifier.executor, notifier) - } else { - this - } - }.build(), - ) - FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder() - .penaltyDeath() - .detectFragmentReuse() - .detectWrongFragmentContainer() - .detectRetainInstanceUsage() - .detectSetUserVisibleHint() - .detectFragmentTagUsage() - .penaltyLog() - .run { - if (notifier != null) { - penaltyListener(notifier) - } else { - this + StrictMode.VmPolicy.Builder().apply { + detectActivityLeaks() + detectLeakedSqlLiteObjects() + detectLeakedClosableObjects() + detectLeakedRegistrationObjects() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectContentUriWithoutPermission() + detectFileUriExposure() + setClassInstanceLimit(LocalMangaRepository::class.java, 1) + setClassInstanceLimit(PagesCache::class.java, 1) + setClassInstanceLimit(MangaLoaderContext::class.java, 1) + setClassInstanceLimit(PageLoader::class.java, 1) + setClassInstanceLimit(ReaderViewModel::class.java, 1) + penaltyLog() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { + penaltyListener(notifier.executor, notifier) } }.build() + ) + FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder().apply { + detectWrongFragmentContainer() + detectFragmentTagUsage() + detectRetainInstanceUsage() + detectSetUserVisibleHint() + detectWrongNestedHierarchy() + detectTargetFragmentUsage() + detectFragmentReuse() + penaltyLog() + if (notifier != null) { + penaltyListener(notifier) + } + }.build() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt index ab8713642..e58b96067 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt @@ -1,20 +1,31 @@ package org.koitharu.kotatsu.core.fs import android.os.Build -import org.koitharu.kotatsu.core.util.iterator.CloseableIterator +import androidx.annotation.RequiresApi +import org.koitharu.kotatsu.core.util.CloseableSequence import org.koitharu.kotatsu.core.util.iterator.MappingIterator import java.io.File import java.nio.file.Files import java.nio.file.Path -class FileSequence(private val dir: File) : Sequence { +sealed interface FileSequence : CloseableSequence { - override fun iterator(): Iterator { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val stream = Files.newDirectoryStream(dir.toPath()) - CloseableIterator(MappingIterator(stream.iterator(), Path::toFile), stream) - } else { - dir.listFiles().orEmpty().iterator() - } + @RequiresApi(Build.VERSION_CODES.O) + class StreamImpl(dir: File) : FileSequence { + + private val stream = Files.newDirectoryStream(dir.toPath()) + + override fun iterator(): Iterator = MappingIterator(stream.iterator(), Path::toFile) + + override fun close() = stream.close() + } + + class ListImpl(dir: File) : FileSequence { + + private val list = dir.listFiles().orEmpty() + + override fun iterator(): Iterator = list.iterator() + + override fun close() = Unit } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CloseableSequence.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CloseableSequence.kt new file mode 100644 index 000000000..9cf3b317b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CloseableSequence.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.core.util + +interface CloseableSequence : Sequence, AutoCloseable diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index 8618d1ec2..2343f8ec7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -15,7 +15,6 @@ import org.jetbrains.annotations.Blocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.fs.FileSequence import java.io.File -import java.io.FileFilter import java.io.InputStream import java.nio.file.attribute.BasicFileAttributes import java.util.zip.ZipEntry @@ -87,9 +86,13 @@ suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) { walkCompat(includeDirectories = false).sumOf { it.length() } } -fun File.children() = FileSequence(this) +inline fun File.withChildren(block: (children: Sequence) -> R): R = FileSequence(this).use(block) -fun Sequence.filterWith(filter: FileFilter): Sequence = filter { f -> filter.accept(f) } +fun FileSequence(dir: File): FileSequence = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FileSequence.StreamImpl(dir) +} else { + FileSequence.ListImpl(dir) +} val File.creationTime get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt deleted file mode 100644 index da59d5efc..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.koitharu.kotatsu.core.util.iterator - -import okhttp3.internal.closeQuietly -import okio.Closeable - -class CloseableIterator( - private val upstream: Iterator, - private val closeable: Closeable, -) : Iterator, Closeable { - - private var isClosed = false - - override fun hasNext(): Boolean { - val result = upstream.hasNext() - if (!result) { - close() - } - return result - } - - override fun next(): T { - try { - return upstream.next() - } catch (e: NoSuchElementException) { - close() - throw e - } - } - - override fun close() { - if (!isClosed) { - closeable.closeQuietly() - isClosed = true - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt index 448341678..88b435350 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt @@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.zip import androidx.annotation.WorkerThread import androidx.collection.ArraySet import okio.Closeable -import org.koitharu.kotatsu.core.util.ext.children +import org.koitharu.kotatsu.core.util.ext.withChildren import java.io.File import java.io.FileInputStream import java.util.zip.Deflater @@ -91,8 +91,10 @@ class ZipOutput( } putNextEntry(entry) closeEntry() - fileToZip.children().forEach { childFile -> - appendFile(childFile, "$name/${childFile.name}") + fileToZip.withChildren { children -> + children.forEach { childFile -> + appendFile(childFile, "$name/${childFile.name}") + } } } else { FileInputStream(fileToZip).use { fis -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt index 344a54004..e2d541e64 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt @@ -29,6 +29,7 @@ import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.local.data.util.withExtraCloseable import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.mimeType +import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.reader.domain.PageLoader import java.util.zip.ZipFile import javax.inject.Inject @@ -98,9 +99,7 @@ class MangaPageFetcher( if (!response.isSuccessful) { throw HttpException(response) } - val body = checkNotNull(response.body) { - "Null response" - } + val body = response.requireBody() val mimeType = response.mimeType val file = body.use { pagesCache.put(pageUrl, it.source()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index c62b7f807..0f985027b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -81,6 +81,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.domain.PageLoader import java.io.File @@ -359,7 +360,7 @@ class DownloadWorker @AssistedInject constructor( .use { response -> val file = File(destination, UUID.randomUUID().toString() + ".tmp") try { - checkNotNull(response.body).use { body -> + response.requireBody().use { body -> file.sink(append = false).buffer().use { it.writeAllCancellable(body.source()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index 9b99de67a..7a3206e44 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -15,10 +15,9 @@ import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.AlphanumComparator -import org.koitharu.kotatsu.core.util.ext.children import org.koitharu.kotatsu.core.util.ext.deleteAwait -import org.koitharu.kotatsu.core.util.ext.filterWith import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.withChildren import org.koitharu.kotatsu.local.data.index.LocalMangaIndex import org.koitharu.kotatsu.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.data.output.LocalMangaOutput @@ -216,10 +215,15 @@ class LocalMangaRepository @Inject constructor( } val dirs = storageManager.getWriteableDirs() runInterruptible(Dispatchers.IO) { - dirs.flatMap { dir -> - dir.children().filterWith(TempFileFilter()) - }.forEach { file -> - file.deleteRecursively() + val filter = TempFileFilter() + dirs.forEach { dir -> + dir.withChildren { children -> + children.forEach { child -> + if (filter.accept(child)) { + child.deleteRecursively() + } + } + } } } return true @@ -246,7 +250,7 @@ class LocalMangaRepository @Inject constructor( private suspend fun getAllFiles() = storageManager.getReadableDirs() .asSequence() .flatMap { dir -> - dir.children().filterNot { it.isHidden } + dir.withChildren { children -> children.filterNot { it.isHidden }.toList() } } private fun Collection.unwrap(): List = map { it.manga } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt index 3667f1e31..302e4a4e7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt @@ -6,11 +6,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.util.AlphanumComparator -import org.koitharu.kotatsu.core.util.ext.children import org.koitharu.kotatsu.core.util.ext.creationTime import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.toListSorted import org.koitharu.kotatsu.core.util.ext.walkCompat +import org.koitharu.kotatsu.core.util.ext.withChildren import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.hasCbzExtension import org.koitharu.kotatsu.local.data.hasImageExtension @@ -101,13 +101,14 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { override suspend fun getPages(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { val file = chapter.url.toUri().toFile() if (file.isDirectory) { - file.children() - .filter { it.isFile && hasImageExtension(it) } - .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) - .map { - val pageUri = it.toUri().toString() - MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource) - } + file.withChildren { children -> + children + .filter { it.isFile && hasImageExtension(it) } + .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) + }.map { + val pageUri = it.toUri().toString() + MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource) + } } else { ZipFile(file).use { zip -> zip.entries() @@ -153,6 +154,6 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { } private fun File.isChapterDirectory(): Boolean { - return isDirectory && children().any { hasImageExtension(it) } + return isDirectory && withChildren { children -> children.any { hasImageExtension(it) } } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt index dc9904cfc..32501d50d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt @@ -129,7 +129,7 @@ class LocalMangaDirOutput( index.getChapterFileName(chapter.value.id)?.let { return it } - val baseName = "${chapter.index}_${chapter.value.name.toFileNameSafe()}".take(18) + val baseName = "${chapter.index}_${chapter.value.name.toFileNameSafe()}".take(32) var i = 0 while (true) { val name = (if (i == 0) baseName else baseName + "_$i") + ".cbz" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 14e7cccc2..1b3464c3c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -56,6 +56,7 @@ import org.koitharu.kotatsu.local.data.isFileUri import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import java.util.LinkedList @@ -233,8 +234,7 @@ class PageLoader @Inject constructor( else -> { val request = createPageRequest(pageUrl, page.source) imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response -> - val body = checkNotNull(response.body) { "Null response body" } - body.withProgress(progress).use { + response.requireBody().withProgress(progress).use { cache.put(pageUrl, it.source()) } }.toUri() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt index 0a27f1ca1..26e3833a7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.network.BaseHttpClient import org.koitharu.kotatsu.core.util.ext.toRequestBody import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.parseJson +import org.koitharu.kotatsu.parsers.util.parseRaw import org.koitharu.kotatsu.parsers.util.removeSurrounding import javax.inject.Inject @@ -30,7 +31,7 @@ class SyncAuthApi @Inject constructor( return response.parseJson().getString("token") } else { val code = response.code - val message = response.use { checkNotNull(it.body).string() }.removeSurrounding('"') + val message = response.parseRaw().removeSurrounding('"') throw SyncApiException(message, code) } } From cebce20bed9004a7c8a4ab90807c0cdc830f3e00 Mon Sep 17 00:00:00 2001 From: mnv Date: Wed, 2 Oct 2024 13:18:51 +0530 Subject: [PATCH 004/109] Fix MangaSource import and improve user agent handling MangaSource class was imported twice from different packages. --- .../kotatsu/browser/BrowserActivity.kt | 101 +++++++++--------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt index 1da50cefb..d236b74d6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt @@ -22,50 +22,49 @@ import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.databinding.ActivityBrowserBinding -import org.koitharu.kotatsu.parsers.model.MangaSource import javax.inject.Inject import com.google.android.material.R as materialR @AndroidEntryPoint class BrowserActivity : BaseActivity(), BrowserCallback { - private lateinit var onBackPressedCallback: WebViewBackPressedCallback - - @Inject - lateinit var mangaRepositoryFactory: MangaRepository.Factory - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { - return - } - supportActionBar?.run { - setDisplayHomeAsUpEnabled(true) - setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) - } - val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE)) - val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository - repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT) - viewBinding.webView.configureForParser(userAgent) - CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true) - viewBinding.webView.webViewClient = BrowserClient(this) - viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) - onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) - onBackPressedDispatcher.addCallback(onBackPressedCallback) - if (savedInstanceState != null) { - return - } - val url = intent?.dataString - if (url.isNullOrEmpty()) { - finishAfterTransition() - } else { - onTitleChanged( - intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_), - url, - ) - viewBinding.webView.loadUrl(url) - } - } + private lateinit var onBackPressedCallback: WebViewBackPressedCallback + + @Inject + lateinit var mangaRepositoryFactory: MangaRepository.Factory + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { + return + } + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) + } + val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE)) + val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository + val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT) + viewBinding.webView.configureForParser(userAgent) + CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true) + viewBinding.webView.webViewClient = BrowserClient(this) + viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) + onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) + onBackPressedDispatcher.addCallback(onBackPressedCallback) + if (savedInstanceState != null) { + return + } + val url = intent?.dataString + if (url.isNullOrEmpty()) { + finishAfterTransition() + } else { + onTitleChanged( + intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_), + url, + ) + viewBinding.webView.loadUrl(url) + } + } override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) @@ -137,17 +136,17 @@ class BrowserActivity : BaseActivity(), BrowserCallback bottom = insets.bottom, ) } - - companion object { - - private const val EXTRA_TITLE = "title" - private const val EXTRA_SOURCE = "source" - - fun newIntent(context: Context, url: String, source: MangaSource?, title: String?): Intent { - return Intent(context, BrowserActivity::class.java) - .setData(Uri.parse(url)) - .putExtra(EXTRA_TITLE, title) - .putExtra(EXTRA_SOURCE, source?.name) - } - } + + companion object { + + private const val EXTRA_TITLE = "title" + private const val EXTRA_SOURCE = "source" + + fun newIntent(context: Context, url: String, source: MangaSource?, title: String?): Intent { + return Intent(context, BrowserActivity::class.java) + .setData(Uri.parse(url)) + .putExtra(EXTRA_TITLE, title) + .putExtra(EXTRA_SOURCE, source?.name) + } + } } From 350f1521a6bf09c38a8b4fa030a016d3cf6dc4e9 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 3 Oct 2024 13:08:00 +0300 Subject: [PATCH 005/109] Fix warnings and cleanup --- app/build.gradle | 2 +- .../kotatsu/bookmarks/domain/Bookmark.kt | 3 - .../kotatsu/browser/BrowserActivity.kt | 102 +++++++++--------- .../org/koitharu/kotatsu/core/AppModule.kt | 1 - .../exceptions/CloudFlareBlockedException.kt | 1 - .../exceptions/NoDataReceivedException.kt | 2 +- .../core/parser/ParserMangaRepository.kt | 4 - .../external/ExternalMangaRepository.kt | 2 +- .../kotatsu/core/ui/ReorderableListAdapter.kt | 10 +- .../kotatsu/core/util/ext/Collections.kt | 2 +- .../koitharu/kotatsu/core/util/ext/Flow.kt | 1 - .../details/ui/adapter/ChaptersAdapter.kt | 2 +- .../download/ui/worker/DownloadWorker.kt | 1 - .../kotatsu/list/ui/MangaListViewModel.kt | 1 - .../kotatsu/list/ui/model/ErrorFooter.kt | 2 - .../local/data/output/LocalMangaOutput.kt | 1 - .../main/ui/protect/ProtectViewModel.kt | 1 - .../kotatsu/reader/ui/ReaderManager.kt | 2 +- .../kotatsu/reader/ui/ReaderViewModel.kt | 1 - .../reader/ui/pager/BaseReaderFragment.kt | 1 - .../selector/ScrobblingSelectorViewModel.kt | 1 - .../settings/DownloadsSettingsFragment.kt | 1 - .../kotatsu/settings/backup/AppBackupAgent.kt | 1 - .../settings/backup/RestoreViewModel.kt | 4 - .../kotatsu/stats/data/StatsEntity.kt | 1 - .../kotatsu/stats/data/StatsRepository.kt | 4 - .../kotatsu/stats/domain/StatsRecord.kt | 8 -- .../kotatsu/stats/ui/StatsViewModel.kt | 2 +- .../stats/ui/sheet/MangaStatsViewModel.kt | 3 - .../kotatsu/stats/ui/views/PieChartView.kt | 3 +- .../kotatsu/sync/data/SyncSettings.kt | 9 +- .../tracker/ui/debug/TrackerDebugViewModel.kt | 2 +- app/src/main/res/color/ripple_toolbar.xml | 5 - app/src/main/res/drawable/bg_card_bottom.xml | 29 ----- app/src/main/res/drawable/bg_card_full.xml | 25 ----- app/src/main/res/drawable/bg_card_none.xml | 23 ---- app/src/main/res/drawable/bg_card_top.xml | 29 ----- app/src/main/res/drawable/ic_discord.xml | 11 -- app/src/main/res/drawable/ic_github.xml | 11 -- app/src/main/res/drawable/ic_list_sheet.xml | 12 --- app/src/main/res/drawable/ic_telegram.xml | 10 -- app/src/main/res/layout/dialog_onboard.xml | 30 ------ app/src/main/res/layout/item_catalog_page.xml | 19 ---- .../layout/item_source_config_checkable.xml | 58 ---------- .../main/res/layout/item_source_locale.xml | 47 -------- .../res/layout/preference_about_links.xml | 43 -------- 46 files changed, 71 insertions(+), 462 deletions(-) delete mode 100644 app/src/main/res/color/ripple_toolbar.xml delete mode 100644 app/src/main/res/drawable/bg_card_bottom.xml delete mode 100644 app/src/main/res/drawable/bg_card_full.xml delete mode 100644 app/src/main/res/drawable/bg_card_none.xml delete mode 100644 app/src/main/res/drawable/bg_card_top.xml delete mode 100644 app/src/main/res/drawable/ic_discord.xml delete mode 100644 app/src/main/res/drawable/ic_github.xml delete mode 100644 app/src/main/res/drawable/ic_list_sheet.xml delete mode 100644 app/src/main/res/drawable/ic_telegram.xml delete mode 100644 app/src/main/res/layout/dialog_onboard.xml delete mode 100644 app/src/main/res/layout/item_catalog_page.xml delete mode 100644 app/src/main/res/layout/item_source_config_checkable.xml delete mode 100644 app/src/main/res/layout/item_source_locale.xml delete mode 100644 app/src/main/res/layout/preference_about_links.xml diff --git a/app/build.gradle b/app/build.gradle index 7bad9b52c..66c8e1af5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -94,7 +94,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.activity:activity-ktx:1.9.2' - implementation 'androidx.fragment:fragment-ktx:1.8.3' + implementation 'androidx.fragment:fragment-ktx:1.8.4' implementation 'androidx.transition:transition-ktx:1.5.1' implementation 'androidx.collection:collection-ktx:1.4.4' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt index ad092b746..acc4c74f6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/Bookmark.kt @@ -17,9 +17,6 @@ data class Bookmark( val percent: Float, ) : ListModel { - val directImageUrl: String? - get() = if (isImageUrlDirect()) imageUrl else null - val imageLoadData: Any get() = if (isImageUrlDirect()) imageUrl else toMangaPage() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt index d236b74d6..159550134 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt @@ -12,7 +12,6 @@ import androidx.core.graphics.Insets import androidx.core.view.isVisible import androidx.core.view.updatePadding import dagger.hilt.android.AndroidEntryPoint -import okhttp3.internal.userAgent import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.network.CommonHeaders @@ -22,49 +21,50 @@ import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.databinding.ActivityBrowserBinding +import org.koitharu.kotatsu.parsers.model.MangaSource import javax.inject.Inject import com.google.android.material.R as materialR @AndroidEntryPoint class BrowserActivity : BaseActivity(), BrowserCallback { - private lateinit var onBackPressedCallback: WebViewBackPressedCallback - - @Inject - lateinit var mangaRepositoryFactory: MangaRepository.Factory - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { - return - } - supportActionBar?.run { - setDisplayHomeAsUpEnabled(true) - setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) - } - val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE)) - val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository - val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT) - viewBinding.webView.configureForParser(userAgent) - CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true) - viewBinding.webView.webViewClient = BrowserClient(this) - viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) - onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) - onBackPressedDispatcher.addCallback(onBackPressedCallback) - if (savedInstanceState != null) { - return - } - val url = intent?.dataString - if (url.isNullOrEmpty()) { - finishAfterTransition() - } else { - onTitleChanged( - intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_), - url, - ) - viewBinding.webView.loadUrl(url) - } - } + private lateinit var onBackPressedCallback: WebViewBackPressedCallback + + @Inject + lateinit var mangaRepositoryFactory: MangaRepository.Factory + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { + return + } + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) + } + val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE)) + val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository + val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT) + viewBinding.webView.configureForParser(userAgent) + CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true) + viewBinding.webView.webViewClient = BrowserClient(this) + viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) + onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) + onBackPressedDispatcher.addCallback(onBackPressedCallback) + if (savedInstanceState != null) { + return + } + val url = intent?.dataString + if (url.isNullOrEmpty()) { + finishAfterTransition() + } else { + onTitleChanged( + intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_), + url, + ) + viewBinding.webView.loadUrl(url) + } + } override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) @@ -136,17 +136,17 @@ class BrowserActivity : BaseActivity(), BrowserCallback bottom = insets.bottom, ) } - - companion object { - - private const val EXTRA_TITLE = "title" - private const val EXTRA_SOURCE = "source" - - fun newIntent(context: Context, url: String, source: MangaSource?, title: String?): Intent { - return Intent(context, BrowserActivity::class.java) - .setData(Uri.parse(url)) - .putExtra(EXTRA_TITLE, title) - .putExtra(EXTRA_SOURCE, source?.name) - } - } + + companion object { + + private const val EXTRA_TITLE = "title" + private const val EXTRA_SOURCE = "source" + + fun newIntent(context: Context, url: String, source: MangaSource?, title: String?): Intent { + return Intent(context, BrowserActivity::class.java) + .setData(Uri.parse(url)) + .putExtra(EXTRA_TITLE, title) + .putExtra(EXTRA_SOURCE, source?.name) + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index f1abcfdb2..cf47f00b2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -1,7 +1,6 @@ package org.koitharu.kotatsu.core import android.app.Application -import android.content.ContentResolver import android.content.Context import android.provider.SearchRecentSuggestions import android.text.Html diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CloudFlareBlockedException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CloudFlareBlockedException.kt index 244dba9f1..4860e4880 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CloudFlareBlockedException.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/CloudFlareBlockedException.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.core.exceptions -import okhttp3.Headers import okio.IOException import org.koitharu.kotatsu.parsers.model.MangaSource diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/NoDataReceivedException.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/NoDataReceivedException.kt index f8de32a1e..3594ce18b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/NoDataReceivedException.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/NoDataReceivedException.kt @@ -3,5 +3,5 @@ package org.koitharu.kotatsu.core.exceptions import okio.IOException class NoDataReceivedException( - private val url: String, + url: String, ) : IOException("No data has been received from $url") diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt index 82a614891..12f4cfcee 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/ParserMangaRepository.kt @@ -8,7 +8,6 @@ import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.config.ConfigKey -import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.Favicons import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter @@ -17,13 +16,10 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaParserSource -import org.koitharu.kotatsu.parsers.model.MangaState -import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import java.util.Locale class ParserMangaRepository( private val parser: MangaParser, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt index 2f4e9db24..05083c982 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalMangaRepository.kt @@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.util.SuspendLazy import java.util.EnumSet class ExternalMangaRepository( - private val contentResolver: ContentResolver, + contentResolver: ContentResolver, override val source: ExternalMangaSource, cache: MemoryContentCache, ) : CachingMangaRepository(cache) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt index c34cb8382..738309af5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt @@ -28,10 +28,12 @@ open class ReorderableListAdapter : ListDelegationAdapter listListeners.forEach { it.onCurrentListChanged(oldList, newList) } } - @Deprecated("Use emit() to dispatch list updates", level = DeprecationLevel.ERROR) - override fun setItems(items: List?) { - super.setItems(items) - } + @Deprecated( + message = "Use emit() to dispatch list updates", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("emit(items)"), + ) + override fun setItems(items: List?) = super.setItems(items) fun reorderItems(oldPos: Int, newPos: Int) { Collections.swap(items ?: return, oldPos, newPos) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt index 8af44c883..a7455af10 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Collections.kt @@ -92,7 +92,7 @@ fun LongSet.toLongArray(): LongArray { return result } -fun LongSet.toSet(): Set = toCollection(ArraySet(size)) +fun LongSet.toSet(): Set = toCollection(ArraySet(size)) fun > LongSet.toCollection(out: R): R = out.also { result -> forEach(result::add) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt index a8db54adc..b5d922e70 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt @@ -17,7 +17,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformWhile -import org.koitharu.kotatsu.R import org.koitharu.kotatsu.parsers.util.SuspendLazy import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt index 5035401c2..ae83503c1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt @@ -12,7 +12,7 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListModel class ChaptersAdapter( - private val onItemClickListener: OnListItemClickListener, + onItemClickListener: OnListItemClickListener, ) : BaseListAdapter(), FastScroller.SectionIndexer { private var hasVolumes = false diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index 0f985027b..651be3654 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -71,7 +71,6 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.TempFileFilter -import org.koitharu.kotatsu.local.data.index.LocalMangaIndex import org.koitharu.kotatsu.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.data.output.LocalMangaOutput import org.koitharu.kotatsu.local.domain.MangaLock diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 5278dbf56..0090b8e10 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -22,7 +22,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga -import org.koitharu.kotatsu.parsers.model.MangaTag abstract class MangaListViewModel( private val settings: AppSettings, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ErrorFooter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ErrorFooter.kt index 55e414919..119ed411a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ErrorFooter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/model/ErrorFooter.kt @@ -1,7 +1,5 @@ package org.koitharu.kotatsu.list.ui.model -import androidx.annotation.DrawableRes - data class ErrorFooter( val exception: Throwable, ) : ListModel { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt index b94c04ddf..c150ac6a4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaOutput.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import okhttp3.internal.format import okio.Closeable import org.koitharu.kotatsu.core.prefs.DownloadFormat import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt index b9a407a4e..77af614f5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/protect/ProtectViewModel.kt @@ -8,7 +8,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call -import org.koitharu.kotatsu.parsers.util.isNumeric import org.koitharu.kotatsu.parsers.util.md5 import javax.inject.Inject diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt index bb045deb8..53069dd1b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderManager.kt @@ -19,7 +19,7 @@ import java.util.EnumMap class ReaderManager( private val fragmentManager: FragmentManager, private val container: FragmentContainerView, - private val settings: AppSettings, + settings: AppSettings, ) { private val modeMap = EnumMap>>(ReaderMode::class.java) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index a0341f7ba..852fba563 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt index 04fa68a81..c4ac87358 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BaseReaderFragment.kt @@ -7,7 +7,6 @@ import androidx.viewbinding.ViewBinding import org.koitharu.kotatsu.core.prefs.ReaderAnimation import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.widgets.ZoomControl -import org.koitharu.kotatsu.core.util.ext.getParcelableCompat import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.reader.ui.ReaderState diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt index 217cee55a..0542c61d2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/scrobbling/common/ui/selector/ScrobblingSelectorViewModel.kt @@ -20,7 +20,6 @@ import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call -import org.koitharu.kotatsu.core.util.ext.ifZero import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.requireValue diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt index 1a2922728..ffcb1e10f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/DownloadsSettingsFragment.kt @@ -17,7 +17,6 @@ import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.DownloadFormat -import org.koitharu.kotatsu.core.prefs.ReaderAnimation import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.resolveFile diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt index 919728c4d..cc076775e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt @@ -15,7 +15,6 @@ import org.koitharu.kotatsu.core.backup.BackupZipOutput import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.File import java.io.FileDescriptor import java.io.FileInputStream diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt index 9acb88717..bef9212ad 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt @@ -1,8 +1,6 @@ package org.koitharu.kotatsu.settings.backup -import android.content.ContentResolver import android.content.Context -import android.net.Uri import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -13,8 +11,6 @@ import org.koitharu.kotatsu.core.backup.BackupEntry import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupZipInput import org.koitharu.kotatsu.core.backup.CompositeResult -import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException -import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsEntity.kt index 0889b3f53..9ebef64a7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsEntity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsEntity.kt @@ -3,7 +3,6 @@ package org.koitharu.kotatsu.stats.data import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey -import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.history.data.HistoryEntity @Entity( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsRepository.kt index 7facb67e0..9b621e1b0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/data/StatsRepository.kt @@ -1,10 +1,7 @@ package org.koitharu.kotatsu.stats.data -import androidx.collection.LongIntMap -import androidx.collection.MutableLongIntMap import androidx.room.withTransaction import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -13,7 +10,6 @@ import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.observeAsFlow -import org.koitharu.kotatsu.core.util.ext.combine import org.koitharu.kotatsu.stats.domain.StatsPeriod import org.koitharu.kotatsu.stats.domain.StatsRecord import java.util.NavigableMap diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsRecord.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsRecord.kt index 190f8efe1..7e4c9e739 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsRecord.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/domain/StatsRecord.kt @@ -1,17 +1,9 @@ package org.koitharu.kotatsu.stats.domain -import android.content.Context -import androidx.annotation.ColorInt -import androidx.core.content.ContextCompat -import androidx.core.graphics.ColorUtils -import com.google.android.material.R -import com.google.android.material.color.MaterialColors -import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.details.data.ReadingTime import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.parsers.model.Manga import java.util.concurrent.TimeUnit -import kotlin.math.absoluteValue data class StatsRecord( val manga: Manga?, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsViewModel.kt index 40b07d252..f6a309b22 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/StatsViewModel.kt @@ -21,7 +21,7 @@ import javax.inject.Inject @HiltViewModel class StatsViewModel @Inject constructor( private val repository: StatsRepository, - private val favouritesRepository: FavouritesRepository, + favouritesRepository: FavouritesRepository, ) : BaseViewModel() { val period = MutableStateFlow(StatsPeriod.WEEK) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsViewModel.kt index f81b4a6bd..9fc1f782b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/sheet/MangaStatsViewModel.kt @@ -1,10 +1,8 @@ package org.koitharu.kotatsu.stats.ui.sheet import androidx.collection.IntList -import androidx.collection.LongIntMap import androidx.collection.MutableIntList import androidx.collection.emptyIntList -import androidx.collection.emptyLongIntMap import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -15,7 +13,6 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.stats.data.StatsRepository -import org.koitharu.kotatsu.stats.domain.StatsRecord import java.time.Instant import java.util.concurrent.TimeUnit import javax.inject.Inject diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/PieChartView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/PieChartView.kt index 6f2cc29cb..178e25f18 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/PieChartView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/stats/ui/views/PieChartView.kt @@ -13,6 +13,7 @@ import androidx.core.graphics.ColorUtils import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.parsers.util.replaceWith +import kotlin.math.atan2 import kotlin.math.sqrt class PieChartView @JvmOverloads constructor( @@ -130,7 +131,7 @@ class PieChartView @JvmOverloads constructor( if (distance < chartBounds.height() / 4f || distance > chartBounds.centerX()) { return -1 } - var touchAngle = Math.toDegrees(Math.atan2(dy, dx)).toFloat() + var touchAngle = Math.toDegrees(atan2(dy, dx)).toFloat() if (touchAngle < 0) { touchAngle += 360 } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt index e686e1349..9a0651b44 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncSettings.kt @@ -4,7 +4,6 @@ import android.accounts.Account import android.accounts.AccountManager import android.content.Context import androidx.annotation.WorkerThread -import dagger.Reusable import dagger.hilt.android.qualifiers.ApplicationContext import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty @@ -30,11 +29,11 @@ class SyncSettings( @set:WorkerThread var syncURL: String get() = account?.let { - val sync_url = accountManager.getUserData(it, KEY_SYNC_URL) - if ( !sync_url.startsWith("http://") && !sync_url.startsWith("https://") ) { - return "http://$sync_url" + val result = accountManager.getUserData(it, KEY_SYNC_URL) + if (!result.startsWith("http://") && !result.startsWith("https://")) { + return "http://$result" } - return sync_url + return result }.ifNullOrEmpty { defaultSyncUrl } set(value) { account?.let { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt index cf591c685..b9b8866ec 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt @@ -16,7 +16,7 @@ import javax.inject.Inject @HiltViewModel class TrackerDebugViewModel @Inject constructor( - private val db: MangaDatabase + db: MangaDatabase ) : BaseViewModel() { val content = db.getTracksDao().observeAll() diff --git a/app/src/main/res/color/ripple_toolbar.xml b/app/src/main/res/color/ripple_toolbar.xml deleted file mode 100644 index 979d0f94a..000000000 --- a/app/src/main/res/color/ripple_toolbar.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/bg_card_bottom.xml b/app/src/main/res/drawable/bg_card_bottom.xml deleted file mode 100644 index f693f7c2a..000000000 --- a/app/src/main/res/drawable/bg_card_bottom.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_card_full.xml b/app/src/main/res/drawable/bg_card_full.xml deleted file mode 100644 index 316531284..000000000 --- a/app/src/main/res/drawable/bg_card_full.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_card_none.xml b/app/src/main/res/drawable/bg_card_none.xml deleted file mode 100644 index 2e6a50b46..000000000 --- a/app/src/main/res/drawable/bg_card_none.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/bg_card_top.xml b/app/src/main/res/drawable/bg_card_top.xml deleted file mode 100644 index 145b73611..000000000 --- a/app/src/main/res/drawable/bg_card_top.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_discord.xml b/app/src/main/res/drawable/ic_discord.xml deleted file mode 100644 index 73fde1989..000000000 --- a/app/src/main/res/drawable/ic_discord.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml deleted file mode 100644 index 55a2da656..000000000 --- a/app/src/main/res/drawable/ic_github.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_list_sheet.xml b/app/src/main/res/drawable/ic_list_sheet.xml deleted file mode 100644 index 2a7ccb3a9..000000000 --- a/app/src/main/res/drawable/ic_list_sheet.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_telegram.xml b/app/src/main/res/drawable/ic_telegram.xml deleted file mode 100644 index c18d3a442..000000000 --- a/app/src/main/res/drawable/ic_telegram.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/layout/dialog_onboard.xml b/app/src/main/res/layout/dialog_onboard.xml deleted file mode 100644 index 2ca236818..000000000 --- a/app/src/main/res/layout/dialog_onboard.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/item_catalog_page.xml b/app/src/main/res/layout/item_catalog_page.xml deleted file mode 100644 index d43cdb9db..000000000 --- a/app/src/main/res/layout/item_catalog_page.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/item_source_config_checkable.xml b/app/src/main/res/layout/item_source_config_checkable.xml deleted file mode 100644 index 9bb6ae9ad..000000000 --- a/app/src/main/res/layout/item_source_config_checkable.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_source_locale.xml b/app/src/main/res/layout/item_source_locale.xml deleted file mode 100644 index 71c333a98..000000000 --- a/app/src/main/res/layout/item_source_locale.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/preference_about_links.xml b/app/src/main/res/layout/preference_about_links.xml deleted file mode 100644 index b5c0ad8ee..000000000 --- a/app/src/main/res/layout/preference_about_links.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - From 1f1309d93486427ff1f4a194a241ab5de9ef80fa Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 3 Oct 2024 14:32:34 +0300 Subject: [PATCH 006/109] Increase source add button size --- .../kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt | 4 ++-- .../settings/sources/catalog/SourceCatalogItemAD.kt | 10 ++++++++++ app/src/main/res/layout/activity_sources_catalog.xml | 5 +++-- app/src/main/res/layout/item_source_catalog.xml | 8 +++----- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt index e5b713d8f..185c3af4a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Theme.kt @@ -29,7 +29,7 @@ fun Context.getThemeColor( @Px fun Context.getThemeDimensionPixelSize( @AttrRes resId: Int, - @ColorInt fallback: Int = 0, + @Px fallback: Int = 0, ) = obtainStyledAttributes(intArrayOf(resId)).use { it.getDimensionPixelSize(0, fallback) } @@ -37,7 +37,7 @@ fun Context.getThemeDimensionPixelSize( @Px fun Context.getThemeDimensionPixelOffset( @AttrRes resId: Int, - @ColorInt fallback: Int = 0, + @Px fallback: Int = 0, ) = obtainStyledAttributes(intArrayOf(resId)).use { it.getDimensionPixelOffset(0, fallback) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt index 035437529..35c94f045 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/catalog/SourceCatalogItemAD.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.sources.catalog import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.core.view.updatePaddingRelative import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding @@ -16,12 +17,14 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ext.crossfade import org.koitharu.kotatsu.core.util.ext.drawableStart import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelOffset import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding import org.koitharu.kotatsu.list.ui.model.ListModel +import com.google.android.material.R as materialR fun sourceCatalogItemSourceAD( coil: ImageLoader, @@ -39,6 +42,13 @@ fun sourceCatalogItemSourceAD( binding.root.setOnClickListener { v -> listener.onItemClick(item, v) } + val basePadding = context.getThemeDimensionPixelOffset( + materialR.attr.listPreferredItemPaddingEnd, + binding.root.paddingStart, + ) + binding.root.updatePaddingRelative( + end = (basePadding - context.resources.getDimensionPixelOffset(R.dimen.margin_small)).coerceAtLeast(0), + ) bind { binding.textViewTitle.text = item.source.getTitle(context) diff --git a/app/src/main/res/layout/activity_sources_catalog.xml b/app/src/main/res/layout/activity_sources_catalog.xml index e2c1a7b09..4e9273ec6 100644 --- a/app/src/main/res/layout/activity_sources_catalog.xml +++ b/app/src/main/res/layout/activity_sources_catalog.xml @@ -12,8 +12,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true" - app:liftOnScrollColor="@null" - app:liftOnScroll="false"> + app:liftOnScroll="false" + app:liftOnScrollColor="@null"> diff --git a/app/src/main/res/layout/item_source_catalog.xml b/app/src/main/res/layout/item_source_catalog.xml index e770233b4..8618bd413 100644 --- a/app/src/main/res/layout/item_source_catalog.xml +++ b/app/src/main/res/layout/item_source_catalog.xml @@ -9,9 +9,8 @@ android:gravity="center_vertical" android:minHeight="?listPreferredItemHeightSmall" android:orientation="horizontal" - android:paddingVertical="@dimen/margin_small" android:paddingStart="?listPreferredItemPaddingStart" - android:paddingEnd="?listPreferredItemPaddingEnd"> + tools:ignore="RtlSymmetry"> From 1290db4a7c0f3ce5592ab0d88cd64dc0a3c5fca4 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Oct 2024 10:23:49 +0300 Subject: [PATCH 007/109] Fixes batch --- app/build.gradle | 8 +-- .../core/parser/MangaLoaderContextImpl.kt | 4 +- .../kotatsu/core/util/ext/Throwable.kt | 12 ++++ .../koitharu/kotatsu/core/zip/ZipOutput.kt | 6 +- .../download/ui/list/DownloadItemAD.kt | 5 ++ .../filter/ui/tags/TagsCatalogViewModel.kt | 67 +++++++++++++++---- .../local/data/LocalMangaRepository.kt | 4 +- .../local/data/index/LocalMangaIndex.kt | 9 ++- .../local/data/index/LocalMangaIndexDao.kt | 3 + .../remotelist/ui/RemoteListFragment.kt | 12 ++-- .../remotelist/ui/RemoteListViewModel.kt | 8 +-- .../kotatsu/tracker/ui/debug/TrackDebugAD.kt | 10 +-- 12 files changed, 106 insertions(+), 42 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 66c8e1af5..1d65bb8a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 674 - versionName = '7.6.1' + versionCode = 675 + versionName = '7.6.2' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -64,7 +64,7 @@ android { } lint { abortOnError true - disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled' + disable 'MissingTranslation', 'PrivateResource', 'SetJavaScriptEnabled' } testOptions { unitTests.includeAndroidResources true @@ -83,7 +83,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:a8df8665ae') { + implementation('com.github.KotatsuApp:kotatsu-parsers:645006fde8') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt index b6fb9f9f3..824aea5d2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt @@ -7,6 +7,7 @@ import android.util.Base64 import android.webkit.WebView import androidx.annotation.MainThread import androidx.core.os.LocaleListCompat +import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -30,6 +31,7 @@ import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.util.map +import org.koitharu.kotatsu.parsers.util.mimeType import java.lang.ref.WeakReference import java.util.Locale import javax.inject.Inject @@ -86,7 +88,7 @@ class MangaLoaderContextImpl @Inject constructor( result.compressTo(it.outputStream()) }.asResponseBody("image/jpeg".toMediaType()) } - } ?: error("Cannot decode bitmap") + } ?: throw ImageDecodeException(response.request.url.toString(), response.mimeType) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt index 85afb772c..41a51ce63 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Throwable.kt @@ -113,6 +113,18 @@ fun Throwable.getDisplayIcon() = when (this) { else -> R.drawable.ic_error_large } +fun Throwable.getCauseUrl(): String? = when (this) { + is ParseException -> url + is NotFoundException -> url + is TooManyRequestExceptions -> url + is CaughtException -> cause?.getCauseUrl() + is CloudFlareBlockedException -> url + is CloudFlareProtectedException -> url + is HttpStatusException -> url + is HttpException -> response.request.url.toString() + else -> null +} + private fun getHttpDisplayMessage(statusCode: Int, resources: Resources): String? = when (statusCode) { 404 -> resources.getString(R.string.not_found_404) in 500..599 -> resources.getString(R.string.server_error, statusCode) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt index 88b435350..82378614a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt @@ -6,6 +6,7 @@ import okio.Closeable import org.koitharu.kotatsu.core.util.ext.withChildren import java.io.File import java.io.FileInputStream +import java.util.concurrent.atomic.AtomicBoolean import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipFile @@ -17,7 +18,7 @@ class ZipOutput( ) : Closeable { private val entryNames = ArraySet() - private var isClosed = false + private val isClosed = AtomicBoolean(false) private val output = ZipOutputStream(file.outputStream()).apply { setLevel(compressionLevel) } @@ -72,9 +73,8 @@ class ZipOutput( } override fun close() { - if (!isClosed) { + if (isClosed.compareAndSet(false, true)) { output.close() - isClosed = true } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt index 274dd0a4d..ec7e77736 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt @@ -124,6 +124,7 @@ fun downloadItemAD( binding.buttonCancel.isVisible = true binding.buttonResume.isVisible = false binding.buttonSkip.isVisible = false + binding.buttonSkipAll.isVisible = false binding.buttonPause.isVisible = false } @@ -147,6 +148,7 @@ fun downloadItemAD( binding.buttonResume.isVisible = item.isPaused binding.buttonResume.setText(if (item.error == null) R.string.resume else R.string.retry) binding.buttonSkip.isVisible = item.isPaused && item.error != null + binding.buttonSkipAll.isVisible = item.isPaused && item.error != null binding.buttonPause.isVisible = item.canPause } @@ -169,6 +171,7 @@ fun downloadItemAD( binding.buttonCancel.isVisible = false binding.buttonResume.isVisible = false binding.buttonSkip.isVisible = false + binding.buttonSkipAll.isVisible = false binding.buttonPause.isVisible = false } @@ -182,6 +185,7 @@ fun downloadItemAD( binding.buttonCancel.isVisible = false binding.buttonResume.isVisible = false binding.buttonSkip.isVisible = false + binding.buttonSkipAll.isVisible = false binding.buttonPause.isVisible = false } @@ -195,6 +199,7 @@ fun downloadItemAD( binding.buttonCancel.isVisible = false binding.buttonResume.isVisible = false binding.buttonSkip.isVisible = false + binding.buttonSkipAll.isVisible = false binding.buttonPause.isVisible = false } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt index 5b83142b1..1cc380bd4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogViewModel.kt @@ -10,22 +10,27 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus +import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.model.FilterProperty import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorState +import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaTag @HiltViewModel(assistedFactory = TagsCatalogViewModel.Factory::class) class TagsCatalogViewModel @AssistedInject constructor( @Assisted private val filter: FilterCoordinator, @Assisted private val isExcluded: Boolean, + private val mangaDataRepository: MangaDataRepository, ) : BaseViewModel() { val searchQuery = MutableStateFlow("") @@ -33,23 +38,13 @@ class TagsCatalogViewModel @AssistedInject constructor( private val filterProperty: StateFlow> get() = if (isExcluded) filter.tagsExcluded else filter.tags + @Suppress("RemoveExplicitTypeArguments") private val tags: StateFlow> = combine( filter.getAllTags(), + flow> { emit(emptyList()); emit(mangaDataRepository.findTags(filter.mangaSource)) }, filterProperty.map { it.selectedItems }, - ) { all, selected -> - all.fold( - onSuccess = { - it.map { tag -> - TagCatalogItem( - tag = tag, - isChecked = tag in selected, - ) - } - }, - onFailure = { - listOf(it.toErrorState(false)) - }, - ) + ) { available, cached, selected -> + buildList(available, cached, selected) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) val content = combine(tags, searchQuery) { raw, query -> @@ -66,6 +61,50 @@ class TagsCatalogViewModel @AssistedInject constructor( } } + private fun buildList( + available: Result>, + cached: Collection, + selected: Set, + ): List { + val capacity = (available.getOrNull()?.size ?: 1) + cached.size + val result = ArrayList(capacity) + val added = HashSet(capacity) + available.getOrNull()?.forEach { tag -> + if (added.add(tag.title)) { + result.add( + TagCatalogItem( + tag = tag, + isChecked = tag in selected, + ), + ) + } + } + cached.forEach { tag -> + if (added.add(tag.title)) { + result.add( + TagCatalogItem( + tag = tag, + isChecked = tag in selected, + ), + ) + } + } + if (result.isNotEmpty()) { + val locale = (filter.mangaSource as? MangaParserSource)?.locale + result.sortWith(compareBy(TagTitleComparator(locale)) { (it as TagCatalogItem).tag }) + } + available.exceptionOrNull()?.let { error -> + result.add( + if (result.isEmpty()) { + error.toErrorState(canRetry = false) + } else { + error.toErrorFooter() + }, + ) + } + return result + } + @AssistedFactory interface Factory { fun create(filter: FilterCoordinator, isExcludeTag: Boolean): TagsCatalogViewModel diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index 7a3206e44..aa9cb07a2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -76,7 +76,9 @@ class LocalMangaRepository @Inject constructor( } override suspend fun getFilterOptions() = MangaListFilterOptions( - availableTags = localMangaIndex.getAvailableTags().mapToSet { MangaTag(title = it, key = it, source = source) }, + availableTags = localMangaIndex.getAvailableTags( + skipNsfw = settings.isNsfwContentDisabled, + ).mapToSet { MangaTag(title = it, key = it, source = source) }, availableContentRating = if (!settings.isNsfwContentDisabled) { EnumSet.of(ContentRating.SAFE, ContentRating.ADULT) } else { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt index e7b6757d6..53caf3c53 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndex.kt @@ -83,8 +83,13 @@ class LocalMangaIndex @Inject constructor( db.getLocalMangaIndexDao().delete(mangaId) } - suspend fun getAvailableTags(): List { - return db.getLocalMangaIndexDao().findTags() + suspend fun getAvailableTags(skipNsfw: Boolean): List { + val dao = db.getLocalMangaIndexDao() + return if (skipNsfw) { + dao.findTags(isNsfw = false) + } else { + dao.findTags() + } } private suspend fun upsert(manga: LocalManga) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt index 8b1b6f1b0..21038c6ae 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/index/LocalMangaIndexDao.kt @@ -13,6 +13,9 @@ interface LocalMangaIndexDao { @Query("SELECT title FROM local_index LEFT JOIN manga_tags ON manga_tags.manga_id = local_index.manga_id LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id WHERE title IS NOT NULL GROUP BY title") suspend fun findTags(): List + @Query("SELECT title FROM local_index LEFT JOIN manga_tags ON manga_tags.manga_id = local_index.manga_id LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id WHERE (SELECT nsfw FROM manga WHERE manga.manga_id = local_index.manga_id) = :isNsfw AND title IS NOT NULL GROUP BY title") + suspend fun findTags(isNsfw: Boolean): List + @Upsert suspend fun upsert(entity: LocalMangaIndexEntity) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 41257db6f..d07dedd78 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.util.ext.addMenuProvider +import org.koitharu.kotatsu.core.util.ext.getCauseUrl import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.withArgs @@ -72,24 +73,23 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner { if (filterCoordinator.isFilterApplied) { filterCoordinator.reset() } else { - openInBrowser() + openInBrowser(null) } } override fun onSecondaryErrorActionClick(error: Throwable) { - openInBrowser() + openInBrowser(error.getCauseUrl()) } - private fun openInBrowser() { - val browserUrl = viewModel.browserUrl - if (browserUrl.isNullOrEmpty()) { + private fun openInBrowser(url: String?) { + if (url.isNullOrEmpty()) { Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT) .show() } else { startActivity( BrowserActivity.newIntent( requireContext(), - browserUrl, + url, viewModel.source, viewModel.source.getTitle(requireContext()), ), diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index c3b948f18..b397f429a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -21,10 +21,10 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.distinctById import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.getCauseUrl import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.sizeOrZero import org.koitharu.kotatsu.download.ui.worker.DownloadWorker @@ -39,7 +39,6 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorState -import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.Manga import javax.inject.Inject @@ -68,9 +67,6 @@ open class RemoteListViewModel @Inject constructor( private var loadingJob: Job? = null private var randomJob: Job? = null - val browserUrl: String? - get() = (repository as? ParserMangaRepository)?.domain?.let { "https://$it" } - override val content = combine( mangaList.map { it?.skipNsfwIfNeeded() }, observeListModeWithTriggers(), @@ -82,7 +78,7 @@ open class RemoteListViewModel @Inject constructor( list.isNullOrEmpty() && error != null -> add( error.toErrorState( canRetry = true, - secondaryAction = if (error !is NotFoundException && browserUrl != null) R.string.open_in_browser else 0, + secondaryAction = if (error.getCauseUrl().isNullOrEmpty()) 0 else R.string.open_in_browser, ), ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt index e1a62459a..e305e5f8e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt @@ -43,17 +43,17 @@ fun trackDebugAD( } binding.textViewTitle.text = item.manga.title binding.textViewSummary.text = buildSpannedString { - item.lastCheckTime?.let { - append( + append( + item.lastCheckTime?.let { DateUtils.getRelativeDateTimeString( context, it.toEpochMilli(), DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0, - ), - ) - } + ) + } ?: getString(R.string.never), + ) if (item.lastResult == TrackEntity.RESULT_FAILED) { append(" - ") bold { From 5301cc7f9704cf7ea943384775499a37b65301ed Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Oct 2024 11:20:49 +0300 Subject: [PATCH 008/109] Ability to start download paused --- .../core/ui/dialog/CommonAlertDialogs.kt | 25 +++++++++++++++++++ .../ui/pager/ChaptersPagesViewModel.kt | 1 + .../download/ui/worker/DownloadWorker.kt | 21 +++++++++++++--- .../kotatsu/list/ui/MangaListFragment.kt | 9 +++++-- .../kotatsu/list/ui/MangaListViewModel.kt | 4 +-- .../kotatsu/search/ui/multi/SearchActivity.kt | 8 ++++-- .../search/ui/multi/SearchViewModel.kt | 4 +-- .../kotatsu/tracker/work/TrackWorker.kt | 1 + app/src/main/res/values/strings.xml | 3 +++ 9 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CommonAlertDialogs.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CommonAlertDialogs.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CommonAlertDialogs.kt new file mode 100644 index 000000000..22b32d910 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/dialog/CommonAlertDialogs.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.core.ui.dialog + +import android.content.Context +import androidx.annotation.UiContext +import org.koitharu.kotatsu.R + +object CommonAlertDialogs { + + fun showDownloadConfirmation( + @UiContext context: Context, + onConfirmed: (startPaused: Boolean) -> Unit, + ) = buildAlertDialog(context, isCentered = true) { + var startPaused = false + setTitle(R.string.save_manga) + setIcon(R.drawable.ic_download) + setMessage(R.string.save_manga_confirm) + setCheckbox(R.string.start_download, true) { _, isChecked -> + startPaused = !isChecked + } + setPositiveButton(R.string.save) { _, _ -> + onConfirmed(startPaused) + } + setNegativeButton(android.R.string.cancel, null) + }.show() +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt index 8dca45cf1..ae3517dd1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/ChaptersPagesViewModel.kt @@ -168,6 +168,7 @@ abstract class ChaptersPagesViewModel( downloadScheduler.schedule( manga = requireManga(), chaptersIds = chaptersIds, + isPaused = false, isSilent = false, ) onDownloadStarted.call(Unit) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index 651be3654..9fe15de56 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -128,7 +128,11 @@ class DownloadWorker @AssistedInject constructor( val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() } val downloadedIds = getDoneChapters(manga) return try { - withContext(PausingHandle()) { + val pausingHandle = PausingHandle() + if (inputData.getBoolean(START_PAUSED, false)) { + pausingHandle.pause() + } + withContext(pausingHandle) { downloadMangaImpl(manga, chaptersIds, downloadedIds) } Result.success(currentState.toWorkData()) @@ -431,10 +435,16 @@ class DownloadWorker @AssistedInject constructor( private val settings: AppSettings, ) { - suspend fun schedule(manga: Manga, chaptersIds: Collection?, isSilent: Boolean) { + suspend fun schedule( + manga: Manga, + chaptersIds: Collection?, + isPaused: Boolean, + isSilent: Boolean, + ) { dataRepository.storeManga(manga) val data = Data.Builder() .putLong(MANGA_ID, manga.id) + .putBoolean(START_PAUSED, isPaused) .putBoolean(IS_SILENT, isSilent) if (!chaptersIds.isNullOrEmpty()) { data.putLongArray(CHAPTERS_IDS, chaptersIds.toLongArray()) @@ -442,11 +452,15 @@ class DownloadWorker @AssistedInject constructor( scheduleImpl(listOf(data.build())) } - suspend fun schedule(manga: Collection) { + suspend fun schedule( + manga: Collection, + isPaused: Boolean, + ) { val data = manga.map { dataRepository.storeManga(it) Data.Builder() .putLong(MANGA_ID, it.id) + .putBoolean(START_PAUSED, isPaused) .build() } scheduleImpl(data) @@ -556,6 +570,7 @@ class DownloadWorker @AssistedInject constructor( const val MANGA_ID = "manga_id" const val CHAPTERS_IDS = "chapters" const val IS_SILENT = "silent" + const val START_PAUSED = "paused" const val TAG = "download" } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 4efd19b52..771ac4103 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -27,6 +27,7 @@ import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.ui.BaseFragment +import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog import org.koitharu.kotatsu.core.ui.list.FitHeightGridLayoutManager import org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager @@ -238,6 +239,7 @@ abstract class MangaListFragment : } override fun onFilterOptionClick(option: ListFilterOption) { + selectionController?.clear() (viewModel as? QuickFilterListener)?.toggleFilterOption(option) } @@ -322,8 +324,11 @@ abstract class MangaListFragment : } R.id.action_save -> { - viewModel.download(selectedItems) - mode?.finish() + val itemsSnapshot = selectedItems + CommonAlertDialogs.showDownloadConfirmation(context ?: return false) { startPaused -> + mode?.finish() + viewModel.download(itemsSnapshot, isPaused = startPaused) + } true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 0090b8e10..700c26e62 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -46,9 +46,9 @@ abstract class MangaListViewModel( abstract fun onRetry() - fun download(items: Set) { + fun download(items: Set, isPaused: Boolean) { launchJob(Dispatchers.Default) { - downloadScheduler.schedule(items) + downloadScheduler.schedule(items, isPaused) onDownloadStarted.call(Unit) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt index 975dad0d4..095f817f6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchActivity.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs import org.koitharu.kotatsu.core.ui.list.ListSelectionController import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.widgets.TipView @@ -184,8 +185,11 @@ class SearchActivity : } R.id.action_save -> { - viewModel.download(collectSelectedItems()) - mode?.finish() + val itemsSnapshot = collectSelectedItems() + CommonAlertDialogs.showDownloadConfirmation(this) { startPaused -> + mode?.finish() + viewModel.download(itemsSnapshot, isPaused = startPaused) + } true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt index 3edd17e9e..c0d3d52aa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/SearchViewModel.kt @@ -109,9 +109,9 @@ class SearchViewModel @Inject constructor( retryCounter.value += 1 } - fun download(items: Set) { + fun download(items: Set, isPaused: Boolean) { launchJob(Dispatchers.Default) { - downloadScheduler.schedule(items) + downloadScheduler.schedule(items, isPaused) onDownloadStarted.call(Unit) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index 0c2cc96c8..4b2d3b9c1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -254,6 +254,7 @@ class TrackWorker @AssistedInject constructor( downloadSchedulerLazy.get().schedule( manga = mangaUpdates.manga, chaptersIds = mangaUpdates.newChapters.mapToSet { it.id }, + isPaused = false, isSilent = true, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a5bcf862c..c1b0ecfaa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -738,4 +738,7 @@ User manual Telegram group Unsupported image format: %s + Start download + Save selected manga? This may consume traffic and disk space + Save manga From 4ad2f3f608d2f0e00d23c8cce4f24f9c512159dd Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Oct 2024 14:27:01 +0300 Subject: [PATCH 009/109] Fixes batch --- .../kotatsu/alternatives/ui/AutoFixService.kt | 9 +++++---- .../kotatsu/core/ErrorReporterReceiver.kt | 9 +++++++-- .../kotatsu/core/ui/ReorderableListAdapter.kt | 6 ++++-- .../ui/worker/DownloadNotificationFactory.kt | 16 +++++++++------- .../koitharu/kotatsu/local/ui/ImportService.kt | 6 ++++-- .../kotatsu/settings/SettingsActivity.kt | 15 +++++++++------ .../sources/manage/SourcesListProducer.kt | 11 +++++++---- .../sources/manage/SourcesManageFragment.kt | 7 +++++-- 8 files changed, 50 insertions(+), 29 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt index 9fa0fdf42..aa6f7d436 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt @@ -165,13 +165,14 @@ class AutoFixService : CoroutineIntentService() { } else { error.getDisplayMessage(applicationContext.resources) }, - ) - .setSmallIcon(android.R.drawable.stat_notify_error) - .addAction( + ).setSmallIcon(android.R.drawable.stat_notify_error) + ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> + notification.addAction( R.drawable.ic_alert_outline, applicationContext.getString(R.string.report), - ErrorReporterReceiver.getPendingIntent(applicationContext, error), + reportIntent, ) + } } return notification.build() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ErrorReporterReceiver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ErrorReporterReceiver.kt index 7d190422e..628093428 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ErrorReporterReceiver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ErrorReporterReceiver.kt @@ -5,9 +5,11 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.net.Uri +import android.os.BadParcelableException import androidx.core.app.PendingIntentCompat import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.report class ErrorReporterReceiver : BroadcastReceiver() { @@ -22,12 +24,15 @@ class ErrorReporterReceiver : BroadcastReceiver() { private const val EXTRA_ERROR = "err" private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR" - fun getPendingIntent(context: Context, e: Throwable): PendingIntent { + fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try { val intent = Intent(context, ErrorReporterReceiver::class.java) intent.setAction(ACTION_REPORT) intent.setData(Uri.parse("err://${e.hashCode()}")) intent.putExtra(EXTRA_ERROR, e) - return checkNotNull(PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)) + PendingIntentCompat.getBroadcast(context, 0, intent, 0, false) + } catch (e: BadParcelableException) { + e.printStackTraceDebug() + null } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt index 738309af5..97543d692 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.withContext import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.model.ListModel -import java.util.Collections +import org.koitharu.kotatsu.parsers.util.move import java.util.LinkedList open class ReorderableListAdapter : ListDelegationAdapter>(), FlowCollector?> { @@ -36,7 +36,9 @@ open class ReorderableListAdapter : ListDelegationAdapter override fun setItems(items: List?) = super.setItems(items) fun reorderItems(oldPos: Int, newPos: Int) { - Collections.swap(items ?: return, oldPos, newPos) + val reordered = items?.toMutableList() ?: return + reordered.move(oldPos, newPos) + super.setItems(reordered) notifyItemMoved(oldPos, newPos) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt index 03f23e5cf..ece4835a5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -213,13 +213,15 @@ class DownloadNotificationFactory @AssistedInject constructor( builder.setWhen(System.currentTimeMillis()) builder.setStyle(NotificationCompat.BigTextStyle().bigText(state.errorMessage)) if (state.error.isReportable()) { - builder.addAction( - NotificationCompat.Action( - 0, - context.getString(R.string.report), - ErrorReporterReceiver.getPendingIntent(context, state.error), - ), - ) + ErrorReporterReceiver.getPendingIntent(context, state.error)?.let { reportIntent -> + builder.addAction( + NotificationCompat.Action( + 0, + context.getString(R.string.report), + reportIntent, + ), + ) + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt index f033847c2..8ff8ca534 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt @@ -139,11 +139,13 @@ class ImportService : CoroutineIntentService() { notification.setContentTitle(applicationContext.getString(R.string.error_occurred)) .setContentText(error.getDisplayMessage(applicationContext.resources)) .setSmallIcon(android.R.drawable.stat_notify_error) - .addAction( + ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> + notification.addAction( R.drawable.ic_alert_outline, applicationContext.getString(R.string.report), - ErrorReporterReceiver.getPendingIntent(applicationContext, error), + reportIntent, ) + } } return notification.build() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt index 2f8d648d1..1112924e2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -10,6 +10,7 @@ import androidx.core.graphics.Insets import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit import androidx.preference.Preference @@ -70,10 +71,12 @@ class SettingsActivity : caller: PreferenceFragmentCompat, pref: Preference, ): Boolean { - val fm = supportFragmentManager - val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false) - fragment.arguments = pref.extras - openFragment(fragment, isFromRoot = caller is RootSettingsFragment) + val fragmentName = pref.fragment ?: return false + openFragment( + fragmentClass = FragmentFactory.loadFragmentClass(classLoader, fragmentName), + args = pref.peekExtras(), + isFromRoot = caller is RootSettingsFragment, + ) return true } @@ -93,11 +96,11 @@ class SettingsActivity : } ?: setTitle(title ?: getString(R.string.settings)) } - fun openFragment(fragment: Fragment, isFromRoot: Boolean) { + fun openFragment(fragmentClass: Class, args: Bundle?, isFromRoot: Boolean) { val hasFragment = supportFragmentManager.findFragmentById(R.id.container) != null supportFragmentManager.commit { setReorderingAllowed(true) - replace(R.id.container, fragment) + replace(R.id.container, fragmentClass, args) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) if (!isMasterDetails || (hasFragment && !isFromRoot)) { addToBackStack(null) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt index 85b06587b..7df5129f6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt @@ -18,10 +18,13 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.db.TABLE_SOURCES import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.isNsfw +import org.koitharu.kotatsu.core.model.unwrap import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.lifecycleScope import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.SourcesSortOrder +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import javax.inject.Inject @@ -63,8 +66,8 @@ class SourcesListProducer @Inject constructor( } private suspend fun buildList(): List { - val enabledSources = repository.getEnabledSources() - val pinned = repository.getPinnedSources() + val enabledSources = repository.getEnabledSources().filter { it.unwrap() is MangaParserSource } + val pinned = repository.getPinnedSources().mapToSet { it.name } val isNsfwDisabled = settings.isNsfwContentDisabled val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER) @@ -79,7 +82,7 @@ class SourcesListProducer @Inject constructor( isEnabled = it in enabledSet, isDraggable = false, isAvailable = !isNsfwDisabled || !it.isNsfw(), - isPinned = it in pinned, + isPinned = it.name in pinned, ) }.ifEmpty { listOf(SourceConfigItem.EmptySearchResult) @@ -100,7 +103,7 @@ class SourcesListProducer @Inject constructor( isEnabled = true, isDraggable = isReorderAvailable, isAvailable = false, - isPinned = it in pinned, + isPinned = it.name in pinned, ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageFragment.kt index b970465cc..d0b19015b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageFragment.kt @@ -106,8 +106,11 @@ class SourcesManageFragment : } override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) { - val fragment = SourceSettingsFragment.newInstance(item.source) - (activity as? SettingsActivity)?.openFragment(fragment, false) + (activity as? SettingsActivity)?.openFragment( + fragmentClass = SourceSettingsFragment::class.java, + args = Bundle(1).apply { putString(SourceSettingsFragment.EXTRA_SOURCE, item.source.name) }, + isFromRoot = false, + ) } override fun onItemLiftClick(item: SourceConfigItem.SourceItem) { From 1d285388937667e62ceee3327269dc17ff520e58 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Fri, 4 Oct 2024 14:27:01 +0300 Subject: [PATCH 010/109] Fixes batch --- app/build.gradle | 3 +-- .../kotlin/org/koitharu/kotatsu/KotatsuApp.kt | 1 - .../kotatsu/alternatives/ui/AutoFixService.kt | 9 ++++---- .../kotatsu/core/ErrorReporterReceiver.kt | 9 ++++++-- .../exceptions/resolve/ExceptionResolver.kt | 21 ++++++++++--------- .../kotatsu/core/ui/ReorderableListAdapter.kt | 6 ++++-- .../koitharu/kotatsu/core/util/ext/Android.kt | 11 ++++++++++ .../ui/worker/DownloadNotificationFactory.kt | 16 +++++++------- .../kotatsu/local/ui/ImportService.kt | 6 ++++-- .../main/domain/CoverRestoreInterceptor.kt | 11 ++++++---- .../koitharu/kotatsu/main/ui/MainActivity.kt | 4 ++-- .../kotatsu/settings/SettingsActivity.kt | 15 +++++++------ .../sources/manage/SourcesListProducer.kt | 11 ++++++---- .../sources/manage/SourcesManageFragment.kt | 7 +++++-- 14 files changed, 82 insertions(+), 48 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1d65bb8a5..96238c85b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,8 +82,7 @@ afterEvaluate { } } dependencies { - //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:645006fde8') { + implementation('com.github.KotatsuApp:kotatsu-parsers:1.2.1') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index 7426b5575..1e9dd8a44 100644 --- a/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -63,7 +63,6 @@ class KotatsuApp : BaseApp() { detectRetainInstanceUsage() detectSetUserVisibleHint() detectWrongNestedHierarchy() - detectTargetFragmentUsage() detectFragmentReuse() penaltyLog() if (notifier != null) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt index 9fa0fdf42..aa6f7d436 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt @@ -165,13 +165,14 @@ class AutoFixService : CoroutineIntentService() { } else { error.getDisplayMessage(applicationContext.resources) }, - ) - .setSmallIcon(android.R.drawable.stat_notify_error) - .addAction( + ).setSmallIcon(android.R.drawable.stat_notify_error) + ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> + notification.addAction( R.drawable.ic_alert_outline, applicationContext.getString(R.string.report), - ErrorReporterReceiver.getPendingIntent(applicationContext, error), + reportIntent, ) + } } return notification.build() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ErrorReporterReceiver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ErrorReporterReceiver.kt index 7d190422e..628093428 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ErrorReporterReceiver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ErrorReporterReceiver.kt @@ -5,9 +5,11 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.net.Uri +import android.os.BadParcelableException import androidx.core.app.PendingIntentCompat import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.report class ErrorReporterReceiver : BroadcastReceiver() { @@ -22,12 +24,15 @@ class ErrorReporterReceiver : BroadcastReceiver() { private const val EXTRA_ERROR = "err" private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR" - fun getPendingIntent(context: Context, e: Throwable): PendingIntent { + fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try { val intent = Intent(context, ErrorReporterReceiver::class.java) intent.setAction(ACTION_REPORT) intent.setData(Uri.parse("err://${e.hashCode()}")) intent.putExtra(EXTRA_ERROR, e) - return checkNotNull(PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)) + PendingIntentCompat.getBroadcast(context, 0, intent, 0, false) + } catch (e: BadParcelableException) { + e.printStackTraceDebug() + null } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt index 44bd5d976..d843dd0d6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt @@ -6,7 +6,6 @@ import androidx.activity.result.ActivityResultCaller import androidx.annotation.StringRes import androidx.collection.MutableScatterMap import androidx.fragment.app.FragmentManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -19,7 +18,8 @@ import org.koitharu.kotatsu.core.exceptions.ProxyConfigException import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog -import org.koitharu.kotatsu.core.util.ext.findActivity +import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog +import org.koitharu.kotatsu.core.util.ext.restartApplication import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.Manga @@ -124,15 +124,16 @@ class ExceptionResolver @AssistedInject constructor( Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show() return } - MaterialAlertDialogBuilder(ctx) - .setTitle(R.string.ignore_ssl_errors) - .setMessage(R.string.ignore_ssl_errors_summary) - .setPositiveButton(R.string.apply) { _, _ -> + buildAlertDialog(ctx) { + setTitle(R.string.ignore_ssl_errors) + setMessage(R.string.ignore_ssl_errors_summary) + setPositiveButton(R.string.apply) { _, _ -> settings.isSSLBypassEnabled = true - Toast.makeText(ctx, R.string.settings_apply_restart_required, Toast.LENGTH_SHORT).show() - ctx.findActivity()?.finishAffinity() - }.setNegativeButton(android.R.string.cancel, null) - .show() + Toast.makeText(ctx, R.string.settings_apply_restart_required, Toast.LENGTH_LONG).show() + ctx.restartApplication() + } + setNegativeButton(android.R.string.cancel, null) + }.show() } private inline fun Host.withContext(block: Context.() -> Unit) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt index 738309af5..97543d692 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/ReorderableListAdapter.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.withContext import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.model.ListModel -import java.util.Collections +import org.koitharu.kotatsu.parsers.util.move import java.util.LinkedList open class ReorderableListAdapter : ListDelegationAdapter>(), FlowCollector?> { @@ -36,7 +36,9 @@ open class ReorderableListAdapter : ListDelegationAdapter override fun setItems(items: List?) = super.setItems(items) fun reorderItems(oldPos: Int, newPos: Int) { - Collections.swap(items ?: return, oldPos, newPos) + val reordered = items?.toMutableList() ?: return + reordered.move(oldPos, newPos) + super.setItems(reordered) notifyItemMoved(oldPos, newPos) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt index d86bcd5c2..7f1a649e0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt @@ -7,10 +7,12 @@ import android.app.ActivityManager import android.app.ActivityManager.MemoryInfo import android.app.ActivityOptions import android.app.LocaleConfig +import android.content.ComponentName import android.content.Context import android.content.Context.ACTIVITY_SERVICE import android.content.Context.POWER_SERVICE import android.content.ContextWrapper +import android.content.Intent import android.content.OperationApplicationException import android.content.SharedPreferences import android.content.SyncResult @@ -33,6 +35,7 @@ import androidx.annotation.IntegerRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDialog +import androidx.core.app.ActivityCompat import androidx.core.app.ActivityOptionsCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -61,6 +64,7 @@ import okio.use import org.json.JSONException import org.jsoup.internal.StringUtil.StringJoiner import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.main.ui.MainActivity import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException @@ -274,3 +278,10 @@ fun WebView.configureForParser(userAgentOverride: String?) = with(settings) { userAgentString = userAgentOverride } } + +fun Context.restartApplication() { + val activity = findActivity() + val intent = Intent.makeRestartActivityTask(ComponentName(this, MainActivity::class.java)) + startActivity(intent) + activity?.finishAndRemoveTask() +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt index 03f23e5cf..ece4835a5 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadNotificationFactory.kt @@ -213,13 +213,15 @@ class DownloadNotificationFactory @AssistedInject constructor( builder.setWhen(System.currentTimeMillis()) builder.setStyle(NotificationCompat.BigTextStyle().bigText(state.errorMessage)) if (state.error.isReportable()) { - builder.addAction( - NotificationCompat.Action( - 0, - context.getString(R.string.report), - ErrorReporterReceiver.getPendingIntent(context, state.error), - ), - ) + ErrorReporterReceiver.getPendingIntent(context, state.error)?.let { reportIntent -> + builder.addAction( + NotificationCompat.Action( + 0, + context.getString(R.string.report), + reportIntent, + ), + ) + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt index f033847c2..8ff8ca534 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt @@ -139,11 +139,13 @@ class ImportService : CoroutineIntentService() { notification.setContentTitle(applicationContext.getString(R.string.error_occurred)) .setContentText(error.getDisplayMessage(applicationContext.resources)) .setSmallIcon(android.R.drawable.stat_notify_error) - .addAction( + ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> + notification.addAction( R.drawable.ic_alert_outline, applicationContext.getString(R.string.report), - ErrorReporterReceiver.getPendingIntent(applicationContext, error), + reportIntent, ) + } } return notification.build() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt index fcf77379c..9f27b6147 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestoreInterceptor.kt @@ -10,9 +10,9 @@ import org.jsoup.HttpStatusException import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.core.model.findById +import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository -import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.exception.ParseException @@ -70,10 +70,10 @@ class CoverRestoreInterceptor @Inject constructor( } private suspend fun restoreMangaImpl(manga: Manga): Boolean { - if (dataRepository.findMangaById(manga.id) == null) { + if (dataRepository.findMangaById(manga.id) == null || manga.isLocal) { return false } - val repo = repositoryFactory.create(manga.source) as? ParserMangaRepository ?: return false + val repo = repositoryFactory.create(manga.source) val fixed = repo.find(manga) ?: return false return if (fixed != manga) { dataRepository.storeManga(fixed) @@ -100,7 +100,10 @@ class CoverRestoreInterceptor @Inject constructor( } private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean { - val repo = repositoryFactory.create(bookmark.manga.source) as? ParserMangaRepository ?: return false + if (bookmark.manga.isLocal) { + return false + } + val repo = repositoryFactory.create(bookmark.manga.source) val chapter = repo.getDetails(bookmark.manga).chapters?.findById(bookmark.chapterId) ?: return false val page = repo.getPages(chapter)[bookmark.page] val imageUrl = page.preview.ifNullOrEmpty { page.url } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index 384fbd453..3e567ede1 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -55,7 +55,7 @@ import org.koitharu.kotatsu.details.service.MangaPrefetchService import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.favourites.ui.container.FavouritesContainerFragment import org.koitharu.kotatsu.history.ui.HistoryListFragment -import org.koitharu.kotatsu.local.data.index.LocalMangaIndex +import org.koitharu.kotatsu.local.ui.LocalIndexUpdateService import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner @@ -352,7 +352,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav MangaPrefetchService.prefetchLast(this@MainActivity) requestNotificationsPermission() } - startService(Intent(this@MainActivity, LocalMangaIndex::class.java)) + startService(Intent(this@MainActivity, LocalIndexUpdateService::class.java)) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt index 2f8d648d1..1112924e2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -10,6 +10,7 @@ import androidx.core.graphics.Insets import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.commit import androidx.preference.Preference @@ -70,10 +71,12 @@ class SettingsActivity : caller: PreferenceFragmentCompat, pref: Preference, ): Boolean { - val fm = supportFragmentManager - val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false) - fragment.arguments = pref.extras - openFragment(fragment, isFromRoot = caller is RootSettingsFragment) + val fragmentName = pref.fragment ?: return false + openFragment( + fragmentClass = FragmentFactory.loadFragmentClass(classLoader, fragmentName), + args = pref.peekExtras(), + isFromRoot = caller is RootSettingsFragment, + ) return true } @@ -93,11 +96,11 @@ class SettingsActivity : } ?: setTitle(title ?: getString(R.string.settings)) } - fun openFragment(fragment: Fragment, isFromRoot: Boolean) { + fun openFragment(fragmentClass: Class, args: Bundle?, isFromRoot: Boolean) { val hasFragment = supportFragmentManager.findFragmentById(R.id.container) != null supportFragmentManager.commit { setReorderingAllowed(true) - replace(R.id.container, fragment) + replace(R.id.container, fragmentClass, args) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) if (!isMasterDetails || (hasFragment && !isFromRoot)) { addToBackStack(null) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt index 85b06587b..7df5129f6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesListProducer.kt @@ -18,10 +18,13 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.db.TABLE_SOURCES import org.koitharu.kotatsu.core.model.getTitle import org.koitharu.kotatsu.core.model.isNsfw +import org.koitharu.kotatsu.core.model.unwrap import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.lifecycleScope import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.SourcesSortOrder +import org.koitharu.kotatsu.parsers.model.MangaParserSource +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import javax.inject.Inject @@ -63,8 +66,8 @@ class SourcesListProducer @Inject constructor( } private suspend fun buildList(): List { - val enabledSources = repository.getEnabledSources() - val pinned = repository.getPinnedSources() + val enabledSources = repository.getEnabledSources().filter { it.unwrap() is MangaParserSource } + val pinned = repository.getPinnedSources().mapToSet { it.name } val isNsfwDisabled = settings.isNsfwContentDisabled val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER) @@ -79,7 +82,7 @@ class SourcesListProducer @Inject constructor( isEnabled = it in enabledSet, isDraggable = false, isAvailable = !isNsfwDisabled || !it.isNsfw(), - isPinned = it in pinned, + isPinned = it.name in pinned, ) }.ifEmpty { listOf(SourceConfigItem.EmptySearchResult) @@ -100,7 +103,7 @@ class SourcesListProducer @Inject constructor( isEnabled = true, isDraggable = isReorderAvailable, isAvailable = false, - isPinned = it in pinned, + isPinned = it.name in pinned, ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageFragment.kt index b970465cc..d0b19015b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageFragment.kt @@ -106,8 +106,11 @@ class SourcesManageFragment : } override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) { - val fragment = SourceSettingsFragment.newInstance(item.source) - (activity as? SettingsActivity)?.openFragment(fragment, false) + (activity as? SettingsActivity)?.openFragment( + fragmentClass = SourceSettingsFragment::class.java, + args = Bundle(1).apply { putString(SourceSettingsFragment.EXTRA_SOURCE, item.source.name) }, + isFromRoot = false, + ) } override fun onItemLiftClick(item: SourceConfigItem.SourceItem) { From b6f618101f37672e524f768b76f0a89f71e91ec1 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 5 Oct 2024 00:07:23 +0200 Subject: [PATCH 011/109] Translated using Weblate (Portuguese) Currently translated at 98.7% (719 of 728 strings) Co-authored-by: Matt Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/ Translation: Kotatsu/Strings --- app/src/main/res/values-pt/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 2b50f8366..3dcab7d09 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -712,4 +712,5 @@ Grupo Telegram Conjunto de imagens Jogo CG + sfw \ No newline at end of file From 2e2a818c05a1669afdab4a5541645fd06290d825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sat, 5 Oct 2024 00:07:23 +0200 Subject: [PATCH 012/109] Translated using Weblate (Estonian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 71.0% (520 of 732 strings) Translated using Weblate (Estonian) Currently translated at 67.9% (495 of 728 strings) Translated using Weblate (Estonian) Currently translated at 100.0% (9 of 9 strings) Co-authored-by: Priit Jõerüüt Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/et/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/et/ Translation: Kotatsu/Strings Translation: Kotatsu/plurals --- app/src/main/res/values-et/plurals.xml | 8 ++ app/src/main/res/values-et/strings.xml | 119 +++++++++++++++++++++---- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/values-et/plurals.xml b/app/src/main/res/values-et/plurals.xml index 47b8933d0..0f97bd76f 100644 --- a/app/src/main/res/values-et/plurals.xml +++ b/app/src/main/res/values-et/plurals.xml @@ -28,4 +28,12 @@ %1$d tund tagasi %1$d tundi tagasi + + %1$d minut + %1$d minutit + + + %1$d tund + %1$d tundi + \ No newline at end of file diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 0ccb636aa..d982a92ba 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -57,7 +57,7 @@ Peata allalaadimised kui vahetad mobiilsele ühendusele Lugemis mood Sisemine mälu - Importitud + Imporditud Näita lugemis progressi näitaja Loe hiljem Tagavara salvestatud @@ -76,7 +76,7 @@ Näita inforiba lugejas Parool peab olema 4 tähemärki või veel Sereri aadress - Uued peatükid mangast mis sa loed näidatakse siin + Uued loetavad mangast peatükid näidatakse siin Hakka lugema mangat ja sa saad isikupärastatud soovitusi Seikle Leija samasuguseid @@ -101,8 +101,8 @@ Ja tühjenda informatsioon uute peatükkide kohta Eemaldada kõik hiljutised otsingupäringud? Midagi pole siin - Eemalda lõpetatud - Kaust allalaadimiseks + Eemalda loetud mangad + Allalaadimiste kaust Loen Lubatud Laadimine… @@ -111,8 +111,8 @@ Webtooni zoom Erinevad keeled Loetelu mood - Eemalda lõppetatud - Alla laetud + Eemaldatud + Allalaaaditud Miku Muuda CAPTCHA vajatud @@ -153,7 +153,7 @@ Pole manga allikaid Valge Täpsusta žanrid, mida te ei soovi ettepanekutes näha - Kustuta \"%s\" seadelt? + Kas kustutame \"%s\" nutiseadmest jäädavalt? Sisu eellaeb Vajuta Tagasi, et lahkuda skaala mood @@ -220,7 +220,7 @@ Suurus: %s Näita lehekülje vahetamise liugurit Lülita sisse manga allikaid, et lugeda mangat online - Kohta + Rakenduse teave Tühista Sobita keskele Valikud @@ -272,7 +272,7 @@ Näita mitu protsenti on loetud ajaloos ja lemmikutes Suvakas Vali peegeldus automaatselt - Kasuta näppujälge, kui see on olemas + Kasuta biomeetriat, kui see on olemas Standard Ajalugu Mitte midagi leitud @@ -292,7 +292,7 @@ Jaga pilt Välja lülitatud Kaua aega tagasi - Vajutades paremal äärele või paremale nuppule alati vahetub järgmisele leheküljele + Äre kohenda lugemisvaates lehtede vahetamise suunad, näoteks vajutades paremal asuvat nuppu alati suundud järgmisele leheküljele. See eelistus mõjutab vaid eraldi raudvaral põhinevaid sisendseadmeid Loetelu Teate seaded Keel @@ -309,7 +309,7 @@ Halda Logi välja B|kB|MB|GB|TB - Lõpetatud + Loetud Jätka Ruudustik Hiljutised @@ -321,7 +321,7 @@ Loe veel Nulli filter Otsi %s-st - Kukkutatud + Lõpetatud 18+ Lülita välja NSFW Töötlen… @@ -370,14 +370,14 @@ Korda parooli Kõik andmed on taastatud Mion - Automaatselt vaheta domeene manga allika jaoks veaga kui peegeldused on saadaval + Kui peegelserverid on saadaval, siis vea puhul vaheta mangade laadimiseks vajalikke domeene automaatselt Pole järjehoidjaid veel Tänan ei Sa saad teha tagavara enda ajaloost, lemmikutest ja siis taastada need Jaga looge Hoija stardis Väline mälu - Salvesta see interneti allikast või impordidtud failist. + Salvesta midagi võrgupõhisest kataloogist või impordi failist. Kiirus Vaikne Salvesta midagi esimesena @@ -420,6 +420,95 @@ Tühjendage võrgu vahemälu Kasutage wsrv.nl teenust liikluskasutuse vähendamiseks ja võimalusel piltide laadimise kiirendamiseks Allalaadimised on jätkanud - Värvide ümberpööramine + Pööra värvid tagurpidi Puhverserver + Sinu otsingutingimustele ei vasta ühtegi mangat + Näita alanurgas lehekülje numbreid + Kõik lugemata peatükid + Järgmised lugemata %s + Kohalikud kaustad manga jaoks + Kirjeldus + Sel kuul + Taust + Selle eelistusega lülitad suumimise nupud vaate all paremas nurgas sisse või välja + Loendi valikud + Viimatiloetud + Ühtegi objekti ei saa enam lisada + Jätkamaks sisesta oma e-posti aadress ja salasõna + Hele + Must + Tume + Suumi välja + Märgi loetuks + Vaade ruudustikuna + Näita ekraani ülaosas praegust aega ja lugemise edenemist + Valge + Manga lugemise ajal ära lülita ekraani välja + Hoia ekraan sisselülitatuna + Kas soovid märkida valitud manga läbiloetuks? +\n +\nHoiatus: sellega kustub ka praeguseks salvestatud lugemise olek. + See kategooria on põhivaatest peidetud ja on leitav siit: Menüü → Halda kategooriaid + Kustuta vaid valitud domeenide küpsised. Enamusel juhtudel tähendab see ka autentimise katkemist vastavas saidis + Kõik peatükid %s tõlkega + Vali peatükid käsitsi + Sul puudub ligipääs sellele failile või kaustale + Häälotsing + Sarnane manga + Vali enda eelistatud kaust + Töös + Ära uuenda soovitusi mahupõhiste võrguühenduste puhul + Ära kontrolli uute peatükkide olemasolu mahupõhiste võrguühenduste puhul + %s eeldab korralikuks toimimiseks robotilõksu lahendamist + Mangade loend + Päringuvastuses on vigased andmed või fail on vigane + Seadmes + Kaustad + Põhivaate valikud + Üles + Nihutasime üles + Kategooriad + Olulisus + Vigane serveri aadress + Terve manga + Esimesed %s + Kõik lugemata peatükid (%s) + Andmed jäid taastamata + Palun kontrolli, et oled valinud õige varundusfaili + Liiga palju päringuid. Proovi uuesti %s + Suumi sisse + Näita suuminuppe + Lõpetatud + Sellega läheb graafika ilusamaks, aga võib väheneda nutiseadme jõudlus + 32-bitine värvirežiim + Peale rakenduse uuendamist soovita uusi andmeallikaid + Peale rakenduse uuendamist küsi, kas soovid kasutusele võtta uusi mangade allikaid + Automaatne + Andmete kolimne on lõppenud + Proovi uuesti + Lukusta ekraani paigutus + Iga 2 päeva järel + Kord nädalas + Kaks korda kuus + Kord kuus + Tee valitud ajavahemiku järel varukoopiaid + Varukoopiate kaust + Viimane õnnestunud varukoopia: %s + x%.1f + Manga + Muu + Jäta vahele + Halltoonides + Veebis leiduv variant + Regulaarsed varukoopiad + Varukoopiate tegemise sagedus + Iga päev + Näita menüüd + Näita/peida kasutajaliides + Eelmine peatükk + Järgmine peatükk + Eelmine leht + Järgmine leht + Hentai + Koomiks \ No newline at end of file From a57fcce72be079849e7703159a5cbeff32a28094 Mon Sep 17 00:00:00 2001 From: Infy's Tagalog Translations Date: Sat, 5 Oct 2024 00:07:23 +0200 Subject: [PATCH 013/109] Translated using Weblate (Filipino) Currently translated at 98.3% (717 of 729 strings) Translated using Weblate (Filipino) Currently translated at 98.3% (716 of 728 strings) Co-authored-by: Infy's Tagalog Translations Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/ Translation: Kotatsu/Strings --- app/src/main/res/values-fil/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 6996c6a10..ce1dd9103 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -708,4 +708,8 @@ Ang source na ito ay hindi sinusuportahan ang paghahanap na may mga filter. Ang iyong mga filter ay na-clear Demograpiko Set ng mga imahe + Mag-debug + Manwal ng gumagamit + Grupo sa Telegram + Di-suportadong format ng imahe: %s \ No newline at end of file From 357517ceacfe828beb2c50611cd52dae41f2e7ef Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 5 Oct 2024 00:07:23 +0200 Subject: [PATCH 014/109] Translated using Weblate (Spanish) Currently translated at 100.0% (732 of 732 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (729 of 729 strings) Co-authored-by: gallegonovato Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/ Translation: Kotatsu/Strings --- app/src/main/res/values-es/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index bbe6f80b5..d8bea057f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -723,4 +723,8 @@ Código fuente Grupo de Telegram Depurar + Formato de imagen no compatible: %s + Iniciar descarga + Guardar manga + ¿Guardar el manga seleccionado? Esto puede consumir tráfico y espacio en disco \ No newline at end of file From e9cd32c8708deaec785b9e08c0f3e40f195e4833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sat, 5 Oct 2024 00:07:24 +0200 Subject: [PATCH 015/109] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (732 of 732 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (729 of 729 strings) Co-authored-by: Oğuz Ersen Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/ Translation: Kotatsu/Strings --- app/src/main/res/values-tr/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 8de84ae26..5b3fd8988 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -723,4 +723,8 @@ Kullanıcı kılavuzu Kaynak kodu Telegram grubu + Desteklenmeyen resim biçimi: %s + İndirmeye başla + Seçilen manga kaydedilsin mi? Bu veri kullanabilir ve disk alanı tüketebilir + Mangayı kaydet \ No newline at end of file From cab56209c1061b466c66c9735d800513d68b3d1d Mon Sep 17 00:00:00 2001 From: gekka <1778962971@qq.com> Date: Sat, 5 Oct 2024 00:07:24 +0200 Subject: [PATCH 016/109] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (732 of 732 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (729 of 729 strings) Co-authored-by: gekka <1778962971@qq.com> Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/ Translation: Kotatsu/Strings --- app/src/main/res/values-zh-rCN/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index dea397e49..15ca9178d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -723,4 +723,8 @@ 用户手册 Telegram 群 有已下载章节的漫画 + 图片文件不受支持:%s + 下载漫画 + 开始下载 + 要下载选择的漫画吗?下载漫画会消耗流量和磁盘空间 \ No newline at end of file From 357308bfbbd1879d37bd9f8056a1a20ba95236a1 Mon Sep 17 00:00:00 2001 From: abc0922001 Date: Sat, 5 Oct 2024 00:07:24 +0200 Subject: [PATCH 017/109] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 90.5% (660 of 729 strings) Co-authored-by: abc0922001 Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/ Translation: Kotatsu/Strings --- app/src/main/res/values-zh-rTW/strings.xml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index cb4524d9c..bc6b4e944 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -31,7 +31,7 @@ 在歷史紀錄與最愛中顯示閱讀百分比 再點擊一次返回鍵以退出 本機儲存區 - 點擊右側邊緣或按下右鍵,總是切換到下一頁。 + 不要根據閱讀模式調整翻頁方向,例如,按右鍵總是切換到下一頁。此選項僅影響硬體輸入設備 儲存或放棄未儲存的變更? 您可以從儲存區中刪除原始檔案以節省空間 最愛 @@ -181,7 +181,7 @@ 登出 移除書籤 書籤 - 使用指紋,如果可用 + 若可用,請使用生物辨識 您最愛的漫畫 您最近閱讀的漫畫 資料刪除 @@ -639,4 +639,20 @@ 此功能將為選定的漫畫尋找替代來源。此任務將需要一些時間,並會在背景中進行 提供依照特定參數過濾漫畫列表的功能 不相容的外掛或內部錯誤。請確保您使用的是最新版本的外掛與 Kotatsu + 此來源不支援帶有過濾條件的搜尋。您的過濾條件已被清除 + 沒有符合您所選過濾條件的漫畫 + 請求過多。請在 %s 之後再試 + %1$d 分 %2$d 秒 + 停用 NSFW 通知 + + 已下載章節的漫畫 + 漫畫「%1$s」(%2$s)已被「%3$s」(%4$s)取代 + 無效的伺服器位址 + %d 秒 + 停用連線檢查 + 不顯示有關 NSFW 漫畫更新的通知 + 檢查新章節日誌 + 有關背景檢查新章節的偵錯資訊 + 來源已停用 + 停用 \ No newline at end of file From ba9f31835fc924aa56f913957aaebc9098bbdf04 Mon Sep 17 00:00:00 2001 From: Draken Date: Sat, 5 Oct 2024 00:07:24 +0200 Subject: [PATCH 018/109] Translated using Weblate (Vietnamese) Currently translated at 100.0% (732 of 732 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (729 of 729 strings) Co-authored-by: Draken Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/ Translation: Kotatsu/Strings --- app/src/main/res/values-vi/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 54702fed8..ed0e15bea 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -723,4 +723,8 @@ Hướng dẫn sử dụng Nhóm Telegram Gỡ lỗi + Định dạng hình ảnh không được hỗ trợ: %s + Lưu manga + Bắt đầu tải về + Lưu manga đã chọn? Điều này sẽ tiêu tốn bộ nhớ của máy và dữ liệu mạng của bạn \ No newline at end of file From 9358617a3aeb416c93a8f471c482bd885f80c516 Mon Sep 17 00:00:00 2001 From: Anon Date: Sat, 5 Oct 2024 00:07:24 +0200 Subject: [PATCH 019/109] Translated using Weblate (Serbian) Currently translated at 100.0% (729 of 729 strings) Translated using Weblate (Serbian) Currently translated at 96.4% (703 of 729 strings) Co-authored-by: Anon Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/ Translation: Kotatsu/Strings --- app/src/main/res/values-sr/strings.xml | 33 +++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 3380aab42..8f286c812 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -422,7 +422,7 @@ Рикка Онемогући Давно - Додиривање десне ивице, или притискање десне стрелице, увек прелази на следећу страницу. + Не прилагођавај смер пребацивања страница на режим читача, нпр. притиском на десни тастер увек се прелази на следећу страницу. Ова опција утиче само на хардверске улазне уређаје Режим без чувања Можеш направити обележивач док читаш мангу Мамими @@ -693,4 +693,35 @@ Ова функција ће пронаћи алтернативне изворе за изабрану мангу. Задатак ће потрајати и наставиће се у позадини Манга „%1$s“ (%2$s) је замењена са „%3$s“ (%4$s) Нису пронађене алтернативе за „%s“ + Роман + Манхуа + Манхва + Недавно додано + Додато давно + Популарно данас + Популарно овог месеца + Популарно ове године + Оригинални језик + Година + Демографија + Схоунен + Схоујо + Сеинен + Јосеи + Године + Било који + Кодомо + Доујинсхи + Сет слика + Отклањање грешака + Изворни код + Упутство за употребу + Телеграм група + Популарно овог часа + Популарно ове седмице + Уметник ЦГ + Неподржан формат слике: %s + Овај извор не подржава претрагу са филтерима. Ваши филтери су обрисани + Један ударац + Игра ЦГ \ No newline at end of file From b46c00f2d0cd870165ab965fa4a23c0d3becfb28 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 5 Oct 2024 16:28:29 +0300 Subject: [PATCH 020/109] Fix parsers version --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 96238c85b..0d14e1a8a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:1.2.1') { + implementation('com.github.KotatsuApp:kotatsu-parsers:6f7e1fcfb2') { exclude group: 'org.json', module: 'json' } From 4faef85086abd4babca4141cbfbad45cadc0e3dd Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 7 Oct 2024 14:40:29 +0300 Subject: [PATCH 021/109] Fix sources suggestion --- .../search/ui/suggestion/SearchSuggestionViewModel.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt index c604a2c11..507d15734 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/SearchSuggestionViewModel.kt @@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.util.ext.sizeOrZero import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import javax.inject.Inject @@ -103,7 +104,7 @@ class SearchSuggestionViewModel @Inject constructor( suggestionJob?.cancel() suggestionJob = combine( query.debounce(DEBOUNCE_TIMEOUT), - sourcesRepository.observeEnabledSources().map { it.toSet() }, + sourcesRepository.observeEnabledSources().map { it.mapToSet { x -> x.name } }, settings.observeAsFlow(AppSettings.KEY_SEARCH_SUGGESTION_TYPES) { searchSuggestionTypes }, ::Triple, ).mapLatest { (searchQuery, enabledSources, types) -> @@ -116,7 +117,7 @@ class SearchSuggestionViewModel @Inject constructor( private suspend fun buildSearchSuggestion( searchQuery: String, - enabledSources: Set, + enabledSources: Set, types: Set, ): List = coroutineScope { val queriesDeferred = if (SearchSuggestionType.QUERIES_RECENT in types) { @@ -169,7 +170,7 @@ class SearchSuggestionViewModel @Inject constructor( if (!mangaList.isNullOrEmpty()) { add(SearchSuggestionItem.MangaList(mangaList)) } - sources?.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) } + sources?.mapTo(this) { SearchSuggestionItem.Source(it, it.name in enabledSources) } queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) } authors?.mapTo(this) { SearchSuggestionItem.Author(it) } hints?.mapTo(this) { SearchSuggestionItem.Hint(it) } From 9ea1122ca094bc0bd53ea5a3198894695b5b67d5 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 7 Oct 2024 15:24:02 +0300 Subject: [PATCH 022/109] Fix CloudFlare protection detection (close #1129) --- app/build.gradle | 6 +- .../browser/cloudflare/CloudFlareActivity.kt | 4 +- .../browser/cloudflare/CloudFlareClient.kt | 8 +-- .../core/network/CloudFlareInterceptor.kt | 58 ++++++++++--------- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0d14e1a8a..02ee0fe0e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 675 - versionName = '7.6.2' + versionCode = 676 + versionName = '7.6.3' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -82,7 +82,7 @@ afterEvaluate { } } dependencies { - implementation('com.github.KotatsuApp:kotatsu-parsers:6f7e1fcfb2') { + implementation('com.github.KotatsuApp:kotatsu-parsers:1ebb298cd7') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt index b75711ae4..09738c113 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt @@ -32,6 +32,7 @@ import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.network.CloudFlareHelper import javax.inject.Inject import com.google.android.material.R as materialR @@ -175,8 +176,7 @@ class CloudFlareActivity : BaseActivity(), CloudFlareCal private suspend fun clearCfCookies(url: HttpUrl) = runInterruptible(Dispatchers.Default) { cookieJar.removeCookies(url) { cookie -> - val name = cookie.name - name.startsWith("cf_") || name.startsWith("_cf") || name.startsWith("__cf") || name == "csrftoken" + CloudFlareHelper.isCloudFlareCookie(cookie.name) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt index 7bdd0aada..e5bd0c5cb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt @@ -2,11 +2,10 @@ package org.koitharu.kotatsu.browser.cloudflare import android.graphics.Bitmap import android.webkit.WebView -import okhttp3.HttpUrl.Companion.toHttpUrl import org.koitharu.kotatsu.browser.BrowserClient import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar +import org.koitharu.kotatsu.parsers.network.CloudFlareHelper -private const val CF_CLEARANCE = "cf_clearance" private const val LOOP_COUNTER = 3 class CloudFlareClient( @@ -50,8 +49,5 @@ class CloudFlareClient( } } - private fun getClearance(): String? { - return cookieJar.loadForRequest(targetUrl.toHttpUrl()) - .find { it.name == CF_CLEARANCE }?.value - } + private fun getClearance() = CloudFlareHelper.getClearanceCookie(cookieJar, targetUrl) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CloudFlareInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CloudFlareInterceptor.kt index 227a64035..5fdd13a8b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CloudFlareInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/CloudFlareInterceptor.kt @@ -2,41 +2,43 @@ package org.koitharu.kotatsu.core.network import okhttp3.Interceptor import okhttp3.Response -import okhttp3.internal.closeQuietly -import org.jsoup.Jsoup +import okio.IOException import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.parsers.model.MangaSource -import java.net.HttpURLConnection.HTTP_FORBIDDEN -import java.net.HttpURLConnection.HTTP_UNAVAILABLE +import org.koitharu.kotatsu.parsers.network.CloudFlareHelper class CloudFlareInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { - val response = chain.proceed(chain.request()) - if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) { - val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use { - Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString()) - } ?: return response - val hasCaptcha = content.getElementById("challenge-error-title") != null - val isBlocked = content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null - if (hasCaptcha || isBlocked) { - val request = response.request - response.closeQuietly() - if (isBlocked) { - throw CloudFlareBlockedException( - url = request.url.toString(), - source = request.tag(MangaSource::class.java), - ) - } else { - throw CloudFlareProtectedException( - url = request.url.toString(), - source = request.tag(MangaSource::class.java), - headers = request.headers, - ) - } - } + val request = chain.request() + val response = chain.proceed(request) + return when (CloudFlareHelper.checkResponseForProtection(response)) { + CloudFlareHelper.PROTECTION_BLOCKED -> response.closeThrowing( + CloudFlareBlockedException( + url = request.url.toString(), + source = request.tag(MangaSource::class.java), + ), + ) + + CloudFlareHelper.PROTECTION_CAPTCHA -> response.closeThrowing( + CloudFlareProtectedException( + url = request.url.toString(), + source = request.tag(MangaSource::class.java), + headers = request.headers, + ), + ) + + else -> response + } + } + + private fun Response.closeThrowing(error: IOException): Nothing { + try { + close() + } catch (e: Exception) { + error.addSuppressed(e) } - return response + throw error } } From 1e22e8de45df94b8e84c2f846fee6bc3928cafb5 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 7 Oct 2024 20:02:34 +0300 Subject: [PATCH 023/109] Improve filter --- .../kotatsu/filter/ui/FilterCoordinator.kt | 9 +++ .../kotatsu/filter/ui/FilterFieldLayout.kt | 5 ++ .../kotatsu/filter/ui/FilterHeaderFragment.kt | 13 ++++ .../kotatsu/filter/ui/FilterHeaderProducer.kt | 75 +++++++++++++++---- .../filter/ui/sheet/FilterSheetFragment.kt | 9 +++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 98 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 110bf6373..ec95fea68 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -332,6 +332,15 @@ class FilterCoordinator @Inject constructor( } } + fun toggleDemographic(value: Demographic, isSelected: Boolean) { + currentListFilter.update { oldValue -> + oldValue.copy( + demographics = if (isSelected) oldValue.demographics + value else oldValue.demographics - value, + query = oldValue.takeQueryIfSupported(), + ) + } + } + fun toggleContentType(value: ContentType, isSelected: Boolean) { currentListFilter.update { oldValue -> oldValue.copy( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterFieldLayout.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterFieldLayout.kt index 90ca05cd8..b5b1f83c0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterFieldLayout.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterFieldLayout.kt @@ -8,6 +8,7 @@ import android.view.View import android.widget.RelativeLayout import android.widget.TextView import androidx.annotation.AttrRes +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes import androidx.core.view.isInvisible @@ -68,6 +69,10 @@ class FilterFieldLayout @JvmOverloads constructor( } } + fun setTitle(@StringRes titleResId: Int) { + binding.textViewTitle.setText(titleResId) + } + fun setError(errorMessage: String?) { if (errorMessage == null && errorView == null) { return diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt index 0dc0271e7..ff7d30917 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -16,7 +16,13 @@ import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet +import org.koitharu.kotatsu.parsers.model.ContentRating +import org.koitharu.kotatsu.parsers.model.ContentType +import org.koitharu.kotatsu.parsers.model.Demographic +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN +import java.util.Locale import javax.inject.Inject @AndroidEntryPoint @@ -55,6 +61,13 @@ class FilterHeaderFragment : BaseFragment(), ChipsV override fun onChipCloseClick(chip: Chip, data: Any?) { when (data) { is String -> filter.setQuery(null) + is ContentRating -> filter.toggleContentRating(data, false) + is Demographic -> filter.toggleDemographic(data, false) + is ContentType -> filter.toggleContentType(data, false) + is MangaState -> filter.toggleState(data, false) + is Locale -> filter.setLocale(null) + is Int -> filter.setYear(YEAR_UNKNOWN) + is IntRange -> filter.setYearRange(YEAR_UNKNOWN, YEAR_UNKNOWN) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt index c29c0eeec..e011beaa7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderProducer.kt @@ -3,12 +3,15 @@ package org.koitharu.kotatsu.filter.ui import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.filter.ui.model.FilterProperty +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.search.domain.MangaSearchRepository import javax.inject.Inject import com.google.android.material.R as materialR @@ -18,15 +21,14 @@ class FilterHeaderProducer @Inject constructor( ) { fun observeHeader(filterCoordinator: FilterCoordinator): Flow { - return combine(filterCoordinator.tags, filterCoordinator.query) { tags, query -> - createChipsList( + return combine(filterCoordinator.tags, filterCoordinator.observe()) { tags, snapshot -> + val chipList = createChipsList( source = filterCoordinator.mangaSource, capabilities = filterCoordinator.capabilities, - property = tags, - query = query, + tagsProperty = tags, + snapshot = snapshot.listFilter, limit = 8, ) - }.combine(filterCoordinator.observe()) { chipList, snapshot -> FilterHeaderModel( chips = chipList, sortOrder = snapshot.sortOrder, @@ -38,20 +40,20 @@ class FilterHeaderProducer @Inject constructor( private suspend fun createChipsList( source: MangaSource, capabilities: MangaListFilterCapabilities, - property: FilterProperty, - query: String?, + tagsProperty: FilterProperty, + snapshot: MangaListFilter, limit: Int, ): List { val result = ArrayDeque(limit + 3) - if (query.isNullOrEmpty() || capabilities.isSearchWithFiltersSupported) { - val selectedTags = property.selectedItems.toMutableSet() + if (snapshot.query.isNullOrEmpty() || capabilities.isSearchWithFiltersSupported) { + val selectedTags = tagsProperty.selectedItems.toMutableSet() var tags = if (selectedTags.isEmpty()) { searchRepository.getTagsSuggestion("", limit, source) } else { searchRepository.getTagsSuggestion(selectedTags).take(limit) } if (tags.size < limit) { - tags = tags + property.availableItems.take(limit - tags.size) + tags = tags + tagsProperty.availableItems.take(limit - tags.size) } if (tags.isEmpty() && selectedTags.isEmpty()) { return emptyList() @@ -77,13 +79,59 @@ class FilterHeaderProducer @Inject constructor( result.addFirst(model) } } - if (!query.isNullOrEmpty()) { + snapshot.locale?.let { result.addFirst( ChipsView.ChipModel( - title = query, + title = it.getDisplayName(it).toTitleCase(it), + icon = R.drawable.ic_language, + isCloseable = true, + data = it, + ), + ) + } + snapshot.types.forEach { + result.addFirst( + ChipsView.ChipModel( + titleResId = it.titleResId, + isCloseable = true, + data = it, + ), + ) + } + snapshot.demographics.forEach { + result.addFirst( + ChipsView.ChipModel( + titleResId = it.titleResId, + isCloseable = true, + data = it, + ), + ) + } + snapshot.contentRating.forEach { + result.addFirst( + ChipsView.ChipModel( + titleResId = it.titleResId, + isCloseable = true, + data = it, + ), + ) + } + snapshot.states.forEach { + result.addFirst( + ChipsView.ChipModel( + titleResId = it.titleResId, + isCloseable = true, + data = it, + ), + ) + } + if (!snapshot.query.isNullOrEmpty()) { + result.addFirst( + ChipsView.ChipModel( + title = snapshot.query, icon = materialR.drawable.abc_ic_search_api_material, isCloseable = true, - data = query, + data = snapshot.query, ), ) } @@ -97,6 +145,5 @@ class FilterHeaderProducer @Inject constructor( private fun moreTagsChip() = ChipsView.ChipModel( titleResId = R.string.more, isDropdown = true, - // icon = materialR.drawable.abc_ic_menu_overflow_material, ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt index dc9115332..53952a129 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt @@ -68,12 +68,20 @@ class FilterSheetFragment : BaseAdaptiveSheet(), filter.year.observe(viewLifecycleOwner, this::onYearChanged) filter.yearRange.observe(viewLifecycleOwner, this::onYearRangeChanged) + binding.layoutGenres.setTitle( + if (filter.capabilities.isMultipleTagsSupported) { + R.string.genres + } else { + R.string.genre + }, + ) binding.spinnerLocale.onItemSelectedListener = this binding.spinnerOriginalLocale.onItemSelectedListener = this binding.spinnerOrder.onItemSelectedListener = this binding.chipsState.onChipClickListener = this binding.chipsTypes.onChipClickListener = this binding.chipsContentRating.onChipClickListener = this + binding.chipsDemographics.onChipClickListener = this binding.chipsGenres.onChipClickListener = this binding.chipsGenresExclude.onChipClickListener = this binding.sliderYear.addOnChangeListener(this::onSliderValueChange) @@ -143,6 +151,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), is ContentType -> filter.toggleContentType(data, !chip.isChecked) is ContentRating -> filter.toggleContentRating(data, !chip.isChecked) + is Demographic -> filter.toggleDemographic(data, !chip.isChecked) null -> TagsCatalogSheet.show(getChildFragmentManager(), chip.parentView?.id == R.id.chips_genresExclude) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1b0ecfaa..00cd1261b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -741,4 +741,5 @@ Start download Save selected manga? This may consume traffic and disk space Save manga + Genre From 557b69d73f5fe932954c9ebf61c5e278de38bb9c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 10 Oct 2024 08:23:22 +0300 Subject: [PATCH 024/109] New download dialog --- app/build.gradle | 4 +- .../core/ui/widgets/TwoLinesItemView.kt | 27 +- .../koitharu/kotatsu/core/util/ext/Bundle.kt | 22 ++ .../koitharu/kotatsu/core/zip/ZipOutput.kt | 1 + .../kotatsu/details/ui/DetailsActivity.kt | 7 +- .../kotatsu/details/ui/DetailsMenuProvider.kt | 20 +- .../details/ui/DownloadDialogHelper.kt | 67 ---- .../ui/dialog/ChapterSelectOptions.kt | 8 + .../download/ui/dialog/ChaptersSelectMacro.kt | 97 +++++ .../download/ui/dialog/DestinationsAdapter.kt | 41 ++ .../ui/dialog/DownloadDialogFragment.kt | 359 ++++++++++++++++++ .../ui/dialog/DownloadDialogViewModel.kt | 241 ++++++++++++ .../download/ui/dialog/DownloadOption.kt | 99 ----- .../download/ui/dialog/DownloadOptionAD.kt | 27 -- .../download/ui/list/DownloadsViewModel.kt | 2 +- .../download/ui/worker/DownloadTask.kt | 73 ++++ .../download/ui/worker/DownloadWorker.kt | 90 +++-- .../download/ui/worker/PausingHandle.kt | 4 +- .../kotatsu/list/ui/MangaListFragment.kt | 12 +- .../kotatsu/list/ui/MangaListViewModel.kt | 9 - .../local/data/LocalMangaRepository.kt | 4 +- .../layout-w600dp-land/activity_details.xml | 6 +- .../layout-w600dp-land/activity_settings.xml | 2 +- .../res/layout/activity_appwidget_shelf.xml | 2 +- app/src/main/res/layout/activity_details.xml | 6 +- app/src/main/res/layout/dialog_download.xml | 224 +++++++++++ .../main/res/layout/item_download_option.xml | 14 - app/src/main/res/layout/item_header.xml | 2 +- app/src/main/res/layout/item_list_group.xml | 2 +- app/src/main/res/layout/view_filter_field.xml | 2 +- .../main/res/layout/view_two_lines_item.xml | 16 +- app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/strings.xml | 8 +- app/src/main/res/values/styles.xml | 4 - 34 files changed, 1194 insertions(+), 310 deletions(-) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/ChapterSelectOptions.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/ChaptersSelectMacro.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DestinationsAdapter.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOption.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOptionAD.kt create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadTask.kt create mode 100644 app/src/main/res/layout/dialog_download.xml delete mode 100644 app/src/main/res/layout/item_download_option.xml diff --git a/app/build.gradle b/app/build.gradle index 02ee0fe0e..141dca202 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 35 - versionCode = 676 - versionName = '7.6.3' + versionCode = 680 + versionName = '7.7-a1' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt index 8bf6e5a49..6bffccaa8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/TwoLinesItemView.kt @@ -11,11 +11,13 @@ import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RoundRectShape import android.util.AttributeSet import android.view.LayoutInflater +import android.widget.Checkable import android.widget.LinearLayout import androidx.annotation.AttrRes import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.widget.ImageViewCompat import androidx.core.widget.TextViewCompat @@ -23,6 +25,7 @@ import com.google.android.material.ripple.RippleUtils import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.getDrawableCompat import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ViewTwoLinesItemBinding @@ -32,7 +35,7 @@ class TwoLinesItemView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0, -) : LinearLayout(context, attrs, defStyleAttr) { +) : LinearLayout(context, attrs, defStyleAttr), Checkable { private val binding = ViewTwoLinesItemBinding.inflate(LayoutInflater.from(context), this) @@ -48,6 +51,12 @@ class TwoLinesItemView @JvmOverloads constructor( binding.subtitle.textAndVisible = value } + var isButtonEnabled: Boolean + get() = binding.button.isEnabled + set(value) { + binding.button.isEnabled = value + } + init { var textColors: ColorStateList? = null context.withStyledAttributes( @@ -68,7 +77,7 @@ class TwoLinesItemView @JvmOverloads constructor( binding.layoutText.updateLayoutParams { marginStart = drawablePadding } setIconResource(getResourceId(R.styleable.TwoLinesItemView_icon, 0)) binding.title.text = getText(R.styleable.TwoLinesItemView_title) - binding.subtitle.text = getText(R.styleable.TwoLinesItemView_subtitle) + binding.subtitle.textAndVisible = getText(R.styleable.TwoLinesItemView_subtitle) textColors = getColorStateList(R.styleable.TwoLinesItemView_android_textColor) val textAppearanceFallback = androidx.appcompat.R.style.TextAppearance_AppCompat TextViewCompat.setTextAppearance( @@ -79,6 +88,10 @@ class TwoLinesItemView @JvmOverloads constructor( binding.subtitle, getResourceId(R.styleable.TwoLinesItemView_subtitleTextAppearance, textAppearanceFallback), ) + binding.icon.isChecked = getBoolean(R.styleable.TwoLinesItemView_android_checked, false) + val button = getDrawableCompat(context, R.styleable.TwoLinesItemView_android_button) + binding.button.setImageDrawable(button) + binding.button.isVisible = button != null } if (textColors == null) { textColors = binding.title.textColors @@ -88,6 +101,16 @@ class TwoLinesItemView @JvmOverloads constructor( ImageViewCompat.setImageTintList(binding.icon, textColors) } + override fun isChecked() = binding.icon.isChecked + + override fun toggle() = binding.icon.toggle() + + override fun setChecked(checked: Boolean) { + binding.icon.isChecked = checked + } + + fun setOnButtonClickListener(listener: OnClickListener?) = binding.button.setOnClickListener(listener) + fun setIconResource(@DrawableRes resId: Int) { binding.icon.setImageResource(resId) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt index 34f3f440e..3913abf87 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Bundle.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.SavedStateHandle import java.io.Serializable import java.util.EnumSet + // https://issuetracker.google.com/issues/240585930 inline fun Bundle.getParcelableCompat(key: String): T? { @@ -84,3 +85,24 @@ fun SavedStateHandle.require(key: String): T { "Value $key not found in SavedStateHandle or has a wrong type" } } + +fun Parcelable.marshall(): ByteArray { + val parcel = Parcel.obtain() + return try { + this.writeToParcel(parcel, 0) + parcel.marshall() + } finally { + parcel.recycle() + } +} + +fun Parcelable.Creator.unmarshall(bytes: ByteArray): T { + val parcel = Parcel.obtain() + return try { + parcel.unmarshall(bytes, 0, bytes.size) + parcel.setDataPosition(0) + createFromParcel(parcel) + } finally { + parcel.recycle() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt index 82378614a..f124671c3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt @@ -21,6 +21,7 @@ class ZipOutput( private val isClosed = AtomicBoolean(false) private val output = ZipOutputStream(file.outputStream()).apply { setLevel(compressionLevel) + // FIXME: Deflater has been closed } @WorkerThread diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 951c479db..8c45b23ce 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -88,6 +88,7 @@ import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter +import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet import org.koitharu.kotatsu.image.ui.ImageActivity @@ -195,6 +196,7 @@ class DetailsActivity : .filterNot { ChaptersPagesSheet.isShown(supportFragmentManager) } .observeEvent(this, DownloadStartedObserver(viewBinding.scrollView)) + DownloadDialogFragment.registerCallback(this, viewBinding.scrollView) menuProvider = DetailsMenuProvider( activity = this, viewModel = viewModel, @@ -210,7 +212,10 @@ class DetailsActivity : when (v.id) { R.id.button_read -> openReader(isIncognitoMode = false) R.id.chip_branch -> showBranchPopupMenu(v) - R.id.button_download -> DownloadDialogHelper(v, viewModel).show(menuProvider) + R.id.button_download -> { + val manga = viewModel.manga.value ?: return + DownloadDialogFragment.show(supportFragmentManager, listOf(manga)) + } R.id.chip_author -> { val manga = viewModel.manga.value ?: return diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt index 0cc332f30..0f7765a87 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt @@ -19,9 +19,8 @@ import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.os.AppShortcutManager -import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.util.ShareHelper -import org.koitharu.kotatsu.download.ui.dialog.DownloadOption +import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.search.ui.multi.SearchActivity import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet @@ -31,7 +30,7 @@ class DetailsMenuProvider( private val viewModel: DetailsViewModel, private val snackbarHost: View, private val appShortcutManager: AppShortcutManager, -) : MenuProvider, OnListItemClickListener { +) : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.opt_details, menu) @@ -75,7 +74,7 @@ class DetailsMenuProvider( } R.id.action_save -> { - DownloadDialogHelper(snackbarHost, viewModel).show(this) + DownloadDialogFragment.show(activity.supportFragmentManager, listOfNotNull(viewModel.manga.value)) } R.id.action_browser -> { @@ -129,17 +128,4 @@ class DetailsMenuProvider( } return true } - - override fun onItemClick(item: DownloadOption, view: View) { - val chaptersIds: Set? = when (item) { - is DownloadOption.WholeManga -> null - is DownloadOption.SelectionHint -> { - viewModel.startChaptersSelection() - return - } - - else -> item.chaptersIds - } - viewModel.download(chaptersIds) - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt deleted file mode 100644 index 50f8fe3d9..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DownloadDialogHelper.kt +++ /dev/null @@ -1,67 +0,0 @@ -package org.koitharu.kotatsu.details.ui - -import android.content.DialogInterface -import android.view.View -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.model.ids -import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog -import org.koitharu.kotatsu.core.ui.dialog.setRecyclerViewList -import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.download.ui.dialog.DownloadOption -import org.koitharu.kotatsu.download.ui.dialog.downloadOptionAD -import org.koitharu.kotatsu.settings.SettingsActivity - -class DownloadDialogHelper( - private val host: View, - private val viewModel: DetailsViewModel, -) { - - fun show(callback: OnListItemClickListener) { - val branch = viewModel.selectedBranchValue - val allChapters = viewModel.manga.value?.chapters ?: return - val branchChapters = viewModel.manga.value?.getChapters(branch).orEmpty() - val history = viewModel.history.value - - val options = buildList { - add(DownloadOption.WholeManga(allChapters.ids())) - if (branch != null && branchChapters.isNotEmpty()) { - add(DownloadOption.AllChapters(branch, branchChapters.ids())) - } - - if (history != null) { - val unreadChapters = branchChapters.dropWhile { it.id != history.chapterId } - if (unreadChapters.isNotEmpty() && unreadChapters.size < branchChapters.size) { - add(DownloadOption.AllUnreadChapters(unreadChapters.ids(), branch)) - if (unreadChapters.size > 5) { - add(DownloadOption.NextUnreadChapters(unreadChapters.take(5).ids())) - if (unreadChapters.size > 10) { - add(DownloadOption.NextUnreadChapters(unreadChapters.take(10).ids())) - } - } - } - } else { - if (branchChapters.size > 5) { - add(DownloadOption.FirstChapters(branchChapters.take(5).ids())) - if (branchChapters.size > 10) { - add(DownloadOption.FirstChapters(branchChapters.take(10).ids())) - } - } - } - add(DownloadOption.SelectionHint()) - } - var dialog: DialogInterface? = null - val listener = OnListItemClickListener { item, _ -> - callback.onItemClick(item, host) - dialog?.dismiss() - } - dialog = buildAlertDialog(host.context) { - setCancelable(true) - setTitle(R.string.download) - setNegativeButton(android.R.string.cancel, null) - setNeutralButton(R.string.settings) { _, _ -> - host.context.startActivity(SettingsActivity.newDownloadsSettingsIntent(host.context)) - } - setRecyclerViewList(options, downloadOptionAD(listener)) - }.also { it.show() } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/ChapterSelectOptions.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/ChapterSelectOptions.kt new file mode 100644 index 000000000..6a4ff0c6f --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/ChapterSelectOptions.kt @@ -0,0 +1,8 @@ +package org.koitharu.kotatsu.download.ui.dialog + +data class ChapterSelectOptions( + val wholeManga: ChaptersSelectMacro.WholeManga, + val wholeBranch: ChaptersSelectMacro.WholeBranch?, + val firstChapters: ChaptersSelectMacro.FirstChapters?, + val unreadChapters: ChaptersSelectMacro.UnreadChapters?, +) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/ChaptersSelectMacro.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/ChaptersSelectMacro.kt new file mode 100644 index 000000000..5302df6d3 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/ChaptersSelectMacro.kt @@ -0,0 +1,97 @@ +package org.koitharu.kotatsu.download.ui.dialog + +import androidx.collection.ArraySet +import androidx.collection.LongLongMap +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.util.mapNotNullToSet + +interface ChaptersSelectMacro { + + fun getChaptersIds(mangaId: Long, chapters: List): Set? + + class WholeManga( + val chaptersCount: Int, + ) : ChaptersSelectMacro { + + override fun getChaptersIds(mangaId: Long, chapters: List): Set? = null + } + + class WholeBranch( + val branches: Map, + val selectedBranch: String?, + ) : ChaptersSelectMacro { + + val chaptersCount: Int = branches[selectedBranch] ?: 0 + + override fun getChaptersIds( + mangaId: Long, + chapters: List + ): Set = chapters.mapNotNullToSet { c -> + if (c.branch == selectedBranch) { + c.id + } else { + null + } + } + + fun copy(branch: String?) = WholeBranch(branches, branch) + } + + class FirstChapters( + val chaptersCount: Int, + val maxAvailableCount: Int, + val branch: String?, + ) : ChaptersSelectMacro { + + override fun getChaptersIds(mangaId: Long, chapters: List): Set { + val result = ArraySet(chaptersCount) + for (c in chapters) { + if (c.branch == branch) { + result.add(c.id) + if (result.size >= chaptersCount) { + break + } + } + } + return result + } + + fun copy(count: Int) = FirstChapters(count, maxAvailableCount, branch) + } + + class UnreadChapters( + val chaptersCount: Int, + val maxAvailableCount: Int, + private val currentChaptersIds: LongLongMap, + ) : ChaptersSelectMacro { + + override fun getChaptersIds(mangaId: Long, chapters: List): Set? { + if (chapters.isEmpty()) { + return null + } + val currentChapterId = currentChaptersIds.getOrDefault(mangaId, chapters.first().id) + var branch: String? = null + var isAdding = false + val result = ArraySet(chaptersCount) + for (c in chapters) { + if (!isAdding) { + if (c.id == currentChapterId) { + branch = c.branch + isAdding = true + } + } + if (isAdding) { + if (c.branch == branch) { + result.add(c.id) + if (result.size >= chaptersCount) { + break + } + } + } + } + return result + } + + fun copy(count: Int) = UnreadChapters(count, maxAvailableCount, currentChaptersIds) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DestinationsAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DestinationsAdapter.kt new file mode 100644 index 000000000..bb9a34b88 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DestinationsAdapter.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.download.ui.dialog + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import androidx.core.view.isVisible +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.util.ext.textAndVisible +import org.koitharu.kotatsu.databinding.ItemStorageConfigBinding +import org.koitharu.kotatsu.settings.storage.DirectoryModel + +class DestinationsAdapter(context: Context, dataset: List) : + ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, android.R.id.text1, dataset) { + + init { + setDropDownViewResource(R.layout.item_storage_config) + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = convertView ?: LayoutInflater.from(parent.context) + .inflate(android.R.layout.simple_spinner_dropdown_item, parent, false) + val item = getItem(position) ?: return view + view.findViewById(android.R.id.text1).text = item.title ?: view.context.getString(item.titleRes) + return view + } + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = convertView ?: LayoutInflater.from(parent.context) + .inflate(R.layout.item_storage_config, parent, false) + val item = getItem(position) ?: return view + val binding = + view.tag as? ItemStorageConfigBinding ?: ItemStorageConfigBinding.bind(view).also { view.tag = it } + binding.imageViewRemove.isVisible = false + binding.textViewTitle.text = item.title ?: view.context.getString(item.titleRes) + binding.textViewSubtitle.textAndVisible = item.file?.path + return view + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt new file mode 100644 index 000000000..007902926 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogFragment.kt @@ -0,0 +1,359 @@ +package org.koitharu.kotatsu.download.ui.dialog + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import android.widget.Spinner +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentResultListener +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.prefs.DownloadFormat +import org.koitharu.kotatsu.core.ui.AlertDialogFragment +import org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView +import org.koitharu.kotatsu.core.util.ext.findActivity +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit +import org.koitharu.kotatsu.core.util.ext.mapToArray +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.observeEvent +import org.koitharu.kotatsu.core.util.ext.parentView +import org.koitharu.kotatsu.core.util.ext.showDistinct +import org.koitharu.kotatsu.core.util.ext.showOrHide +import org.koitharu.kotatsu.core.util.ext.withArgs +import org.koitharu.kotatsu.databinding.DialogDownloadBinding +import org.koitharu.kotatsu.download.ui.list.DownloadsActivity +import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.format +import org.koitharu.kotatsu.settings.storage.DirectoryModel + +@AndroidEntryPoint +class DownloadDialogFragment : AlertDialogFragment(), View.OnClickListener { + + private val viewModel by viewModels() + private var optionViews: Array? = null + + override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?) = + DialogDownloadBinding.inflate(inflater, container, false) + + override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { + return super.onBuildDialog(builder) + .setTitle(R.string.save_manga) + .setCancelable(true) + } + + override fun onViewBindingCreated(binding: DialogDownloadBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + optionViews = arrayOf( + binding.optionWholeManga, + binding.optionWholeBranch, + binding.optionFirstChapters, + binding.optionUnreadChapters, + ).onEach { + it.setOnClickListener(this) + it.setOnButtonClickListener(this) + } + binding.buttonCancel.setOnClickListener(this) + binding.buttonConfirm.setOnClickListener(this) + binding.textViewMore.setOnClickListener(this) + + binding.textViewSummary.text = viewModel.manga.joinToStringWithLimit(binding.root.context, 120) { it.title } + + viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) + viewModel.onScheduled.observeEvent(viewLifecycleOwner, this::onDownloadScheduled) + viewModel.onError.observeEvent(viewLifecycleOwner, this::onError) + viewModel.defaultFormat.observe(viewLifecycleOwner, this::onDefaultFormatChanged) + viewModel.availableDestinations.observe(viewLifecycleOwner, this::onDestinationsChanged) + viewModel.chaptersSelectOptions.observe(viewLifecycleOwner, this::onChapterSelectOptionsChanged) + viewModel.isOptionsLoading.observe(viewLifecycleOwner, binding.progressBar::showOrHide) + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + showMoreOptions(requireViewBinding().textViewMore.isChecked) + setCheckedOption( + savedInstanceState?.getInt(KEY_CHECKED_OPTION, R.id.option_whole_manga) ?: R.id.option_whole_manga, + ) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + optionViews?.find { it.isChecked }?.let { + outState.putInt(KEY_CHECKED_OPTION, it.id) + } + } + + override fun onDestroyView() { + super.onDestroyView() + optionViews = null + } + + override fun onClick(v: View) { + when (v.id) { + R.id.button_cancel -> dialog?.cancel() + R.id.button_confirm -> viewBinding?.run { + val options = viewModel.chaptersSelectOptions.value + viewModel.confirm( + startNow = switchStart.isChecked, + chaptersMacro = when { + optionWholeManga.isChecked -> options.wholeManga + optionWholeBranch.isChecked -> options.wholeBranch ?: return@run + optionFirstChapters.isChecked -> options.firstChapters ?: return@run + optionUnreadChapters.isChecked -> options.unreadChapters ?: return@run + else -> return@run + }, + format = DownloadFormat.entries.getOrNull(spinnerFormat.selectedItemPosition), + destination = viewModel.availableDestinations.value.getOrNull(spinnerDestination.selectedItemPosition), + ) + } + + R.id.textView_more -> { + val binding = viewBinding ?: return + binding.textViewMore.toggle() + showMoreOptions(binding.textViewMore.isChecked) + } + + R.id.button -> when (v.parentView?.id ?: return) { + R.id.option_whole_branch -> showBranchSelection(v) + R.id.option_first_chapters -> showFirstChaptersCountSelection(v) + R.id.option_unread_chapters -> showUnreadChaptersCountSelection(v) + } + + else -> if (v is TwoLinesItemView) { + setCheckedOption(v.id) + } + } + } + + private fun onError(e: Throwable) { + MaterialAlertDialogBuilder(context ?: return) + .setNegativeButton(R.string.close, null) + .setTitle(R.string.error) + .setMessage(e.getDisplayMessage(resources)) + .show() + dismiss() + } + + private fun onLoadingStateChanged(value: Boolean) { + with(requireViewBinding()) { + buttonConfirm.isEnabled = !value + } + } + + private fun onDefaultFormatChanged(format: DownloadFormat?) { + val spinner = viewBinding?.spinnerFormat ?: return + spinner.setSelection(format?.ordinal ?: Spinner.INVALID_POSITION) + } + + private fun onDestinationsChanged(directories: List) { + viewBinding?.spinnerDestination?.run { + adapter = DestinationsAdapter(context, directories) + setSelection(directories.indexOfFirst { it.isChecked }) + } + } + + private fun onChapterSelectOptionsChanged(options: ChapterSelectOptions) { + with(viewBinding ?: return) { + // Whole manga + optionWholeManga.subtitle = if (options.wholeManga.chaptersCount > 0) { + resources.getQuantityString( + R.plurals.chapters, + options.wholeManga.chaptersCount, + options.wholeManga.chaptersCount, + ) + } else { + null + } + // All chapters for branch + optionWholeBranch.isVisible = options.wholeBranch != null + options.wholeBranch?.let { + optionWholeBranch.title = resources.getString( + R.string.download_option_all_chapters, + it.selectedBranch, + ) + optionWholeBranch.subtitle = if (it.chaptersCount > 0) { + resources.getQuantityString( + R.plurals.chapters, + it.chaptersCount, + it.chaptersCount, + ) + } else { + null + } + } + // First N chapters + optionFirstChapters.isVisible = options.firstChapters != null + options.firstChapters?.let { + optionFirstChapters.title = resources.getString( + R.string.download_option_first_n_chapters, + resources.getQuantityString( + R.plurals.chapters, + it.chaptersCount, + it.chaptersCount, + ), + ) + optionFirstChapters.subtitle = it.branch + } + // Next N unread chapters + optionUnreadChapters.isVisible = options.unreadChapters != null + options.unreadChapters?.let { + optionUnreadChapters.title = if (it.chaptersCount == Int.MAX_VALUE) { + resources.getString(R.string.download_option_all_unread) + } else { + resources.getString( + R.string.download_option_next_unread_n_chapters, + resources.getQuantityString( + R.plurals.chapters, + it.chaptersCount, + it.chaptersCount, + ), + ) + } + } + } + } + + private fun onDownloadScheduled(isStarted: Boolean) { + val bundle = Bundle(1) + bundle.putBoolean(ARG_STARTED, isStarted) + setFragmentResult(RESULT_KEY, bundle) + dismiss() + } + + private fun showMoreOptions(isVisible: Boolean) = viewBinding?.apply { + cardFormat.isVisible = isVisible + textViewFormat.isVisible = isVisible + cardDestination.isVisible = isVisible + textViewDestination.isVisible = isVisible + } + + private fun setCheckedOption(id: Int) { + for (optionView in optionViews ?: return) { + optionView.isChecked = id == optionView.id + optionView.isButtonEnabled = optionView.isChecked + } + } + + private fun showBranchSelection(v: View) { + val option = viewModel.chaptersSelectOptions.value.wholeBranch ?: return + val branches = option.branches.keys.toList() + if (branches.size <= 1) { + return + } + val menu = PopupMenu(v.context, v) + for ((i, branch) in branches.withIndex()) { + menu.menu.add(Menu.NONE, Menu.NONE, i, branch ?: getString(R.string.unknown)) + } + menu.setOnMenuItemClickListener { + viewModel.setSelectedBranch(branches.getOrNull(it.order)) + true + } + menu.show() + } + + private fun showFirstChaptersCountSelection(v: View) { + val option = viewModel.chaptersSelectOptions.value.firstChapters ?: return + val menu = PopupMenu(v.context, v) + chaptersCount(option.maxAvailableCount).forEach { i -> + menu.menu.add(i.format()) + } + menu.setOnMenuItemClickListener { + viewModel.setFirstChaptersCount( + it.title?.toString()?.toIntOrNull() ?: return@setOnMenuItemClickListener false, + ) + true + } + menu.show() + } + + private fun showUnreadChaptersCountSelection(v: View) { + val option = viewModel.chaptersSelectOptions.value.unreadChapters ?: return + val menu = PopupMenu(v.context, v) + chaptersCount(option.maxAvailableCount).forEach { i -> + menu.menu.add(i.format()) + } + menu.menu.add(getString(R.string.chapters_all)) + menu.setOnMenuItemClickListener { + viewModel.setUnreadChaptersCount(it.title?.toString()?.toIntOrNull() ?: Int.MAX_VALUE) + true + } + menu.show() + } + + private fun chaptersCount(max: Int) = sequence { + yield(1) + var seed = 5 + var step = 5 + while (seed + step <= max) { + yield(seed) + step = when { + seed < 20 -> 5 + seed < 60 -> 10 + else -> 20 + } + seed += step + } + if (seed < max) { + yield(max) + } + } + + private class SnackbarResultListener(private val host: View) : FragmentResultListener { + + override fun onFragmentResult(requestKey: String, result: Bundle) { + val isStarted = result.getBoolean(ARG_STARTED, true) + val snackbar = Snackbar.make( + host, + if (isStarted) R.string.download_started else R.string.download_added, + Snackbar.LENGTH_LONG, + ) + (host.context.findActivity() as? BottomNavOwner)?.let { + snackbar.anchorView = it.bottomNav + } + snackbar.setAction(R.string.details) { + it.context.startActivity(Intent(it.context, DownloadsActivity::class.java)) + } + snackbar.show() + } + } + + companion object { + + private const val TAG = "DownloadDialogFragment" + private const val RESULT_KEY = "DOWNLOAD_STARTED" + private const val ARG_STARTED = "started" + private const val KEY_CHECKED_OPTION = "checked_opt" + const val ARG_MANGA = "manga" + + fun show(fm: FragmentManager, manga: Collection) = DownloadDialogFragment().withArgs(1) { + putParcelableArray(ARG_MANGA, manga.mapToArray { ParcelableManga(it) }) + }.showDistinct(fm, TAG) + + fun registerCallback(activity: FragmentActivity, snackbarHost: View) = + activity.supportFragmentManager.setFragmentResultListener( + RESULT_KEY, + activity, + SnackbarResultListener(snackbarHost), + ) + + fun registerCallback(fragment: Fragment, snackbarHost: View) = + fragment.childFragmentManager.setFragmentResultListener( + RESULT_KEY, + fragment.viewLifecycleOwner, + SnackbarResultListener(snackbarHost), + ) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt new file mode 100644 index 000000000..f8d8e3b77 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadDialogViewModel.kt @@ -0,0 +1,241 @@ +package org.koitharu.kotatsu.download.ui.dialog + +import androidx.collection.ArrayMap +import androidx.collection.ArraySet +import androidx.collection.MutableLongLongMap +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.getPreferredBranch +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.DownloadFormat +import org.koitharu.kotatsu.core.ui.BaseViewModel +import org.koitharu.kotatsu.core.util.ext.MutableEventFlow +import org.koitharu.kotatsu.core.util.ext.call +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.require +import org.koitharu.kotatsu.core.util.ext.sizeOrZero +import org.koitharu.kotatsu.download.ui.worker.DownloadTask +import org.koitharu.kotatsu.download.ui.worker.DownloadWorker +import org.koitharu.kotatsu.history.data.HistoryRepository +import org.koitharu.kotatsu.local.data.LocalMangaRepository +import org.koitharu.kotatsu.local.data.LocalStorageManager +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.SuspendLazy +import org.koitharu.kotatsu.parsers.util.mapToSet +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.settings.storage.DirectoryModel +import javax.inject.Inject + +@HiltViewModel +class DownloadDialogViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val mangaDataRepository: MangaDataRepository, + private val scheduler: DownloadWorker.Scheduler, + private val localStorageManager: LocalStorageManager, + private val localMangaRepository: LocalMangaRepository, + private val mangaRepositoryFactory: MangaRepository.Factory, + private val historyRepository: HistoryRepository, + private val settings: AppSettings, +) : BaseViewModel() { + + val manga = savedStateHandle.require>(DownloadDialogFragment.ARG_MANGA).map { + it.manga + } + private val mangaDetails = SuspendLazy { + coroutineScope { + manga.map { m -> + async { m.getDetails() } + }.awaitAll() + } + } + + val onScheduled = MutableEventFlow() + val defaultFormat = MutableStateFlow(null) + val availableDestinations = MutableStateFlow(listOf(defaultDestination())) + val chaptersSelectOptions = MutableStateFlow( + ChapterSelectOptions( + wholeManga = ChaptersSelectMacro.WholeManga(0), + wholeBranch = null, + firstChapters = null, + unreadChapters = null, + ), + ) + val isOptionsLoading = MutableStateFlow(true) + + init { + launchJob(Dispatchers.Default) { + defaultFormat.value = settings.preferredDownloadFormat + } + launchJob(Dispatchers.Default) { + try { + loadAvailableOptions() + } finally { + isOptionsLoading.value = false + } + } + loadAvailableDestinations() + } + + fun confirm( + startNow: Boolean, + chaptersMacro: ChaptersSelectMacro, + format: DownloadFormat?, + destination: DirectoryModel?, + ) { + launchLoadingJob(Dispatchers.Default) { + val tasks = mangaDetails.get().map { m -> + val chapters = checkNotNull(m.chapters) { "Manga \"${m.title}\" cannot be loaded" } + mangaDataRepository.storeManga(m) + DownloadTask( + mangaId = m.id, + isPaused = !startNow, + isSilent = false, + chaptersIds = chaptersMacro.getChaptersIds(m.id, chapters)?.toLongArray(), + destination = destination?.file, + format = format, + ) + } + scheduler.schedule(tasks) + onScheduled.call(startNow) + } + } + + fun setSelectedBranch(branch: String?) { + val snapshot = chaptersSelectOptions.value + chaptersSelectOptions.value = snapshot.copy( + wholeBranch = snapshot.wholeBranch?.copy(branch), + ) + } + + fun setFirstChaptersCount(count: Int) { + val snapshot = chaptersSelectOptions.value + chaptersSelectOptions.value = snapshot.copy( + firstChapters = snapshot.firstChapters?.copy(count), + ) + } + + fun setUnreadChaptersCount(count: Int) { + val snapshot = chaptersSelectOptions.value + chaptersSelectOptions.value = snapshot.copy( + unreadChapters = snapshot.unreadChapters?.copy(count), + ) + } + + private fun defaultDestination() = DirectoryModel( + title = null, + titleRes = R.string.system_default, + file = null, + isRemovable = false, + isChecked = true, + isAvailable = true, + ) + + private suspend fun loadAvailableOptions() { + val details = mangaDetails.get() + var totalChapters = 0 + val branches = ArrayMap() + var maxChapters = 0 + var maxUnreadChapters = 0 + val preferredBranches = ArraySet(details.size) + val currentChaptersIds = MutableLongLongMap(details.size) + + details.forEach { m -> + val history = historyRepository.getOne(m) + if (history != null) { + currentChaptersIds[m.id] = history.chapterId + val unreadChaptersCount = m.chapters?.dropWhile { it.id != history.chapterId }.sizeOrZero() + maxUnreadChapters = maxOf(maxUnreadChapters, unreadChaptersCount) + } else { + maxUnreadChapters = maxOf(maxUnreadChapters, m.chapters.sizeOrZero()) + } + maxChapters = maxOf(maxChapters, m.chapters.sizeOrZero()) + preferredBranches.add(m.getPreferredBranch(history)) + m.chapters?.forEach { c -> + totalChapters++ + branches.increment(c.branch) + } + } + val defaultBranch = preferredBranches.firstOrNull() + chaptersSelectOptions.value = ChapterSelectOptions( + wholeManga = ChaptersSelectMacro.WholeManga(totalChapters), + wholeBranch = if (branches.size > 1) { + ChaptersSelectMacro.WholeBranch( + branches = branches, + selectedBranch = defaultBranch, + ) + } else { + null + }, + firstChapters = if (maxChapters > 0) { + ChaptersSelectMacro.FirstChapters( + chaptersCount = minOf(5, maxChapters), + maxAvailableCount = maxChapters, + branch = defaultBranch, + ) + } else { + null + }, + unreadChapters = if (currentChaptersIds.isNotEmpty()) { + ChaptersSelectMacro.UnreadChapters( + chaptersCount = minOf(5, maxUnreadChapters), + maxAvailableCount = maxUnreadChapters, + currentChaptersIds = currentChaptersIds, + ) + } else { + null + }, + ) + } + + private fun loadAvailableDestinations() = launchJob(Dispatchers.Default) { + val defaultDir = manga.mapToSet { + localMangaRepository.getOutputDir(it, null) + }.singleOrNull() + val dirs = localStorageManager.getWriteableDirs() + availableDestinations.value = buildList(dirs.size + 1) { + if (defaultDir == null) { + add(defaultDestination()) + } else if (defaultDir !in dirs) { + add( + DirectoryModel( + title = localStorageManager.getDirectoryDisplayName(defaultDir, isFullPath = false), + titleRes = 0, + file = defaultDir, + isChecked = true, + isAvailable = true, + isRemovable = false, + ), + ) + } + dirs.mapTo(this) { dir -> + DirectoryModel( + title = localStorageManager.getDirectoryDisplayName(dir, isFullPath = false), + titleRes = 0, + file = dir, + isChecked = dir == defaultDir, + isAvailable = true, + isRemovable = false, + ) + } + } + } + + private suspend fun Manga.getDetails(): Manga = runCatchingCancellable { + mangaRepositoryFactory.create(source).getDetails(this) + }.onFailure { e -> + e.printStackTraceDebug() + }.getOrDefault(this) + + private fun MutableMap.increment(key: T) { + put(key, getOrDefault(key, 0) + 1) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOption.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOption.kt deleted file mode 100644 index ae9bf076a..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOption.kt +++ /dev/null @@ -1,99 +0,0 @@ -package org.koitharu.kotatsu.download.ui.dialog - -import android.content.res.Resources -import androidx.annotation.DrawableRes -import org.koitharu.kotatsu.R -import java.util.Locale -import com.google.android.material.R as materialR - -sealed interface DownloadOption { - - val chaptersIds: Set - - @get:DrawableRes - val iconResId: Int - - val chaptersCount: Int - get() = chaptersIds.size - - fun getLabel(resources: Resources): CharSequence - - class AllChapters( - val branch: String, - override val chaptersIds: Set, - ) : DownloadOption { - - override val iconResId = R.drawable.ic_select_group - - override fun getLabel(resources: Resources): CharSequence { - return resources.getString(R.string.download_option_all_chapters, branch) - } - } - - class WholeManga( - override val chaptersIds: Set, - ) : DownloadOption { - - override val iconResId = materialR.drawable.abc_ic_menu_selectall_mtrl_alpha - - override fun getLabel(resources: Resources): CharSequence { - return resources.getString(R.string.download_option_whole_manga) - } - } - - class FirstChapters( - override val chaptersIds: Set, - ) : DownloadOption { - - override val iconResId = R.drawable.ic_list_start - - override fun getLabel(resources: Resources): CharSequence { - return resources.getString( - R.string.download_option_first_n_chapters, - resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount) - .lowercase(Locale.getDefault()), - ) - } - } - - class AllUnreadChapters( - override val chaptersIds: Set, - val branch: String?, - ) : DownloadOption { - - override val iconResId = R.drawable.ic_list_end - - override fun getLabel(resources: Resources): CharSequence { - return if (branch == null) { - resources.getString(R.string.download_option_all_unread) - } else { - resources.getString(R.string.download_option_all_unread_b, branch) - } - } - } - - class NextUnreadChapters( - override val chaptersIds: Set, - ) : DownloadOption { - - override val iconResId = R.drawable.ic_list_next - - override fun getLabel(resources: Resources): CharSequence { - return resources.getString( - R.string.download_option_next_unread_n_chapters, - resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount) - .lowercase(Locale.getDefault()), - ) - } - } - - class SelectionHint : DownloadOption { - - override val chaptersIds: Set = emptySet() - override val iconResId = R.drawable.ic_tap - - override fun getLabel(resources: Resources): CharSequence { - return resources.getString(R.string.download_option_manual_selection) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOptionAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOptionAD.kt deleted file mode 100644 index 3a277787f..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/dialog/DownloadOptionAD.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.koitharu.kotatsu.download.ui.dialog - -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -import org.koitharu.kotatsu.databinding.ItemDownloadOptionBinding - -fun downloadOptionAD( - onClickListener: OnListItemClickListener, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemDownloadOptionBinding.inflate(layoutInflater, parent, false) }, -) { - - binding.root.setOnClickListener { v -> onClickListener.onItemClick(item, v) } - - bind { - with(binding.root) { - title = item.getLabel(resources) - subtitle = if (item.chaptersCount == 0) null else resources.getQuantityString( - R.plurals.chapters, - item.chaptersCount, - item.chaptersCount, - ) - setIconResource(item.iconResId) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt index a58e152f3..bd6229d96 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt @@ -299,7 +299,7 @@ class DownloadsViewModel @Inject constructor( } private fun observeChapters(manga: Manga, workId: UUID): StateFlow?> = flow { - val chapterIds = workScheduler.getInputChaptersIds(workId)?.toSet() + val chapterIds = workScheduler.getTask(workId)?.chaptersIds val chapters = (tryLoad(manga) ?: manga).chapters ?: return@flow suspend fun mapChapters(): List { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadTask.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadTask.kt new file mode 100644 index 000000000..8f6edcc76 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadTask.kt @@ -0,0 +1,73 @@ +package org.koitharu.kotatsu.download.ui.worker + +import android.os.Parcelable +import androidx.work.Data +import kotlinx.parcelize.Parcelize +import org.koitharu.kotatsu.core.prefs.DownloadFormat +import org.koitharu.kotatsu.parsers.util.find +import java.io.File + +@Parcelize +class DownloadTask( + val mangaId: Long, + val isPaused: Boolean, + val isSilent: Boolean, + val chaptersIds: LongArray?, + val destination: File?, + val format: DownloadFormat?, +) : Parcelable { + + constructor(data: Data) : this( + mangaId = data.getLong(MANGA_ID, 0L), + isPaused = data.getBoolean(START_PAUSED, false), + isSilent = data.getBoolean(IS_SILENT, false), + chaptersIds = data.getLongArray(CHAPTERS)?.takeUnless(LongArray::isEmpty), + destination = data.getString(DESTINATION)?.let { File(it) }, + format = data.getString(FORMAT)?.let { DownloadFormat.entries.find(it) }, + ) + + fun toData(): Data = Data.Builder() + .putLong(MANGA_ID, mangaId) + .putBoolean(START_PAUSED, isPaused) + .putBoolean(IS_SILENT, isSilent) + .putLongArray(CHAPTERS, chaptersIds ?: LongArray(0)) + .putString(DESTINATION, destination?.path) + .putString(FORMAT, format?.name) + .build() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DownloadTask + + if (mangaId != other.mangaId) return false + if (isPaused != other.isPaused) return false + if (isSilent != other.isSilent) return false + if (!(chaptersIds contentEquals other.chaptersIds)) return false + if (destination != other.destination) return false + if (format != other.format) return false + + return true + } + + override fun hashCode(): Int { + var result = mangaId.hashCode() + result = 31 * result + isPaused.hashCode() + result = 31 * result + isSilent.hashCode() + result = 31 * result + (chaptersIds?.contentHashCode() ?: 0) + result = 31 * result + (destination?.hashCode() ?: 0) + result = 31 * result + (format?.hashCode() ?: 0) + return result + } + + private companion object { + + const val MANGA_ID = "manga_id" + const val IS_SILENT = "silent" + const val START_PAUSED = "paused" + const val CHAPTERS = "chapters" + const val DESTINATION = "dest" + const val FORMAT = "format" + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index 9fe15de56..73c7bd556 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -105,10 +105,8 @@ class DownloadWorker @AssistedInject constructor( notificationFactoryFactory: DownloadNotificationFactory.Factory, ) : CoroutineWorker(appContext, params) { - private val notificationFactory = notificationFactoryFactory.create( - uuid = params.id, - isSilent = params.inputData.getBoolean(IS_SILENT, false), - ) + private val task = DownloadTask(params.inputData) + private val notificationFactory = notificationFactoryFactory.create(uuid = params.id, isSilent = task.isSilent) private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val slowdownDispatcher = DownloadSlowdownDispatcher(mangaRepositoryFactory, SLOWDOWN_DELAY) @@ -122,18 +120,16 @@ class DownloadWorker @AssistedInject constructor( override suspend fun doWork(): Result { setForeground(getForegroundInfo()) - val mangaId = inputData.getLong(MANGA_ID, 0L) - val manga = mangaDataRepository.findMangaById(mangaId) ?: return Result.failure() + val manga = mangaDataRepository.findMangaById(task.mangaId) ?: return Result.failure() publishState(DownloadState(manga = manga, isIndeterminate = true).also { lastPublishedState = it }) - val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() } val downloadedIds = getDoneChapters(manga) return try { val pausingHandle = PausingHandle() - if (inputData.getBoolean(START_PAUSED, false)) { + if (task.isPaused) { pausingHandle.pause() } withContext(pausingHandle) { - downloadMangaImpl(manga, chaptersIds, downloadedIds) + downloadMangaImpl(manga, task, downloadedIds) } Result.success(currentState.toWorkData()) } catch (e: CancellationException) { @@ -174,7 +170,7 @@ class DownloadWorker @AssistedInject constructor( private suspend fun downloadMangaImpl( subject: Manga, - includedIds: LongArray?, + task: DownloadTask, excludedIds: Set, ) { var manga = subject @@ -187,7 +183,7 @@ class DownloadWorker @AssistedInject constructor( PausingReceiver.createIntentFilter(id), ContextCompat.RECEIVER_NOT_EXPORTED, ) - val destination = localMangaRepository.getOutputDir(manga) + val destination = localMangaRepository.getOutputDir(manga, task.destination) checkNotNull(destination) { applicationContext.getString(R.string.cannot_find_available_storage) } var output: LocalMangaOutput? = null try { @@ -197,7 +193,11 @@ class DownloadWorker @AssistedInject constructor( } val repo = mangaRepositoryFactory.create(manga.source) val mangaDetails = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga - output = LocalMangaOutput.getOrCreate(destination, mangaDetails, settings.preferredDownloadFormat) + output = LocalMangaOutput.getOrCreate( + root = destination, + manga = mangaDetails, + format = task.format ?: settings.preferredDownloadFormat, + ) val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl } if (coverUrl.isNotEmpty()) { downloadFile(coverUrl, destination, repo.source).let { file -> @@ -205,7 +205,7 @@ class DownloadWorker @AssistedInject constructor( file.deleteAwait() } } - val chapters = getChapters(mangaDetails, includedIds) + val chapters = getChapters(mangaDetails, task) for ((chapterIndex, chapter) in chapters.withIndex()) { checkIsPaused() if (chaptersToSkip.remove(chapter.value.id)) { @@ -311,6 +311,10 @@ class DownloadWorker @AssistedInject constructor( DOWNLOAD_ERROR_DELAY } if (countDown <= 0 || retryDelay < 0 || retryDelay > MAX_RETRY_DELAY) { + val pausingHandle = PausingHandle.current() + if (pausingHandle.skipAllErrors()) { + return null + } publishState( currentState.copy( isPaused = true, @@ -321,7 +325,6 @@ class DownloadWorker @AssistedInject constructor( ), ) countDown = MAX_FAILSAFE_ATTEMPTS - val pausingHandle = PausingHandle.current() pausingHandle.pause() try { pausingHandle.awaitResumed() @@ -404,10 +407,10 @@ class DownloadWorker @AssistedInject constructor( private fun getChapters( manga: Manga, - includedIds: LongArray?, + task: DownloadTask, ): List> { val chapters = checkNotNull(manga.chapters) { "Chapters list must not be null" } - val chaptersIdsSet = includedIds?.toMutableSet() + val chaptersIdsSet = task.chaptersIds?.toMutableSet() val result = ArrayList>((chaptersIdsSet ?: chapters).size) val counters = HashMap() for (chapter in chapters) { @@ -420,7 +423,7 @@ class DownloadWorker @AssistedInject constructor( } if (chaptersIdsSet != null) { check(chaptersIdsSet.isEmpty()) { - "${chaptersIdsSet.size} of ${includedIds.size} requested chapters not found in manga" + "${chaptersIdsSet.size} of ${task.chaptersIds.size} requested chapters not found in manga" } } check(result.isNotEmpty()) { "Chapters list must not be empty" } @@ -435,35 +438,42 @@ class DownloadWorker @AssistedInject constructor( private val settings: AppSettings, ) { + @Deprecated("") suspend fun schedule( manga: Manga, - chaptersIds: Collection?, + chaptersIds: Set?, isPaused: Boolean, isSilent: Boolean, ) { dataRepository.storeManga(manga) - val data = Data.Builder() - .putLong(MANGA_ID, manga.id) - .putBoolean(START_PAUSED, isPaused) - .putBoolean(IS_SILENT, isSilent) - if (!chaptersIds.isNullOrEmpty()) { - data.putLongArray(CHAPTERS_IDS, chaptersIds.toLongArray()) - } - scheduleImpl(listOf(data.build())) + val task = DownloadTask( + mangaId = manga.id, + isPaused = isPaused, + isSilent = isSilent, + chaptersIds = chaptersIds?.toLongArray(), + destination = null, + format = null, + ) + schedule(listOf(task)) } + @Deprecated("") suspend fun schedule( manga: Collection, isPaused: Boolean, ) { - val data = manga.map { + val tasks = manga.map { dataRepository.storeManga(it) - Data.Builder() - .putLong(MANGA_ID, it.id) - .putBoolean(START_PAUSED, isPaused) - .build() + DownloadTask( + mangaId = it.id, + isPaused = isPaused, + isSilent = false, + chaptersIds = null, + destination = null, + format = null, + ) } - scheduleImpl(data) + schedule(tasks) } fun observeWorks(): Flow> = workManager @@ -478,8 +488,8 @@ class DownloadWorker @AssistedInject constructor( .build() } - suspend fun getInputChaptersIds(workId: UUID): LongArray? { - return workManager.getWorkInputData(workId)?.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() } + suspend fun getTask(workId: UUID): DownloadTask? { + return workManager.getWorkInputData(workId)?.let { DownloadTask(it) } } suspend fun cancel(id: UUID) { @@ -537,18 +547,18 @@ class DownloadWorker @AssistedInject constructor( } } - private suspend fun scheduleImpl(data: Collection) { - if (data.isEmpty()) { + suspend fun schedule(tasks: Collection) { + if (tasks.isEmpty()) { return } val constraints = createConstraints() - val requests = data.map { inputData -> + val requests = tasks.map { task -> OneTimeWorkRequestBuilder() .setConstraints(constraints) .addTag(TAG) .keepResultsForAtLeast(30, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS) - .setInputData(inputData) + .setInputData(task.toData()) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() } @@ -567,10 +577,6 @@ class DownloadWorker @AssistedInject constructor( const val DOWNLOAD_ERROR_DELAY = 2_000L const val MAX_RETRY_DELAY = 7_200_000L // 2 hours const val SLOWDOWN_DELAY = 200L - const val MANGA_ID = "manga_id" - const val CHAPTERS_IDS = "chapters" - const val IS_SILENT = "silent" - const val START_PAUSED = "paused" const val TAG = "download" } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt index e02205230..3eb184121 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/PausingHandle.kt @@ -53,7 +53,9 @@ class PausingHandle : AbstractCoroutineContextElement(PausingHandle) { } } - fun skipCurrentError(): Boolean = skipError.compareAndSet(expect = true, update = skipAllErrors) + fun skipAllErrors(): Boolean = skipAllErrors + + fun skipCurrentError(): Boolean = skipError.compareAndSet(expect = true, update = false) companion object : CoroutineContext.Key { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index 771ac4103..46e5ae20a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -27,7 +27,6 @@ import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.ui.BaseFragment -import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog import org.koitharu.kotatsu.core.ui.list.FitHeightGridLayoutManager import org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager @@ -46,7 +45,7 @@ import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.details.ui.DetailsActivity -import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver +import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.QuickFilterListener @@ -126,6 +125,7 @@ abstract class MangaListFragment : isEnabled = isSwipeRefreshEnabled } addMenuProvider(MangaListMenuProvider(this)) + DownloadDialogFragment.registerCallback(this, binding.recyclerView) viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged) viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged) @@ -133,7 +133,6 @@ abstract class MangaListFragment : viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) - viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView)) } override fun onDestroyView() { @@ -324,11 +323,8 @@ abstract class MangaListFragment : } R.id.action_save -> { - val itemsSnapshot = selectedItems - CommonAlertDialogs.showDownloadConfirmation(context ?: return false) { startPaused -> - mode?.finish() - viewModel.download(itemsSnapshot, isPaused = startPaused) - } + DownloadDialogFragment.show(childFragmentManager, selectedItems) + mode?.finish() true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 700c26e62..372edaba8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -17,7 +17,6 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.util.ext.MutableEventFlow -import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.ui.model.ListModel @@ -37,7 +36,6 @@ abstract class MangaListViewModel( key = AppSettings.KEY_GRID_SIZE, valueProducer = { gridSize / 100f }, ) - val onDownloadStarted = MutableEventFlow() val isIncognitoModeEnabled: Boolean get() = settings.isIncognitoModeEnabled @@ -46,13 +44,6 @@ abstract class MangaListViewModel( abstract fun onRetry() - fun download(items: Set, isPaused: Boolean) { - launchJob(Dispatchers.Default) { - downloadScheduler.schedule(items, isPaused) - onDownloadStarted.call(Unit) - } - } - protected fun List.skipNsfwIfNeeded() = if (settings.isNsfwContentDisabled) { filterNot { it.isNsfw } } else { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index aa9cb07a2..caa49998a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -200,8 +200,8 @@ class LocalMangaRepository @Inject constructor( override suspend fun getRelated(seed: Manga): List = emptyList() - suspend fun getOutputDir(manga: Manga): File? { - val defaultDir = storageManager.getDefaultWriteableDir() + suspend fun getOutputDir(manga: Manga, fallback: File?): File? { + val defaultDir = fallback ?: storageManager.getDefaultWriteableDir() if (defaultDir != null && LocalMangaOutput.get(defaultDir, manga) != null) { return defaultDir } diff --git a/app/src/main/res/layout-w600dp-land/activity_details.xml b/app/src/main/res/layout-w600dp-land/activity_details.xml index e3e6fab06..f9fb29423 100644 --- a/app/src/main/res/layout-w600dp-land/activity_details.xml +++ b/app/src/main/res/layout-w600dp-land/activity_details.xml @@ -218,7 +218,7 @@ android:padding="@dimen/grid_spacing" android:singleLine="true" android:text="@string/description" - android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" + android:textAppearance="?textAppearanceTitleSmall" app:layout_constraintEnd_toStartOf="@id/button_description_more" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/button_read" /> @@ -274,7 +274,7 @@ android:padding="@dimen/grid_spacing" android:singleLine="true" android:text="@string/tracking" - android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" + android:textAppearance="?textAppearanceTitleSmall" app:layout_constraintEnd_toStartOf="@id/button_scrobbling_more" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/chips_tags" /> @@ -343,7 +343,7 @@ android:padding="@dimen/grid_spacing" android:singleLine="true" android:text="@string/related_manga" - android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" + android:textAppearance="?textAppearanceTitleSmall" app:layout_constraintEnd_toStartOf="@id/button_related_more" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/recyclerView_scrobbling" /> diff --git a/app/src/main/res/layout-w600dp-land/activity_settings.xml b/app/src/main/res/layout-w600dp-land/activity_settings.xml index 4b3d15d9e..244fedc9d 100644 --- a/app/src/main/res/layout-w600dp-land/activity_settings.xml +++ b/app/src/main/res/layout-w600dp-land/activity_settings.xml @@ -47,7 +47,7 @@ android:gravity="center_vertical|start" android:padding="8dp" android:singleLine="true" - android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" + android:textAppearance="?textAppearanceTitleSmall" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/container_master" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/activity_appwidget_shelf.xml b/app/src/main/res/layout/activity_appwidget_shelf.xml index 8fae4aa9a..c69f0d86b 100644 --- a/app/src/main/res/layout/activity_appwidget_shelf.xml +++ b/app/src/main/res/layout/activity_appwidget_shelf.xml @@ -51,7 +51,7 @@ android:paddingEnd="?listPreferredItemPaddingEnd" android:singleLine="true" android:text="@string/favourites_categories" - android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" /> + android:textAppearance="?textAppearanceTitleSmall" /> diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml index ecb78409f..a4c2ac740 100644 --- a/app/src/main/res/layout/activity_details.xml +++ b/app/src/main/res/layout/activity_details.xml @@ -227,7 +227,7 @@ android:padding="@dimen/grid_spacing" android:singleLine="true" android:text="@string/description" - android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" + android:textAppearance="?textAppearanceTitleSmall" app:layout_constraintEnd_toStartOf="@id/button_description_more" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/button_read" /> @@ -283,7 +283,7 @@ android:padding="@dimen/grid_spacing" android:singleLine="true" android:text="@string/tracking" - android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" + android:textAppearance="?textAppearanceTitleSmall" app:layout_constraintEnd_toStartOf="@id/button_scrobbling_more" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/chips_tags" /> @@ -352,7 +352,7 @@ android:padding="@dimen/grid_spacing" android:singleLine="true" android:text="@string/related_manga" - android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" + android:textAppearance="?textAppearanceTitleSmall" app:layout_constraintEnd_toStartOf="@id/button_related_more" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/recyclerView_scrobbling" /> diff --git a/app/src/main/res/layout/dialog_download.xml b/app/src/main/res/layout/dialog_download.xml new file mode 100644 index 000000000..16841ae30 --- /dev/null +++ b/app/src/main/res/layout/dialog_download.xml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +