From 77e393ae48280f34dcc54627e0b3ec245a39328d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 29 Jun 2024 15:56:32 +0300 Subject: [PATCH 01/26] Pages crop proof-of-concept --- app/build.gradle | 12 +-- .../kotatsu/core/prefs/AppSettings.kt | 15 +++- .../reader/domain/DetectReaderModeUseCase.kt | 1 + .../kotatsu/reader/domain/PageLoader.kt | 10 +++ .../reader/domain/WhitespaceDetector.kt | 79 +++++++++++++++++++ .../reader/ui/config/ReaderSettings.kt | 5 ++ .../kotatsu/reader/ui/pager/BasePageHolder.kt | 12 ++- .../reader/ui/pager/PageHolderDelegate.kt | 22 +++++- .../reader/ui/pager/standard/PageHolder.kt | 15 ++-- .../reader/ui/pager/webtoon/WebtoonHolder.kt | 15 ++-- .../settings/ReaderSettingsFragment.kt | 5 ++ app/src/main/res/values/arrays.xml | 4 + app/src/main/res/values/constants.xml | 4 + app/src/main/res/xml/pref_reader.xml | 6 ++ 14 files changed, 182 insertions(+), 23 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/WhitespaceDetector.kt diff --git a/app/build.gradle b/app/build.gradle index faa3a2bf8..a2af9bac0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,7 +93,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.activity:activity-ktx:1.9.0' - implementation 'androidx.fragment:fragment-ktx:1.8.0' + implementation 'androidx.fragment:fragment-ktx:1.8.1' implementation 'androidx.transition:transition-ktx:1.5.0' implementation 'androidx.collection:collection-ktx:1.4.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2' @@ -136,7 +136,7 @@ dependencies { implementation 'io.coil-kt:coil-base:2.6.0' implementation 'io.coil-kt:coil-svg:2.6.0' - implementation 'com.github.KotatsuApp:subsampling-scale-image-view:8cafac256e' + implementation 'com.github.KotatsuApp:subsampling-scale-image-view:882bc0620c' implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'io.noties.markwon:core:4.6.2' @@ -154,10 +154,10 @@ dependencies { testImplementation 'org.json:json:20240303' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation 'androidx.test:core-ktx:1.5.0' - androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' + androidTestImplementation 'androidx.test:runner:1.6.1' + androidTestImplementation 'androidx.test:rules:1.6.1' + androidTestImplementation 'androidx.test:core-ktx:1.6.1' + androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.1' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 4f652cfe6..fdbd623d8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -33,7 +33,6 @@ import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import java.io.File import java.net.Proxy import java.util.EnumSet -import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -485,6 +484,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { val isAutoLocalChaptersCleanupEnabled: Boolean get() = prefs.getBoolean(KEY_CHAPTERS_CLEAR_AUTO, false) + fun isPagesCropEnabled(mode: ReaderMode): Boolean { + val rawValue = prefs.getStringSet(KEY_READER_CROP, emptySet()) + if (rawValue.isNullOrEmpty()) { + return false + } + val needle = if (mode == ReaderMode.WEBTOON) READER_CROP_WEBTOON else READER_CROP_PAGED + return needle.toString() in rawValue + } + fun isTipEnabled(tip: String): Boolean { return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true } @@ -597,6 +605,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_READER_ANIMATION = "reader_animation2" const val KEY_READER_MODE = "reader_mode" const val KEY_READER_MODE_DETECT = "reader_mode_detect" + const val KEY_READER_CROP = "reader_crop" const val KEY_APP_PASSWORD = "app_password" const val KEY_APP_PASSWORD_NUMERIC = "app_password_num" const val KEY_PROTECT_APP = "protect_app" @@ -698,5 +707,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { // old keys are for migration only private const val KEY_IMAGES_PROXY_OLD = "images_proxy" + + // values + private const val READER_CROP_PAGED = 1 + private const val READER_CROP_WEBTOON = 2 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt index cb7ec0917..1be842980 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt @@ -61,6 +61,7 @@ class DetectReaderModeUseCase @Inject constructor( val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" } val url = repository.getPageUrl(page) val uri = Uri.parse(url) + // TODO file support val size = if (uri.scheme == "cbz") { runInterruptible(Dispatchers.IO) { val zip = ZipFile(uri.schemeSpecificPart) 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 ec6e41640..c0ffb3d70 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 @@ -2,12 +2,14 @@ package org.koitharu.kotatsu.reader.domain import android.content.Context import android.graphics.BitmapFactory +import android.graphics.Rect import android.net.Uri import androidx.annotation.AnyThread import androidx.collection.LongSparseArray import androidx.collection.set import androidx.core.net.toFile import androidx.core.net.toUri +import com.davemorrissey.labs.subscaleview.ImageSource import dagger.hilt.android.ActivityRetainedLifecycle import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ActivityRetainedScoped @@ -51,6 +53,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.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import java.util.LinkedList import java.util.concurrent.atomic.AtomicInteger @@ -83,6 +86,7 @@ class PageLoader @Inject constructor( private val prefetchQueue = LinkedList() private val counter = AtomicInteger(0) private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive + private val whitespaceDetector = WhitespaceDetector(context) fun isPrefetchApplicable(): Boolean { return repository is RemoteMangaRepository @@ -154,6 +158,12 @@ class PageLoader @Inject constructor( } } + suspend fun getTrimmedBounds(uri: Uri): Rect? = runCatchingCancellable { + whitespaceDetector.getBounds(ImageSource.Uri(uri)) + }.onFailure { error -> + error.printStackTraceDebug() + }.getOrNull() + suspend fun getPageUrl(page: MangaPage): String { return getRepository(page.source).getPageUrl(page) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/WhitespaceDetector.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/WhitespaceDetector.kt new file mode 100644 index 000000000..06e32e7c5 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/WhitespaceDetector.kt @@ -0,0 +1,79 @@ +package org.koitharu.kotatsu.reader.domain + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Point +import android.graphics.Rect +import androidx.core.graphics.get +import com.davemorrissey.labs.subscaleview.ImageSource +import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder +import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.math.abs + +class WhitespaceDetector( + private val context: Context +) { + + private val mutex = Mutex() + + suspend fun getBounds(imageSource: ImageSource): Rect? = mutex.withLock { + runInterruptible(Dispatchers.IO) { + val decoder = SkiaImageRegionDecoder(Bitmap.Config.RGB_565) + try { + val size = decoder.init(context, imageSource) + detectWhitespaces(decoder, size) + } finally { + decoder.recycle() + } + } + } + + // TODO + private fun detectWhitespaces(decoder: ImageRegionDecoder, size: Point): Rect? { + val result = Rect(0, 0, size.x, size.y) + val window = Rect() + val windowSize = 200 + + var baseColor = -1 + window.set(0, 0, windowSize, windowSize) + decoder.decodeRegion(window, 1).use { bitmap -> + baseColor = bitmap[0, 0] + outerTop@ for (x in 1 until bitmap.width / 2) { + for (y in 1 until bitmap.height / 2) { + if (isSameColor(baseColor, bitmap[x, y])) { + result.left = x + result.top = y + } else { + break@outerTop + } + } + } + } + window.set(size.x - windowSize - 1, size.y - windowSize - 1, size.x - 1, size.y - 1) + decoder.decodeRegion(window, 1).use { bitmap -> + outerBottom@ for (x in (bitmap.width / 2 until bitmap.width).reversed()) { + for (y in (bitmap.height / 2 until bitmap.height).reversed()) { + if (isSameColor(baseColor, bitmap[x, y])) { + result.right = size.x - x + result.bottom = size.y - y + } else { + break@outerBottom + } + } + } + } + return result.takeUnless { it.isEmpty || (it.width() == size.x && it.height() == size.y) } + } + + private fun isSameColor(a: Int, b: Int) = abs(a - b) <= 4 // TODO + + private inline fun Bitmap.use(block: (Bitmap) -> R) = try { + block(this) + } finally { + recycle() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt index 60708cfcc..f93822540 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderSettings.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.util.ext.isLowRamDevice import org.koitharu.kotatsu.reader.domain.ReaderColorFilter @@ -54,6 +55,10 @@ class ReaderSettings( view.background = bg.resolve(view.context) } + fun isPagesCropEnabled(isWebtoon: Boolean) = settings.isPagesCropEnabled( + if (isWebtoon) ReaderMode.WEBTOON else ReaderMode.STANDARD, + ) + @CheckResult fun applyBitmapConfig(ssiv: SubsamplingScaleImageView): Boolean { val config = bitmapConfig diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt index 001cfdf0d..a85d98616 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt @@ -13,6 +13,7 @@ import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.pager.PageHolderDelegate.State +import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonHolder abstract class BasePageHolder( protected val binding: B, @@ -24,7 +25,14 @@ abstract class BasePageHolder( ) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), PageHolderDelegate.Callback { @Suppress("LeakingThis") - protected val delegate = PageHolderDelegate(loader, settings, this, networkState, exceptionResolver) + protected val delegate = PageHolderDelegate( + loader = loader, + readerSettings = settings, + callback = this, + networkState = networkState, + exceptionResolver = exceptionResolver, + isWebtoon = this is WebtoonHolder, + ) protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root) val context: Context @@ -70,7 +78,7 @@ abstract class BasePageHolder( delegate.onRecycle() } - protected fun SubsamplingScaleImageView.applyDownsampling(isForeground: Boolean) { + protected fun SubsamplingScaleImageView.applyDownSampling(isForeground: Boolean) { downSampling = when { isForeground || !settings.isReaderOptimizationEnabled -> 1 context.isLowRamDevice() -> 8 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index 0b58a3c31..3cfcea351 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.reader.ui.pager +import android.graphics.Rect import android.net.Uri import androidx.lifecycle.Observer import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener @@ -32,6 +33,7 @@ class PageHolderDelegate( private val callback: Callback, private val networkState: NetworkState, private val exceptionResolver: ExceptionResolver, + private val isWebtoon: Boolean, ) : DefaultOnImageEventListener, Observer { private val scope = loader.loaderScope + Dispatchers.Main.immediate @@ -39,6 +41,7 @@ class PageHolderDelegate( private set private var job: Job? = null private var uri: Uri? = null + private var cachedBounds: Rect? = null private var error: Throwable? = null init { @@ -88,6 +91,7 @@ class PageHolderDelegate( fun onRecycle() { state = State.EMPTY uri = null + cachedBounds = null error = null job?.cancel() } @@ -95,7 +99,7 @@ class PageHolderDelegate( fun reload() { if (state == State.SHOWN) { uri?.let { - callback.onImageReady(it) + callback.onImageReady(it, cachedBounds) } } } @@ -138,8 +142,13 @@ class PageHolderDelegate( state = State.CONVERTING try { val newUri = loader.convertBimap(uri) + cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) { + loader.getTrimmedBounds(newUri) + } else { + null + } state = State.CONVERTED - callback.onImageReady(newUri) + callback.onImageReady(newUri, cachedBounds) } catch (ce: CancellationException) { throw ce } catch (e2: Throwable) { @@ -166,7 +175,12 @@ class PageHolderDelegate( file } state = State.LOADED - callback.onImageReady(checkNotNull(uri)) + cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) { + loader.getTrimmedBounds(checkNotNull(uri)) + } else { + null + } + callback.onImageReady(checkNotNull(uri), cachedBounds) } catch (e: CancellationException) { throw e } catch (e: Throwable) { @@ -196,7 +210,7 @@ class PageHolderDelegate( fun onError(e: Throwable) - fun onImageReady(uri: Uri) + fun onImageReady(uri: Uri, bounds: Rect?) fun onImageShowing(settings: ReaderSettings) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt index c4b9d6b80..e724884b4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard import android.annotation.SuppressLint import android.graphics.PointF +import android.graphics.Rect import android.net.Uri import android.view.View import android.view.animation.DecelerateInterpolator @@ -46,12 +47,12 @@ open class PageHolder( override fun onResume() { super.onResume() - binding.ssiv.applyDownsampling(isForeground = true) + binding.ssiv.applyDownSampling(isForeground = true) } override fun onPause() { super.onPause() - binding.ssiv.applyDownsampling(isForeground = false) + binding.ssiv.applyDownSampling(isForeground = false) } override fun onConfigChanged() { @@ -59,7 +60,7 @@ open class PageHolder( if (settings.applyBitmapConfig(binding.ssiv)) { delegate.reload() } - binding.ssiv.applyDownsampling(isResumed()) + binding.ssiv.applyDownSampling(isResumed()) binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled } @@ -89,8 +90,12 @@ open class PageHolder( } } - override fun onImageReady(uri: Uri) { - binding.ssiv.setImage(ImageSource.Uri(uri)) + override fun onImageReady(uri: Uri, bounds: Rect?) { + val source = ImageSource.Uri(uri) + if (bounds != null) { + source.region(bounds) + } + binding.ssiv.setImage(source) } override fun onImageShowing(settings: ReaderSettings) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt index 48111be7b..745a57971 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon +import android.graphics.Rect import android.net.Uri import android.view.View import androidx.core.view.isVisible @@ -39,12 +40,12 @@ class WebtoonHolder( override fun onResume() { super.onResume() - binding.ssiv.applyDownsampling(isForeground = true) + binding.ssiv.applyDownSampling(isForeground = true) } override fun onPause() { super.onPause() - binding.ssiv.applyDownsampling(isForeground = false) + binding.ssiv.applyDownSampling(isForeground = false) } override fun onConfigChanged() { @@ -52,7 +53,7 @@ class WebtoonHolder( if (settings.applyBitmapConfig(binding.ssiv)) { delegate.reload() } - binding.ssiv.applyDownsampling(isResumed()) + binding.ssiv.applyDownSampling(isResumed()) } override fun onBind(data: ReaderPage) { @@ -89,8 +90,12 @@ class WebtoonHolder( } } - override fun onImageReady(uri: Uri) { - binding.ssiv.setImage(ImageSource.Uri(uri)) + override fun onImageReady(uri: Uri, bounds: Rect?) { + val source = ImageSource.Uri(uri) + if (bounds != null) { + source.region(bounds) + } + binding.ssiv.setImage(source) } override fun onImageShowing(settings: ReaderSettings) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt index be4e8be6c..d46843db9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.os.Bundle import android.view.View import androidx.preference.ListPreference +import androidx.preference.MultiSelectListPreference import androidx.preference.Preference import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R @@ -17,6 +18,7 @@ import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat import org.koitharu.kotatsu.parsers.util.names import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity +import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider import org.koitharu.kotatsu.settings.utils.PercentSummaryProvider import org.koitharu.kotatsu.settings.utils.SliderPreference @@ -48,6 +50,9 @@ class ReaderSettingsFragment : entryValues = ZoomMode.entries.names() setDefaultValueCompat(ZoomMode.FIT_CENTER.name) } + findPreference(AppSettings.KEY_READER_CROP)?.run { + summaryProvider = MultiSummaryProvider(R.string.disabled) + } findPreference(AppSettings.KEY_WEBTOON_ZOOM_OUT)?.summaryProvider = PercentSummaryProvider() updateReaderModeDependency() } diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 06552452b..2107a6fe3 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -97,4 +97,8 @@ @string/system_default @string/more_frequently + + @string/pages + @string/webtoon + diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 8c22f632a..5fbd6afaa 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -68,4 +68,8 @@ 1 2 + + 1 + 2 + diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml index e13444949..a991989ab 100644 --- a/app/src/main/res/xml/pref_reader.xml +++ b/app/src/main/res/xml/pref_reader.xml @@ -88,6 +88,12 @@ android:summary="@string/reader_optimize_summary" android:title="@string/reader_optimize" /> + + Date: Sat, 6 Jul 2024 12:46:34 +0300 Subject: [PATCH 02/26] Update parsers and add image server option support --- app/build.gradle | 10 +++--- .../kotatsu/core/prefs/SourceSettings.kt | 2 ++ .../settings/sources/SourceSettingsExt.kt | 31 ++++++++++++++++--- .../sources/SourceSettingsFragment.kt | 2 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_source.xml | 6 ++-- build.gradle | 2 +- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a2af9bac0..f26e16455 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:7ed8c9f787') { + implementation('com.github.KotatsuApp:kotatsu-parsers:7433fb8fa0') { exclude group: 'org.json', module: 'json' } @@ -96,9 +96,9 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.8.1' implementation 'androidx.transition:transition-ktx:1.5.0' implementation 'androidx.collection:collection-ktx:1.4.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2' - implementation 'androidx.lifecycle:lifecycle-service:2.8.2' - implementation 'androidx.lifecycle:lifecycle-process:2.8.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3' + implementation 'androidx.lifecycle:lifecycle-service:2.8.3' + implementation 'androidx.lifecycle:lifecycle-process:2.8.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' @@ -106,7 +106,7 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' implementation 'com.google.android.material:material:1.12.0' - implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.2' + implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.3' implementation 'androidx.webkit:webkit:1.11.0' implementation 'androidx.work:work-runtime:2.9.0' diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt index 8b87e8169..6fa8a8ae4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt @@ -38,6 +38,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue) is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue) + is ConfigKey.PreferredImageServer -> prefs.getString(key.key, key.defaultValue)?.takeUnless(String::isEmpty) } as T } @@ -47,6 +48,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean) is ConfigKey.UserAgent -> putString(key.key, (value as String?)?.sanitizeHeaderValue()) is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean) + is ConfigKey.PreferredImageServer -> putString(key.key, value as String?) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt index 064697808..59f249c17 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.sources import android.view.inputmethod.EditorInfo import androidx.preference.EditTextPreference +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat @@ -23,9 +24,9 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang is ConfigKey.Domain -> { val presetValues = key.presetValues if (presetValues.size <= 1) { - EditTextPreference(requireContext()) + EditTextPreference(screen.context) } else { - AutoCompleteTextViewPreference(requireContext()).apply { + AutoCompleteTextViewPreference(screen.context).apply { entries = presetValues.toStringArray() } }.apply { @@ -43,7 +44,7 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang } is ConfigKey.UserAgent -> { - AutoCompleteTextViewPreference(requireContext()).apply { + AutoCompleteTextViewPreference(screen.context).apply { entries = arrayOf( UserAgents.FIREFOX_MOBILE, UserAgents.CHROME_MOBILE, @@ -64,19 +65,32 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang } is ConfigKey.ShowSuspiciousContent -> { - SwitchPreferenceCompat(requireContext()).apply { + SwitchPreferenceCompat(screen.context).apply { setDefaultValue(key.defaultValue) setTitle(R.string.show_suspicious_content) } } is ConfigKey.SplitByTranslations -> { - SwitchPreferenceCompat(requireContext()).apply { + SwitchPreferenceCompat(screen.context).apply { setDefaultValue(key.defaultValue) setTitle(R.string.split_by_translations) setSummary(R.string.split_by_translations_summary) } } + + is ConfigKey.PreferredImageServer -> { + ListPreference(screen.context).apply { + entries = key.presetValues.values.mapToArray { + it ?: context.getString(R.string.automatic) + } + entryValues = key.presetValues.keys.mapToArray { it.orEmpty() } + setDefaultValue(key.defaultValue.orEmpty()) + setTitle(R.string.image_server) + setDialogTitle(R.string.image_server) + summaryProvider = ListPreference.SimpleSummaryProvider.getInstance() + } + } } preference.isIconSpaceReserved = false preference.key = key.key @@ -88,3 +102,10 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang private fun Array.toStringArray(): Array { return Array(size) { i -> this[i] as? String ?: "" } } + +@Suppress("UNCHECKED_CAST") +private inline fun Collection.mapToArray(transform: (T) -> R): Array { + val result = arrayOfNulls(size) + forEachIndexed { index, t -> result[index] = transform(t) } + return result as Array +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt index 460bdabcc..55923a382 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsFragment.kt @@ -36,7 +36,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenc addPreferencesFromRepository(viewModel.repository) findPreference(KEY_ENABLE)?.run { - setOnPreferenceChangeListener(this@SourceSettingsFragment) + onPreferenceChangeListener = this@SourceSettingsFragment } findPreference(KEY_AUTH)?.run { val authProvider = viewModel.repository.getAuthProvider() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2e0983cd..0c1e79e42 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -654,4 +654,5 @@ New All languages Block when incognito mode + Preferred image server diff --git a/app/src/main/res/xml/pref_source.xml b/app/src/main/res/xml/pref_source.xml index e27b1eb4a..3ce86fef8 100644 --- a/app/src/main/res/xml/pref_source.xml +++ b/app/src/main/res/xml/pref_source.xml @@ -23,14 +23,14 @@ android:order="101" android:persistent="false" android:summary="@string/clear_source_cookies_summary" - android:title="@string/clear_cookies" /> + android:title="@string/clear_cookies" + app:allowDividerAbove="true" /> + android:title="@string/download_slowdown" /> diff --git a/build.gradle b/build.gradle index 72ac4332e..e4c4dcc09 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.4.1' + classpath 'com.android.tools.build:gradle:8.5.0' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1' classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.24-1.0.20' From dfb50fbddc8c375b5cc6b3fb164b3bb8acdf18b1 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 6 Jul 2024 14:21:46 +0300 Subject: [PATCH 03/26] Add image server option to reader config sheet --- .../kotatsu/core/prefs/SourceSettings.kt | 2 +- .../kotatsu/core/util/ext/Collections.kt | 9 +- .../kotatsu/core/util/ext/Coroutines.kt | 11 +++ .../koitharu/kotatsu/local/data/PagesCache.kt | 7 ++ .../kotatsu/reader/domain/PageLoader.kt | 9 ++ .../kotatsu/reader/ui/ReaderViewModel.kt | 27 ++++-- .../reader/ui/config/ImageServerDelegate.kt | 85 +++++++++++++++++++ .../reader/ui/config/ReaderConfigSheet.kt | 39 +++++++++ .../settings/sources/SourceSettingsExt.kt | 8 +- app/src/main/res/drawable/ic_images.xml | 12 +++ .../main/res/layout/sheet_reader_config.xml | 13 +++ app/src/main/res/values/strings.xml | 1 + 12 files changed, 206 insertions(+), 17 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt create mode 100644 app/src/main/res/drawable/ic_images.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt index 6fa8a8ae4..8b38fb341 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SourceSettings.kt @@ -48,7 +48,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean) is ConfigKey.UserAgent -> putString(key.key, (value as String?)?.sanitizeHeaderValue()) is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean) - is ConfigKey.PreferredImageServer -> putString(key.key, value as String?) + is ConfigKey.PreferredImageServer -> putString(key.key, value as String? ?: "") } } 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 fb41830ae..11400295c 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 @@ -69,4 +69,11 @@ fun Iterable.sortedWithSafe(comparator: Comparator): List = try } } -fun Collection<*>?.sizeOrZero() = if (this == null) 0 else size +fun Collection<*>?.sizeOrZero() = this?.size ?: 0 + +@Suppress("UNCHECKED_CAST") +inline fun Collection.mapToArray(transform: (T) -> R): Array { + val result = arrayOfNulls(size) + forEachIndexed { index, t -> result[index] = transform(t) } + return result as Array +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt index c2ccef138..ab7f26c38 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coroutines.kt @@ -12,12 +12,16 @@ import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.joinAll import kotlinx.coroutines.plus import kotlinx.coroutines.suspendCancellableCoroutine import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope +import org.koitharu.kotatsu.parsers.util.cancelAll import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -90,3 +94,10 @@ fun Deferred.peek(): T? = if (isCompleted) { } else { null } + +@Suppress("SuspendFunctionOnCoroutineScope") +suspend fun CoroutineScope.cancelChildrenAndJoin(cause: CancellationException? = null) { + val jobs = coroutineContext[Job]?.children?.toList() ?: return + jobs.cancelAll(cause) + jobs.joinAll() +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt index a8c3763f7..a66b60956 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/PagesCache.kt @@ -82,6 +82,13 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) { } } + suspend fun clear() { + val cache = lruCache.get() + runInterruptible(Dispatchers.IO) { + cache.clearCache() + } + } + private suspend fun getAvailableSize(): Long = runCatchingCancellable { val statFs = StatFs(cacheDir.get().absolutePath) statFs.availableBytes 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 c0ffb3d70..36654dd7d 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 @@ -37,6 +37,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP +import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin import org.koitharu.kotatsu.core.util.ext.compressToPNG import org.koitharu.kotatsu.core.util.ext.ensureRamAtLeast import org.koitharu.kotatsu.core.util.ext.ensureSuccess @@ -168,6 +169,14 @@ class PageLoader @Inject constructor( return getRepository(page.source).getPageUrl(page) } + suspend fun invalidate(clearCache: Boolean) { + tasks.clear() + loaderScope.cancelChildrenAndJoin() + if (clearCache) { + cache.clear() + } + } + private fun onIdle() = loaderScope.launch { prefetchLock.withLock { while (prefetchQueue.isNotEmpty()) { 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 5a145b563..b70924a8d 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 @@ -291,17 +291,28 @@ constructor( val prevJob = loadingJob loadingJob = launchLoadingJob(Dispatchers.Default) { prevJob?.cancelAndJoin() - val currentChapterId = currentState.requireValue().chapterId - val allChapters = checkNotNull(manga).allChapters - var index = allChapters.indexOfFirst { x -> x.id == currentChapterId } - if (index < 0) { - return@launchLoadingJob + val prevState = currentState.requireValue() + val newChapterId = if (delta != 0) { + val allChapters = checkNotNull(manga).allChapters + var index = allChapters.indexOfFirst { x -> x.id == prevState.chapterId } + if (index < 0) { + return@launchLoadingJob + } + index += delta + (allChapters.getOrNull(index) ?: return@launchLoadingJob).id + } else { + prevState.chapterId } - index += delta - val newChapterId = (allChapters.getOrNull(index) ?: return@launchLoadingJob).id content.value = ReaderContent(emptyList(), null) chaptersLoader.loadSingleChapter(newChapterId) - content.value = ReaderContent(chaptersLoader.snapshot(), ReaderState(newChapterId, 0, 0)) + content.value = ReaderContent( + chaptersLoader.snapshot(), + ReaderState( + chapterId = newChapterId, + page = if (delta == 0) prevState.page else 0, + scroll = if (delta == 0) prevState.scroll else 0, + ), + ) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt new file mode 100644 index 000000000..8090ead79 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ImageServerDelegate.kt @@ -0,0 +1,85 @@ +package org.koitharu.kotatsu.reader.ui.config + +import android.content.Context +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.core.util.ext.mapToArray +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.SuspendLazy +import kotlin.coroutines.resume + +class ImageServerDelegate( + private val mangaRepositoryFactory: MangaRepository.Factory, + private val mangaSource: MangaSource?, +) { + + private val repositoryLazy = SuspendLazy { + mangaRepositoryFactory.create(checkNotNull(mangaSource)) as RemoteMangaRepository + } + + suspend fun isAvailable() = withContext(Dispatchers.Default) { + repositoryLazy.tryGet().map { repository -> + repository.getConfigKeys().any { it is ConfigKey.PreferredImageServer } + }.getOrDefault(false) + } + + suspend fun getValue(): String? = withContext(Dispatchers.Default) { + repositoryLazy.tryGet().map { repository -> + val key = repository.getConfigKeys().firstNotNullOfOrNull { it as? ConfigKey.PreferredImageServer } + if (key != null) { + key.presetValues[repository.getConfig()[key]] + } else { + null + } + }.getOrNull() + } + + suspend fun showDialog(context: Context): Boolean { + val repository = withContext(Dispatchers.Default) { + repositoryLazy.tryGet().getOrNull() + } ?: return false + val key = repository.getConfigKeys().firstNotNullOfOrNull { + it as? ConfigKey.PreferredImageServer + } ?: return false + val entries = key.presetValues.values.mapToArray { + it ?: context.getString(R.string.automatic) + } + val entryValues = key.presetValues.keys.toTypedArray() + val config = repository.getConfig() + val initialValue = config[key] + var currentValue = initialValue + val changed = suspendCancellableCoroutine { cont -> + val dialog = MaterialAlertDialogBuilder(context) + .setTitle(R.string.image_server) + .setCancelable(true) + .setSingleChoiceItems(entries, entryValues.indexOf(initialValue)) { _, i -> + currentValue = entryValues[i] + }.setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.cancel() + }.setPositiveButton(android.R.string.ok) { _, _ -> + if (currentValue != initialValue) { + config[key] = currentValue + cont.resume(true) + } else { + cont.resume(false) + } + }.setOnCancelListener { + cont.resume(false) + }.create() + dialog.show() + cont.invokeOnCancellation { + dialog.cancel() + } + } + if (changed) { + repository.invalidateCache() + } + return changed + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt index 93c03a049..cfb59eb12 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/config/ReaderConfigSheet.kt @@ -16,8 +16,10 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.coroutines.plus import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.observeAsStateFlow @@ -29,6 +31,7 @@ import org.koitharu.kotatsu.core.util.ext.showDistinct import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding +import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.ReaderViewModel import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity import org.koitharu.kotatsu.settings.SettingsActivity @@ -47,7 +50,14 @@ class ReaderConfigSheet : @Inject lateinit var orientationHelper: ScreenOrientationHelper + @Inject + lateinit var mangaRepositoryFactory: MangaRepository.Factory + + @Inject + lateinit var pageLoader: PageLoader + private lateinit var mode: ReaderMode + private lateinit var imageServerDelegate: ImageServerDelegate @Inject lateinit var settings: AppSettings @@ -57,6 +67,10 @@ class ReaderConfigSheet : mode = arguments?.getInt(ARG_MODE) ?.let { ReaderMode.valueOf(it) } ?: ReaderMode.STANDARD + imageServerDelegate = ImageServerDelegate( + mangaRepositoryFactory = mangaRepositoryFactory, + mangaSource = viewModel.manga?.toManga()?.source, + ) } override fun onCreateViewBinding( @@ -83,11 +97,20 @@ class ReaderConfigSheet : binding.buttonSavePage.setOnClickListener(this) binding.buttonScreenRotate.setOnClickListener(this) binding.buttonSettings.setOnClickListener(this) + binding.buttonImageServer.setOnClickListener(this) binding.buttonColorFilter.setOnClickListener(this) binding.sliderTimer.addOnChangeListener(this) binding.switchScrollTimer.setOnCheckedChangeListener(this) binding.switchDoubleReader.setOnCheckedChangeListener(this) + viewLifecycleScope.launch { + val isAvailable = imageServerDelegate.isAvailable() + if (isAvailable) { + bindImageServerTitle() + } + binding.buttonImageServer.isVisible = isAvailable + } + settings.observeAsStateFlow( scope = lifecycleScope + Dispatchers.Default, key = AppSettings.KEY_READER_AUTOSCROLL_SPEED, @@ -124,6 +147,14 @@ class ReaderConfigSheet : val manga = viewModel.manga?.toManga() ?: return startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page)) } + + R.id.button_image_server -> viewLifecycleScope.launch { + if (imageServerDelegate.showDialog(v.context)) { + bindImageServerTitle() + pageLoader.invalidate(clearCache = true) + viewModel.switchChapterBy(0) + } + } } } @@ -194,6 +225,14 @@ class ReaderConfigSheet : switch.setOnCheckedChangeListener(this) } + private suspend fun bindImageServerTitle() { + viewBinding?.buttonImageServer?.text = getString( + R.string.inline_preference_pattern, + getString(R.string.image_server), + imageServerDelegate.getValue() ?: getString(R.string.automatic), + ) + } + interface Callback { var isAutoScrollEnabled: Boolean diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt index 59f249c17..ef58ffc15 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/SourceSettingsExt.kt @@ -8,6 +8,7 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.core.util.ext.mapToArray import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.settings.utils.AutoCompleteTextViewPreference @@ -102,10 +103,3 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang private fun Array.toStringArray(): Array { return Array(size) { i -> this[i] as? String ?: "" } } - -@Suppress("UNCHECKED_CAST") -private inline fun Collection.mapToArray(transform: (T) -> R): Array { - val result = arrayOfNulls(size) - forEachIndexed { index, t -> result[index] = transform(t) } - return result as Array -} diff --git a/app/src/main/res/drawable/ic_images.xml b/app/src/main/res/drawable/ic_images.xml new file mode 100644 index 000000000..df36798a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_images.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/sheet_reader_config.xml b/app/src/main/res/layout/sheet_reader_config.xml index ff3ecadfb..fa1bb6547 100644 --- a/app/src/main/res/layout/sheet_reader_config.xml +++ b/app/src/main/res/layout/sheet_reader_config.xml @@ -210,6 +210,19 @@ android:textAppearance="?attr/textAppearanceButton" app:drawableStartCompat="@drawable/ic_appearance" /> + + All languages Block when incognito mode Preferred image server + %1$s: %2$s From 81aac0d431b3b0e21ede2eb9978c0351e96ae8a8 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 6 Jul 2024 19:25:08 +0300 Subject: [PATCH 04/26] Pages crop feature #326 #919 --- .../core/ui/image/TrimTransformation.kt | 22 +-- .../kotatsu/core/util/ext/Graphics.kt | 7 + .../kotatsu/reader/domain/EdgeDetector.kt | 150 ++++++++++++++++++ .../kotatsu/reader/domain/PageLoader.kt | 12 +- .../reader/domain/WhitespaceDetector.kt | 79 --------- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_reader.xml | 2 +- 7 files changed, 169 insertions(+), 104 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/EdgeDetector.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/WhitespaceDetector.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TrimTransformation.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TrimTransformation.kt index 88dda77b5..15695ff11 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TrimTransformation.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/image/TrimTransformation.kt @@ -1,15 +1,10 @@ package org.koitharu.kotatsu.core.ui.image import android.graphics.Bitmap -import androidx.annotation.ColorInt -import androidx.core.graphics.alpha -import androidx.core.graphics.blue import androidx.core.graphics.get -import androidx.core.graphics.green -import androidx.core.graphics.red import coil.size.Size import coil.transform.Transformation -import kotlin.math.abs +import org.koitharu.kotatsu.reader.domain.EdgeDetector.Companion.isColorTheSame class TrimTransformation( private val tolerance: Int = 20, @@ -28,7 +23,7 @@ class TrimTransformation( var isColBlank = true val prevColor = input[x, 0] for (y in 1 until input.height) { - if (!isColorTheSame(input[x, y], prevColor)) { + if (!isColorTheSame(input[x, y], prevColor, tolerance)) { isColBlank = false break } @@ -47,7 +42,7 @@ class TrimTransformation( var isColBlank = true val prevColor = input[x, 0] for (y in 1 until input.height) { - if (!isColorTheSame(input[x, y], prevColor)) { + if (!isColorTheSame(input[x, y], prevColor, tolerance)) { isColBlank = false break } @@ -63,7 +58,7 @@ class TrimTransformation( var isRowBlank = true val prevColor = input[0, y] for (x in 1 until input.width) { - if (!isColorTheSame(input[x, y], prevColor)) { + if (!isColorTheSame(input[x, y], prevColor, tolerance)) { isRowBlank = false break } @@ -79,7 +74,7 @@ class TrimTransformation( var isRowBlank = true val prevColor = input[0, y] for (x in 1 until input.width) { - if (!isColorTheSame(input[x, y], prevColor)) { + if (!isColorTheSame(input[x, y], prevColor, tolerance)) { isRowBlank = false break } @@ -98,13 +93,6 @@ class TrimTransformation( } } - private fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int): Boolean { - return abs(a.red - b.red) <= tolerance && - abs(a.green - b.green) <= tolerance && - abs(a.blue - b.blue) <= tolerance && - abs(a.alpha - b.alpha) <= tolerance - } - override fun equals(other: Any?): Boolean { return this === other || (other is TrimTransformation && other.tolerance == tolerance) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Graphics.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Graphics.kt index 2e59b582f..2a9f0b81c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Graphics.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Graphics.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.util.ext +import android.graphics.Bitmap import android.graphics.Rect import kotlin.math.roundToInt @@ -11,3 +12,9 @@ fun Rect.scale(factor: Double) { (height() - newHeight) / 2, ) } + +inline fun Bitmap.use(block: (Bitmap) -> R) = try { + block(this) +} finally { + recycle() +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/EdgeDetector.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/EdgeDetector.kt new file mode 100644 index 000000000..c21e83300 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/EdgeDetector.kt @@ -0,0 +1,150 @@ +package org.koitharu.kotatsu.reader.domain + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Point +import android.graphics.Rect +import androidx.annotation.ColorInt +import androidx.core.graphics.alpha +import androidx.core.graphics.blue +import androidx.core.graphics.get +import androidx.core.graphics.green +import androidx.core.graphics.red +import com.davemorrissey.labs.subscaleview.ImageSource +import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder +import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import org.koitharu.kotatsu.core.util.ext.use +import kotlin.math.abs + +class EdgeDetector(private val context: Context) { + + private val mutex = Mutex() + + suspend fun getBounds(imageSource: ImageSource): Rect? = mutex.withLock { + withContext(Dispatchers.IO) { + val decoder = SkiaPooledImageRegionDecoder(Bitmap.Config.RGB_565) + try { + val size = runInterruptible { + decoder.init(context, imageSource) + } + val edges = coroutineScope { + listOf( + async { detectLeftRightEdge(decoder, size, isLeft = true) }, + async { detectTopBottomEdge(decoder, size, isTop = true) }, + async { detectLeftRightEdge(decoder, size, isLeft = false) }, + async { detectTopBottomEdge(decoder, size, isTop = false) }, + ).awaitAll() + } + var hasEdges = false + for (edge in edges) { + if (edge > 0) { + hasEdges = true + } else if (edge < 0) { + return@withContext null + } + } + if (hasEdges) { + Rect(edges[0], edges[1], size.x - edges[2], size.y - edges[3]) + } else { + null + } + } finally { + decoder.recycle() + } + } + } + + private fun detectLeftRightEdge(decoder: ImageRegionDecoder, size: Point, isLeft: Boolean): Int { + var width = size.x + val rectCount = size.x / BLOCK_SIZE + val maxRect = rectCount / 3 + for (i in 0 until rectCount) { + if (i > maxRect) { + return -1 + } + var dd = BLOCK_SIZE + for (j in 0 until size.y / BLOCK_SIZE) { + val regionX = if (isLeft) i * BLOCK_SIZE else size.x - (i + 1) * BLOCK_SIZE + decoder.decodeRegion(region(regionX, j * BLOCK_SIZE), 1).use { bitmap -> + for (ii in 0 until minOf(BLOCK_SIZE, dd)) { + for (jj in 0 until BLOCK_SIZE) { + val bi = if (isLeft) ii else BLOCK_SIZE - ii - 1 + if (bitmap[bi, jj].isNotWhite()) { + width = minOf(width, BLOCK_SIZE * i + ii) + dd-- + break + } + } + } + } + if (dd == 0) { + break + } + } + if (dd < BLOCK_SIZE) { + break // We have already found vertical field or it is not exist + } + } + return width + } + + private fun detectTopBottomEdge(decoder: ImageRegionDecoder, size: Point, isTop: Boolean): Int { + var height = size.y + val rectCount = size.y / BLOCK_SIZE + val maxRect = rectCount / 3 + for (j in 0 until rectCount) { + if (j > maxRect) { + return -1 + } + var dd = BLOCK_SIZE + for (i in 0 until size.x / BLOCK_SIZE) { + val regionY = if (isTop) j * BLOCK_SIZE else size.y - (j + 1) * BLOCK_SIZE + decoder.decodeRegion(region(i * BLOCK_SIZE, regionY), 1).use { bitmap -> + for (jj in 0 until minOf(BLOCK_SIZE, dd)) { + for (ii in 0 until BLOCK_SIZE) { + val bj = if (isTop) jj else BLOCK_SIZE - jj - 1 + if (bitmap[ii, bj].isNotWhite()) { + height = minOf(height, BLOCK_SIZE * j + jj) + dd-- + break + } + } + } + } + if (dd == 0) { + break + } + } + if (dd < BLOCK_SIZE) { + break // We have already found vertical field or it is not exist + } + } + return height + } + + companion object { + + private const val BLOCK_SIZE = 100 + private const val COLOR_TOLERANCE = 16 + + fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int, tolerance: Int): Boolean { + return abs(a.red - b.red) <= tolerance && + abs(a.green - b.green) <= tolerance && + abs(a.blue - b.blue) <= tolerance && + abs(a.alpha - b.alpha) <= tolerance + } + + private fun Int.isNotWhite() = !isColorTheSame(this, Color.WHITE, COLOR_TOLERANCE) + + private fun region(x: Int, y: Int) = Rect(x, y, x + BLOCK_SIZE, y + BLOCK_SIZE) + } +} 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 36654dd7d..08d1e537b 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 @@ -47,6 +47,7 @@ import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode import org.koitharu.kotatsu.core.util.ext.isTargetNotEmpty import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.ramAvailable +import org.koitharu.kotatsu.core.util.ext.use import org.koitharu.kotatsu.core.util.ext.withProgress import org.koitharu.kotatsu.core.util.progress.ProgressDeferred import org.koitharu.kotatsu.local.data.PagesCache @@ -87,7 +88,7 @@ class PageLoader @Inject constructor( private val prefetchQueue = LinkedList() private val counter = AtomicInteger(0) private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive - private val whitespaceDetector = WhitespaceDetector(context) + private val edgeDetector = EdgeDetector(context) fun isPrefetchApplicable(): Boolean { return repository is RemoteMangaRepository @@ -147,20 +148,17 @@ class PageLoader @Inject constructor( } else { val file = uri.toFile() context.ensureRamAtLeast(file.length() * 2) - val image = runInterruptible(Dispatchers.IO) { + runInterruptible(Dispatchers.IO) { BitmapFactory.decodeFile(file.absolutePath) - } - try { + }.use { image -> image.compressToPNG(file) - } finally { - image.recycle() } uri } } suspend fun getTrimmedBounds(uri: Uri): Rect? = runCatchingCancellable { - whitespaceDetector.getBounds(ImageSource.Uri(uri)) + edgeDetector.getBounds(ImageSource.Uri(uri)) }.onFailure { error -> error.printStackTraceDebug() }.getOrNull() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/WhitespaceDetector.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/WhitespaceDetector.kt deleted file mode 100644 index 06e32e7c5..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/WhitespaceDetector.kt +++ /dev/null @@ -1,79 +0,0 @@ -package org.koitharu.kotatsu.reader.domain - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Point -import android.graphics.Rect -import androidx.core.graphics.get -import com.davemorrissey.labs.subscaleview.ImageSource -import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder -import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlin.math.abs - -class WhitespaceDetector( - private val context: Context -) { - - private val mutex = Mutex() - - suspend fun getBounds(imageSource: ImageSource): Rect? = mutex.withLock { - runInterruptible(Dispatchers.IO) { - val decoder = SkiaImageRegionDecoder(Bitmap.Config.RGB_565) - try { - val size = decoder.init(context, imageSource) - detectWhitespaces(decoder, size) - } finally { - decoder.recycle() - } - } - } - - // TODO - private fun detectWhitespaces(decoder: ImageRegionDecoder, size: Point): Rect? { - val result = Rect(0, 0, size.x, size.y) - val window = Rect() - val windowSize = 200 - - var baseColor = -1 - window.set(0, 0, windowSize, windowSize) - decoder.decodeRegion(window, 1).use { bitmap -> - baseColor = bitmap[0, 0] - outerTop@ for (x in 1 until bitmap.width / 2) { - for (y in 1 until bitmap.height / 2) { - if (isSameColor(baseColor, bitmap[x, y])) { - result.left = x - result.top = y - } else { - break@outerTop - } - } - } - } - window.set(size.x - windowSize - 1, size.y - windowSize - 1, size.x - 1, size.y - 1) - decoder.decodeRegion(window, 1).use { bitmap -> - outerBottom@ for (x in (bitmap.width / 2 until bitmap.width).reversed()) { - for (y in (bitmap.height / 2 until bitmap.height).reversed()) { - if (isSameColor(baseColor, bitmap[x, y])) { - result.right = size.x - x - result.bottom = size.y - y - } else { - break@outerBottom - } - } - } - } - return result.takeUnless { it.isEmpty || (it.width() == size.x && it.height() == size.y) } - } - - private fun isSameColor(a: Int, b: Int) = abs(a - b) <= 4 // TODO - - private inline fun Bitmap.use(block: (Bitmap) -> R) = try { - block(this) - } finally { - recycle() - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f25127691..c97bb7fef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -656,4 +656,5 @@ Block when incognito mode Preferred image server %1$s: %2$s + Crop pages diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml index a991989ab..6183b186a 100644 --- a/app/src/main/res/xml/pref_reader.xml +++ b/app/src/main/res/xml/pref_reader.xml @@ -92,7 +92,7 @@ android:entries="@array/reader_crop" android:entryValues="@array/values_reader_crop" android:key="reader_crop" - android:title="Crop pages (beta)" /> + android:title="@string/crop_pages" /> Date: Fri, 5 Jul 2024 21:23:15 +0300 Subject: [PATCH 05/26] Add functionality to expand manga title on click --- .../kotatsu/details/ui/DetailsActivity.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) 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 c62e78451..302d60824 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 @@ -3,6 +3,11 @@ package org.koitharu.kotatsu.details.ui import android.content.Context import android.content.Intent import android.graphics.Color +import android.view.MotionEvent +import android.content.ClipData +import android.content.ClipboardManager +import android.view.GestureDetector +import android.view.ViewGroup import android.os.Bundle import android.text.style.DynamicDrawableSpan import android.text.style.ForegroundColorSpan @@ -123,7 +128,7 @@ class DetailsActivity : lateinit var tagHighlighter: ListExtraProvider private val viewModel: DetailsViewModel by viewModels() - + private lateinit var gestureDetector: GestureDetector private lateinit var menuProvider: DetailsMenuProvider override fun onCreate(savedInstanceState: Bundle?) { @@ -157,6 +162,32 @@ class DetailsActivity : viewBinding.containerBottomSheet?.let { sheet -> onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet)) } + gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + val tv = viewBinding.textViewTitle + TransitionManager.beginDelayedTransition(tv.parent as ViewGroup) + if (tv.maxLines == 5) { + tv.maxLines = 20 // Expand text + } else { + tv.maxLines = 5 // Collapse text + } + return true + } + + override fun onLongPress(e: MotionEvent) { + val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("copied text", viewBinding.textViewTitle.text) + clipboardManager.setPrimaryClip(clip) + Toast.makeText(this@DetailsActivity, "Text copied", Toast.LENGTH_SHORT).show() + } + }) + + viewBinding.textViewTitle.setOnTouchListener { _, motionEvent -> + gestureDetector.onTouchEvent(motionEvent) + true + } + + viewModel.details.filterNotNull().observe(this, ::onMangaUpdated) viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved) From a158a488f21e1805711f94fac7c7e6471cf2b5c9 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 07:47:56 +0300 Subject: [PATCH 06/26] Fix error if manga has no chapters --- .../org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt index e59f7f712..a87a32b97 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt @@ -31,7 +31,11 @@ fun HistoryInfo( history: MangaHistory?, isIncognitoMode: Boolean ): HistoryInfo { - val chapters = manga?.chapters?.get(branch) + val chapters = if (manga?.chapters?.isEmpty() == true) { + emptyList() + } else { + manga?.chapters?.get(branch) + } val currentChapter = if (history != null && !chapters.isNullOrEmpty()) { chapters.indexOfFirst { it.id == history.chapterId } } else { From c3ca734005462c7760c8e661516c69b265c3369b Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 08:55:21 +0300 Subject: [PATCH 07/26] Update reader state on chapter switch --- .../kotatsu/reader/ui/ReaderViewModel.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 b70924a8d..38ccf2da1 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 @@ -283,7 +283,9 @@ constructor( prevJob?.cancelAndJoin() content.value = ReaderContent(emptyList(), null) chaptersLoader.loadSingleChapter(id) - content.value = ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0)) + val newState = ReaderState(id, page, 0) + content.value = ReaderContent(chaptersLoader.snapshot(), newState) + saveCurrentState(newState) } } @@ -305,14 +307,13 @@ constructor( } content.value = ReaderContent(emptyList(), null) chaptersLoader.loadSingleChapter(newChapterId) - content.value = ReaderContent( - chaptersLoader.snapshot(), - ReaderState( - chapterId = newChapterId, - page = if (delta == 0) prevState.page else 0, - scroll = if (delta == 0) prevState.scroll else 0, - ), + val newState = ReaderState( + chapterId = newChapterId, + page = if (delta == 0) prevState.page else 0, + scroll = if (delta == 0) prevState.scroll else 0, ) + content.value = ReaderContent(chaptersLoader.snapshot(), newState) + saveCurrentState(newState) } } From 9f5d4ed52c1c46bc9286d04a1fed2d5a7f4ee0f8 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 09:38:34 +0300 Subject: [PATCH 08/26] Refactor details title expansion --- .../core/ui/widgets/SelectableTextView.kt | 7 ++- .../kotatsu/details/ui/DetailsActivity.kt | 33 +------------- .../kotatsu/details/ui/TitleExpandListener.kt | 45 +++++++++++++++++++ app/src/main/res/layout/activity_details.xml | 2 +- app/src/main/res/values/integers.xml | 1 + 5 files changed, 54 insertions(+), 34 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/details/ui/TitleExpandListener.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SelectableTextView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SelectableTextView.kt index 1f80fb57b..41e0f2fc8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SelectableTextView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SelectableTextView.kt @@ -23,11 +23,16 @@ class SelectableTextView @JvmOverloads constructor( private fun fixSelectionRange() { if (selectionStart < 0 || selectionEnd < 0) { val spannableText = text as? Spannable ?: return - Selection.setSelection(spannableText, text.length) + Selection.setSelection(spannableText, spannableText.length) } } override fun scrollTo(x: Int, y: Int) { super.scrollTo(0, 0) } + + fun selectAll() { + val spannableText = text as? Spannable ?: return + Selection.selectAll(spannableText) + } } 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 302d60824..c3e4b772d 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 @@ -3,11 +3,6 @@ package org.koitharu.kotatsu.details.ui import android.content.Context import android.content.Intent import android.graphics.Color -import android.view.MotionEvent -import android.content.ClipData -import android.content.ClipboardManager -import android.view.GestureDetector -import android.view.ViewGroup import android.os.Bundle import android.text.style.DynamicDrawableSpan import android.text.style.ForegroundColorSpan @@ -128,7 +123,6 @@ class DetailsActivity : lateinit var tagHighlighter: ListExtraProvider private val viewModel: DetailsViewModel by viewModels() - private lateinit var gestureDetector: GestureDetector private lateinit var menuProvider: DetailsMenuProvider override fun onCreate(savedInstanceState: Bundle?) { @@ -162,32 +156,7 @@ class DetailsActivity : viewBinding.containerBottomSheet?.let { sheet -> onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet)) } - gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() { - override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - val tv = viewBinding.textViewTitle - TransitionManager.beginDelayedTransition(tv.parent as ViewGroup) - if (tv.maxLines == 5) { - tv.maxLines = 20 // Expand text - } else { - tv.maxLines = 5 // Collapse text - } - return true - } - - override fun onLongPress(e: MotionEvent) { - val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("copied text", viewBinding.textViewTitle.text) - clipboardManager.setPrimaryClip(clip) - Toast.makeText(this@DetailsActivity, "Text copied", Toast.LENGTH_SHORT).show() - } - }) - - viewBinding.textViewTitle.setOnTouchListener { _, motionEvent -> - gestureDetector.onTouchEvent(motionEvent) - true - } - - + TitleExpandListener(viewBinding.textViewTitle).attach() viewModel.details.filterNotNull().observe(this, ::onMangaUpdated) viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/TitleExpandListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/TitleExpandListener.kt new file mode 100644 index 000000000..5d5765100 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/TitleExpandListener.kt @@ -0,0 +1,45 @@ +package org.koitharu.kotatsu.details.ui + +import android.annotation.SuppressLint +import android.transition.TransitionManager +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.view.View.OnTouchListener +import android.view.ViewGroup +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.widgets.SelectableTextView +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled + +@SuppressLint("ClickableViewAccessibility") +class TitleExpandListener( + private val textView: SelectableTextView, +) : GestureDetector.SimpleOnGestureListener(), OnTouchListener { + + private val gestureDetector = GestureDetector(textView.context, this) + private val linesExpanded = textView.resources.getInteger(R.integer.details_description_lines) + private val linesCollapsed = textView.resources.getInteger(R.integer.details_title_lines) + + override fun onTouch(v: View?, event: MotionEvent) = gestureDetector.onTouchEvent(event) + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + if (textView.context.isAnimationsEnabled) { + TransitionManager.beginDelayedTransition(textView.parent as ViewGroup) + } + if (textView.maxLines in 1 until Integer.MAX_VALUE) { + textView.maxLines = Integer.MAX_VALUE + } else { + textView.maxLines = linesCollapsed + } + return true + } + + override fun onLongPress(e: MotionEvent) { + textView.maxLines = Integer.MAX_VALUE + textView.selectAll() + } + + fun attach() { + textView.setOnTouchListener(this) + } +} diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml index 51a35a540..9645983aa 100644 --- a/app/src/main/res/layout/activity_details.xml +++ b/app/src/main/res/layout/activity_details.xml @@ -84,7 +84,7 @@ android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:ellipsize="end" - android:maxLines="5" + android:maxLines="@integer/details_title_lines" android:textAppearance="?attr/textAppearanceHeadlineSmall" android:textIsSelectable="true" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index e405948e9..5c1164864 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -8,6 +8,7 @@ 3 2 4 + 4 450 From 5ab7e586f35532326d38dec25776fb85e7340d97 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 10:18:18 +0300 Subject: [PATCH 09/26] Option to sort manga sources by last used #947 --- .../koitharu/kotatsu/core/backup/JsonDeserializer.kt | 3 +++ .../koitharu/kotatsu/core/backup/JsonSerializer.kt | 3 +++ .../org/koitharu/kotatsu/core/db/MangaDatabase.kt | 4 +++- .../koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt | 11 ++++++++++- .../kotatsu/core/db/entity/MangaSourceEntity.kt | 2 ++ .../kotatsu/core/db/migrations/Migration21To22.kt | 12 ++++++++++++ .../kotatsu/explore/data/MangaSourcesRepository.kt | 10 ++++++++++ .../kotatsu/explore/data/SourcesSortOrder.kt | 1 + .../koitharu/kotatsu/local/ui/LocalListViewModel.kt | 3 +++ .../kotatsu/remotelist/ui/RemoteListViewModel.kt | 7 ++++++- 10 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration21To22.kt diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt index 02229b7de..cc042d7fe 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt @@ -12,6 +12,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault +import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault import org.koitharu.kotatsu.parsers.util.json.getStringOrNull class JsonDeserializer(private val json: JSONObject) { @@ -85,6 +86,8 @@ class JsonDeserializer(private val json: JSONObject) { isEnabled = json.getBoolean("enabled"), sortKey = json.getInt("sort_key"), addedIn = json.getIntOrDefault("added_in", 0), + lastUsedAt = json.getLongOrDefault("used_at", 0L), + isPinned = json.getBooleanOrDefault("pinned", false), ) fun toMap(): Map { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializer.kt index cfe7451d0..28bf270da 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializer.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializer.kt @@ -89,6 +89,9 @@ class JsonSerializer private constructor(private val json: JSONObject) { put("source", e.source) put("enabled", e.isEnabled) put("sort_key", e.sortKey) + put("added_in", e.addedIn) + put("used_at", e.lastUsedAt) + put("pinned", e.isPinned) }, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt index bda4584aa..1bec0c9b8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -34,6 +34,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration18To19 import org.koitharu.kotatsu.core.db.migrations.Migration19To20 import org.koitharu.kotatsu.core.db.migrations.Migration1To2 import org.koitharu.kotatsu.core.db.migrations.Migration20To21 +import org.koitharu.kotatsu.core.db.migrations.Migration21To22 import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration4To5 @@ -59,7 +60,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TracksDao -const val DATABASE_VERSION = 21 +const val DATABASE_VERSION = 22 @Database( entities = [ @@ -120,6 +121,7 @@ fun getDatabaseMigrations(context: Context): Array = arrayOf( Migration18To19(), Migration19To20(), Migration20To21(), + Migration21To22(), ) fun MangaDatabase(context: Context): MangaDatabase = Room diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt index de66fd655..b39df0ab6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt @@ -42,6 +42,12 @@ abstract class MangaSourcesDao { @Query("UPDATE sources SET sort_key = :sortKey WHERE source = :source") abstract suspend fun setSortKey(source: String, sortKey: Int) + @Query("UPDATE sources SET used_at = :value WHERE source = :source") + abstract suspend fun setLastUsed(source: String, value: Long) + + @Query("UPDATE sources SET pinned = :isPinned WHERE source = :source") + abstract suspend fun setPinned(source: String, isPinned: Boolean) + @Insert(onConflict = OnConflictStrategy.IGNORE) @Transaction abstract suspend fun insertIfAbsent(entries: Collection) @@ -53,7 +59,7 @@ abstract class MangaSourcesDao { val orderBy = getOrderBy(order) @Language("RoomSql") - val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy") + val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy") return observeImpl(query) } @@ -73,6 +79,8 @@ abstract class MangaSourcesDao { isEnabled = isEnabled, sortKey = getMaxSortKey() + 1, addedIn = BuildConfig.VERSION_CODE, + lastUsedAt = 0, + isPinned = false, ) upsert(entity) } @@ -91,5 +99,6 @@ abstract class MangaSourcesDao { SourcesSortOrder.ALPHABETIC -> "source ASC" SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC" SourcesSortOrder.MANUAL -> "sort_key ASC" + SourcesSortOrder.LAST_USED -> "used_at DESC" } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaSourceEntity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaSourceEntity.kt index 8c8784a46..849cfcd61 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaSourceEntity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/entity/MangaSourceEntity.kt @@ -15,4 +15,6 @@ data class MangaSourceEntity( @ColumnInfo(name = "enabled") val isEnabled: Boolean, @ColumnInfo(name = "sort_key", index = true) val sortKey: Int, @ColumnInfo(name = "added_in") val addedIn: Int, + @ColumnInfo(name = "used_at") val lastUsedAt: Long, + @ColumnInfo(name = "pinned") val isPinned: Boolean, ) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration21To22.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration21To22.kt new file mode 100644 index 000000000..c45503038 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/migrations/Migration21To22.kt @@ -0,0 +1,12 @@ +package org.koitharu.kotatsu.core.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration21To22 : Migration(21, 22) { + + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE sources ADD COLUMN `used_at` INTEGER NOT NULL DEFAULT 0") + db.execSQL("ALTER TABLE sources ADD COLUMN `pinned` INTEGER NOT NULL DEFAULT 0") + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt index 485dbc1cd..48e1ffff8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt @@ -214,6 +214,8 @@ class MangaSourcesRepository @Inject constructor( isEnabled = false, sortKey = ++maxSortKey, addedIn = BuildConfig.VERSION_CODE, + lastUsedAt = 0, + isPinned = false, ) } dao.insertIfAbsent(entities) @@ -224,6 +226,14 @@ class MangaSourcesRepository @Inject constructor( return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty() } + suspend fun setIsPinned(source: MangaSource, isPinned: Boolean) { + dao.setPinned(source.name, isPinned) + } + + suspend fun trackUsage(source: MangaSource) { + dao.setLastUsed(source.name, System.currentTimeMillis()) + } + private suspend fun setSourcesEnabledImpl(sources: Collection, isEnabled: Boolean) { if (sources.size == 1) { // fast path dao.setEnabled(sources.first().name, isEnabled) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/SourcesSortOrder.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/SourcesSortOrder.kt index 9c42be758..041fbe678 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/SourcesSortOrder.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/SourcesSortOrder.kt @@ -9,4 +9,5 @@ enum class SourcesSortOrder( ALPHABETIC(R.string.by_name), POPULARITY(R.string.popular), MANUAL(R.string.manual), + LAST_USED(R.string.last_used), } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index c38aa1de3..4eed4c626 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.download.ui.worker.DownloadWorker +import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.domain.ExploreRepository import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.list.domain.ListExtraProvider @@ -39,6 +40,7 @@ class LocalListViewModel @Inject constructor( exploreRepository: ExploreRepository, @LocalStorageChanges private val localStorageChanges: SharedFlow, private val localStorageManager: LocalStorageManager, + sourcesRepository: MangaSourcesRepository, ) : RemoteListViewModel( savedStateHandle, mangaRepositoryFactory, @@ -47,6 +49,7 @@ class LocalListViewModel @Inject constructor( listExtraProvider, downloadScheduler, exploreRepository, + sourcesRepository, ), SharedPreferences.OnSharedPreferenceChangeListener { val onMangaRemoved = MutableEventFlow() 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 e1923e1ca..05653b7fc 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 @@ -28,6 +28,7 @@ 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.DownloadWorker +import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.domain.ExploreRepository import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.MangaFilter @@ -45,7 +46,6 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.util.concatUrl import javax.inject.Inject private const val FILTER_MIN_INTERVAL = 250L @@ -59,6 +59,7 @@ open class RemoteListViewModel @Inject constructor( listExtraProvider: ListExtraProvider, downloadScheduler: DownloadWorker.Scheduler, private val exploreRepository: ExploreRepository, + sourcesRepository: MangaSourcesRepository, ) : MangaListViewModel(settings, downloadScheduler), MangaFilter by filter { val source = savedStateHandle.require(RemoteListFragment.ARG_SOURCE) @@ -117,6 +118,10 @@ open class RemoteListViewModel @Inject constructor( }.catch { error -> listError.value = error }.launchIn(viewModelScope) + + launchJob(Dispatchers.Default) { + sourcesRepository.trackUsage(source) + } } override fun onRefresh() { From 9b3ce4d8499b83631798825c58a39f5491d1e5bf Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 11:15:44 +0300 Subject: [PATCH 10/26] Ability to pin manga sources (close #830, close #531) --- .../kotatsu/core/db/dao/MangaSourcesDao.kt | 9 ++-- .../explore/data/MangaSourcesRepository.kt | 33 ++++++++++++-- .../kotatsu/explore/ui/ExploreFragment.kt | 10 +++++ .../kotatsu/explore/ui/ExploreViewModel.kt | 12 ++++++ .../adapter/SourceConfigAdapterDelegates.kt | 43 +++---------------- .../sources/adapter/SourceConfigListener.kt | 2 + .../sources/manage/SourcesListProducer.kt | 3 ++ .../sources/manage/SourcesManageFragment.kt | 4 ++ .../sources/manage/SourcesManageViewModel.kt | 13 +++++- .../sources/model/SourceConfigItem.kt | 1 + app/src/main/res/drawable/ic_pin_small.xml | 12 ++++++ app/src/main/res/drawable/ic_settings.xml | 2 +- app/src/main/res/drawable/ic_shortcut.xml | 12 ++++++ app/src/main/res/drawable/ic_unpin.xml | 12 ++++++ .../main/res/layout/item_source_config.xml | 2 + app/src/main/res/menu/mode_source.xml | 14 +++++- app/src/main/res/menu/popup_source_config.xml | 5 +++ app/src/main/res/values/strings.xml | 6 +++ 18 files changed, 149 insertions(+), 46 deletions(-) create mode 100644 app/src/main/res/drawable/ic_pin_small.xml create mode 100644 app/src/main/res/drawable/ic_shortcut.xml create mode 100644 app/src/main/res/drawable/ic_unpin.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt index b39df0ab6..fcef09da8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt @@ -18,7 +18,7 @@ import org.koitharu.kotatsu.explore.data.SourcesSortOrder @Dao abstract class MangaSourcesDao { - @Query("SELECT * FROM sources ORDER BY sort_key") + @Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key") abstract suspend fun findAll(): List @Query("SELECT source FROM sources WHERE enabled = 1") @@ -27,7 +27,7 @@ abstract class MangaSourcesDao { @Query("SELECT * FROM sources WHERE added_in >= :version") abstract suspend fun findAllFromVersion(version: Int): List - @Query("SELECT * FROM sources ORDER BY sort_key") + @Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key") abstract fun observeAll(): Flow> @Query("SELECT enabled FROM sources WHERE source = :source") @@ -55,6 +55,9 @@ abstract class MangaSourcesDao { @Upsert abstract suspend fun upsert(entry: MangaSourceEntity) + @Query("SELECT * FROM sources WHERE pinned = 1") + abstract suspend fun findAllPinned(): List + fun observeEnabled(order: SourcesSortOrder): Flow> { val orderBy = getOrderBy(order) @@ -67,7 +70,7 @@ abstract class MangaSourcesDao { val orderBy = getOrderBy(order) @Language("RoomSql") - val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy") + val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy") return findAllImpl(query) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt index 48e1ffff8..c3e4b7449 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt @@ -51,6 +51,14 @@ class MangaSourcesRepository @Inject constructor( return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order) } + suspend fun getPinnedSources(): Set { + assimilateNewSources() + val skipNsfw = settings.isNsfwContentDisabled + return dao.findAllPinned().mapNotNullTo(EnumSet.noneOf(MangaSource::class.java)) { + it.source.toMangaSourceOrNull()?.takeUnless { x -> skipNsfw && x.isNsfw() } + } + } + suspend fun getDisabledSources(): Set { assimilateNewSources() val result = EnumSet.copyOf(remoteSources) @@ -226,8 +234,11 @@ class MangaSourcesRepository @Inject constructor( return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty() } - suspend fun setIsPinned(source: MangaSource, isPinned: Boolean) { - dao.setPinned(source.name, isPinned) + suspend fun setIsPinned(sources: Collection, isPinned: Boolean): ReversibleHandle { + setSourcesPinnedImpl(sources, isPinned) + return ReversibleHandle { + setSourcesEnabledImpl(sources, !isPinned) + } } suspend fun trackUsage(source: MangaSource) { @@ -246,6 +257,18 @@ class MangaSourcesRepository @Inject constructor( } } + private suspend fun setSourcesPinnedImpl(sources: Collection, isPinned: Boolean) { + if (sources.size == 1) { // fast path + dao.setPinned(sources.first().name, isPinned) + return + } + db.withTransaction { + for (source in sources) { + dao.setPinned(source.name, isPinned) + } + } + } + private suspend fun getNewSources(): MutableSet { val entities = dao.findAll() val result = EnumSet.copyOf(remoteSources) @@ -260,6 +283,7 @@ class MangaSourcesRepository @Inject constructor( sortOrder: SourcesSortOrder?, ): MutableList { val result = ArrayList(size) + val pinned = EnumSet.noneOf(MangaSource::class.java) for (entity in this) { val source = entity.source.toMangaSourceOrNull() ?: continue if (skipNsfwSources && source.isNsfw()) { @@ -267,10 +291,13 @@ class MangaSourcesRepository @Inject constructor( } if (source in remoteSources) { result.add(source) + if (entity.isPinned) { + pinned.add(source) + } } } if (sortOrder == SourcesSortOrder.ALPHABETIC) { - result.sortBy { it.title } + result.sortWith(compareBy { it in pinned }.thenBy { it.title }) } return result } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt index a9c34b87b..fc334dde9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt @@ -196,6 +196,16 @@ class ExploreFragment : mode.finish() } + R.id.action_pin -> { + viewModel.setSourcesPinned(selectedSources, isPinned = true) + mode.finish() + } + + R.id.action_unpin -> { + viewModel.setSourcesPinned(selectedSources, isPinned = false) + mode.finish() + } + else -> return false } return true diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt index 30eba8c14..fad516b34 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt @@ -108,6 +108,18 @@ class ExploreViewModel @Inject constructor( } } + fun setSourcesPinned(sources: Set, isPinned: Boolean) { + launchJob(Dispatchers.Default) { + sourcesRepository.setIsPinned(sources, isPinned) + val message = if (sources.size == 1) { + if (isPinned) R.string.source_pinned else R.string.source_unpinned + } else { + if (isPinned) R.string.sources_pinned else R.string.sources_unpinned + } + onActionDone.call(ReversibleAction(message, null)) + } + } + fun respondSuggestionTip(isAccepted: Boolean) { settings.isSuggestionsEnabled = isAccepted settings.closeTip(TIP_SUGGESTIONS) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt index e8c41887d..3470c28de 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigAdapterDelegates.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.sources.adapter import android.view.View import androidx.appcompat.widget.PopupMenu +import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.view.isGone import androidx.core.view.isVisible @@ -16,49 +17,14 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri import org.koitharu.kotatsu.core.ui.image.FaviconDrawable import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener 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.newImageRequest import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding -import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding import org.koitharu.kotatsu.databinding.ItemTipBinding import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem -fun sourceConfigItemCheckableDelegate( - listener: SourceConfigListener, - coil: ImageLoader, - lifecycleOwner: LifecycleOwner, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> - ItemSourceConfigCheckableBinding.inflate( - layoutInflater, - parent, - false, - ) - }, -) { - - binding.switchToggle.setOnCheckedChangeListener { _, isChecked -> - listener.onItemEnabledChanged(item, isChecked) - } - - bind { - binding.textViewTitle.text = item.source.getTitle(context) - binding.switchToggle.isChecked = item.isEnabled - binding.switchToggle.isEnabled = item.isAvailable - binding.textViewDescription.text = item.source.getSummary(context) - val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) - binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { - crossfade(context) - error(fallbackIcon) - placeholder(fallbackIcon) - fallback(fallbackIcon) - source(item.source) - enqueueWith(coil) - } - } -} - fun sourceConfigItemDelegate2( listener: SourceConfigListener, coil: ImageLoader, @@ -73,6 +39,7 @@ fun sourceConfigItemDelegate2( }, ) { + val iconPinned = ContextCompat.getDrawable(context, R.drawable.ic_pin_small) val eventListener = View.OnClickListener { v -> when (v.id) { R.id.imageView_add -> listener.onItemEnabledChanged(item, true) @@ -89,6 +56,7 @@ fun sourceConfigItemDelegate2( binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable binding.imageViewRemove.isVisible = item.isEnabled binding.imageViewMenu.isVisible = item.isEnabled + binding.textViewTitle.drawableStart = if (item.isPinned) iconPinned else null binding.textViewDescription.text = item.source.getSummary(context) val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { @@ -132,12 +100,15 @@ private fun showSourceMenu( menu.inflate(R.menu.popup_source_config) menu.menu.findItem(R.id.action_shortcut) ?.isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(anchor.context) + menu.menu.findItem(R.id.action_pin)?.isVisible = item.isEnabled + menu.menu.findItem(R.id.action_pin)?.isChecked = item.isPinned menu.menu.findItem(R.id.action_lift)?.isVisible = item.isDraggable menu.setOnMenuItemClickListener { when (it.itemId) { R.id.action_settings -> listener.onItemSettingsClick(item) R.id.action_lift -> listener.onItemLiftClick(item) R.id.action_shortcut -> listener.onItemShortcutClick(item) + R.id.action_pin -> listener.onItemPinClick(item) } true } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt index 2a09d6516..82fa5212f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/adapter/SourceConfigListener.kt @@ -11,5 +11,7 @@ interface SourceConfigListener : OnTipCloseListener { fun onItemShortcutClick(item: SourceConfigItem.SourceItem) + fun onItemPinClick(item: SourceConfigItem.SourceItem) + fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) } 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 c519807b4..f088acd2a 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 @@ -61,6 +61,7 @@ class SourcesListProducer @Inject constructor( private suspend fun buildList(): List { val enabledSources = repository.getEnabledSources() + val pinned = repository.getPinnedSources() val isNsfwDisabled = settings.isNsfwContentDisabled val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER) @@ -75,6 +76,7 @@ class SourcesListProducer @Inject constructor( isEnabled = it in enabledSet, isDraggable = false, isAvailable = !isNsfwDisabled || !it.isNsfw(), + isPinned = it in pinned, ) }.ifEmpty { listOf(SourceConfigItem.EmptySearchResult) @@ -95,6 +97,7 @@ class SourcesListProducer @Inject constructor( isEnabled = true, isDraggable = isReorderAvailable, isAvailable = false, + isPinned = it 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 f5a7e051a..b970465cc 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 @@ -120,6 +120,10 @@ class SourcesManageFragment : } } + override fun onItemPinClick(item: SourceConfigItem.SourceItem) { + viewModel.setPinned(item.source, !item.isPinned) + } + override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) { viewModel.setEnabled(item.source, isEnabled) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageViewModel.kt index 8175142e4..d39187b00 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/manage/SourcesManageViewModel.kt @@ -58,8 +58,9 @@ class SourcesManageViewModel @Inject constructor( fun canReorder(oldPos: Int, newPos: Int): Boolean { val snapshot = content.value - if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false - return (snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled == true + val oldPosItem = snapshot.getOrNull(oldPos) as? SourceConfigItem.SourceItem ?: return false + val newPosItem = snapshot.getOrNull(newPos) as? SourceConfigItem.SourceItem ?: return false + return oldPosItem.isEnabled && newPosItem.isEnabled && oldPosItem.isPinned == newPosItem.isPinned } fun setEnabled(source: MangaSource, isEnabled: Boolean) { @@ -71,6 +72,14 @@ class SourcesManageViewModel @Inject constructor( } } + fun setPinned(source: MangaSource, isPinned: Boolean) { + launchJob(Dispatchers.Default) { + val rollback = repository.setIsPinned(setOf(source), isPinned) + val message = if (isPinned) R.string.source_pinned else R.string.source_unpinned + onActionDone.call(ReversibleAction(message, rollback)) + } + } + fun bringToTop(source: MangaSource) { val snapshot = content.value launchJob(Dispatchers.Default) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt index a68b6581b..b5ef3263b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/model/SourceConfigItem.kt @@ -13,6 +13,7 @@ sealed interface SourceConfigItem : ListModel { val isEnabled: Boolean, val isDraggable: Boolean, val isAvailable: Boolean, + val isPinned: Boolean, ) : SourceConfigItem { val isNsfw: Boolean diff --git a/app/src/main/res/drawable/ic_pin_small.xml b/app/src/main/res/drawable/ic_pin_small.xml new file mode 100644 index 000000000..383c74b9f --- /dev/null +++ b/app/src/main/res/drawable/ic_pin_small.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index 2eef40149..e6e61e610 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -6,6 +6,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_shortcut.xml b/app/src/main/res/drawable/ic_shortcut.xml new file mode 100644 index 000000000..fe64806f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_shortcut.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_unpin.xml b/app/src/main/res/drawable/ic_unpin.xml new file mode 100644 index 000000000..a3c89da86 --- /dev/null +++ b/app/src/main/res/drawable/ic_unpin.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/item_source_config.xml b/app/src/main/res/layout/item_source_config.xml index b1c524539..4d958f260 100644 --- a/app/src/main/res/layout/item_source_config.xml +++ b/app/src/main/res/layout/item_source_config.xml @@ -35,9 +35,11 @@ android:id="@+id/textView_title" android:layout_width="match_parent" android:layout_height="wrap_content" + android:drawablePadding="4dp" android:ellipsize="end" android:singleLine="true" android:textAppearance="?attr/textAppearanceTitleSmall" + tools:drawableStart="@drawable/ic_pin_small" tools:text="@tools:sample/lorem[15]" /> + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c97bb7fef..1300322d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -657,4 +657,10 @@ Preferred image server %1$s: %2$s Crop pages + Pin + Unpin + Source pinned + Source unpinned + Sources unpinned + Sources pinned From dcee5542c5e393f5a36038ec03e638c782e010d3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 11:38:35 +0300 Subject: [PATCH 11/26] Add recent sources to search suggestions --- .../kotatsu/core/db/dao/MangaSourcesDao.kt | 3 ++ .../core/prefs/SearchSuggestionType.kt | 1 + .../explore/data/MangaSourcesRepository.kt | 9 +++- .../search/domain/MangaSearchRepository.kt | 2 + .../suggestion/SearchSuggestionViewModel.kt | 8 +++ .../adapter/SearchSuggestionAdapter.kt | 1 + .../adapter/SearchSuggestionSourceTipAD.kt | 43 +++++++++++++++ .../suggestion/model/SearchSuggestionItem.kt | 13 +++++ .../item_search_suggestion_source_tip.xml | 52 +++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 10 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceTipAD.kt create mode 100644 app/src/main/res/layout/item_search_suggestion_source_tip.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt index fcef09da8..ab1b77150 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/MangaSourcesDao.kt @@ -27,6 +27,9 @@ abstract class MangaSourcesDao { @Query("SELECT * FROM sources WHERE added_in >= :version") abstract suspend fun findAllFromVersion(version: Int): List + @Query("SELECT * FROM sources ORDER BY used_at DESC LIMIT :limit") + abstract suspend fun findLastUsed(limit: Int): List + @Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key") abstract fun observeAll(): Flow> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SearchSuggestionType.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SearchSuggestionType.kt index ab3cb3c49..f25ef6f99 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SearchSuggestionType.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/SearchSuggestionType.kt @@ -12,5 +12,6 @@ enum class SearchSuggestionType( QUERIES_SUGGEST(R.string.suggested_queries), MANGA(R.string.content_type_manga), SOURCES(R.string.remote_sources), + RECENT_SOURCES(R.string.recent_sources), AUTHORS(R.string.authors), } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt index c3e4b7449..1021e6d70 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/data/MangaSourcesRepository.kt @@ -59,6 +59,11 @@ class MangaSourcesRepository @Inject constructor( } } + suspend fun getTopSources(limit: Int): List { + assimilateNewSources() + return dao.findLastUsed(limit).toSources(settings.isNsfwContentDisabled, null) + } + suspend fun getDisabledSources(): Set { assimilateNewSources() val result = EnumSet.copyOf(remoteSources) @@ -242,7 +247,9 @@ class MangaSourcesRepository @Inject constructor( } suspend fun trackUsage(source: MangaSource) { - dao.setLastUsed(source.name, System.currentTimeMillis()) + if (!settings.isIncognitoModeEnabled && !(settings.isHistoryExcludeNsfw && source.isNsfw())) { + dao.setLastUsed(source.name, System.currentTimeMillis()) + } } private suspend fun setSourcesEnabledImpl(sources: Collection, isEnabled: Boolean) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 3adc10d2a..e3df45981 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -125,6 +125,8 @@ class MangaSearchRepository @Inject constructor( return db.getTagsDao().findRareTags(source.name, limit).toMangaTagsList() } + suspend fun getSourcesSuggestion(limit: Int): List = sourcesRepository.getTopSources(limit) + fun getSourcesSuggestion(query: String, limit: Int): List { if (query.length < 3) { return emptyList() 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 315cb8ec8..36f952dc1 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 @@ -37,6 +37,7 @@ private const val MAX_HINTS_ITEMS = 3 private const val MAX_AUTHORS_ITEMS = 2 private const val MAX_TAGS_ITEMS = 8 private const val MAX_SOURCES_ITEMS = 6 +private const val MAX_SOURCES_TIPS_ITEMS = 2 @HiltViewModel class SearchSuggestionViewModel @Inject constructor( @@ -149,12 +150,18 @@ class SearchSuggestionViewModel @Inject constructor( } else { null } + val sourcesTipsDeferred = if (searchQuery.isEmpty() && SearchSuggestionType.RECENT_SOURCES in types) { + async { repository.getSourcesSuggestion(MAX_SOURCES_TIPS_ITEMS) } + } else { + null + } val tags = tagsDeferred?.await() val mangaList = mangaDeferred?.await() val queries = queriesDeferred?.await() val hints = hintsDeferred?.await() val authors = authorsDeferred?.await() + val sourcesTips = sourcesTipsDeferred?.await() buildList(queries.sizeOrZero() + sources.sizeOrZero() + authors.sizeOrZero() + hints.sizeOrZero() + 2) { if (!tags.isNullOrEmpty()) { @@ -167,6 +174,7 @@ class SearchSuggestionViewModel @Inject constructor( queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) } authors?.mapTo(this) { SearchSuggestionItem.Author(it) } hints?.mapTo(this) { SearchSuggestionItem.Hint(it) } + sourcesTips?.mapTo(this) { SearchSuggestionItem.SourceTip(it) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt index 3152927f0..9b923e571 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionAdapter.kt @@ -18,6 +18,7 @@ class SearchSuggestionAdapter( delegatesManager .addDelegate(SEARCH_SUGGESTION_ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener)) .addDelegate(searchSuggestionSourceAD(coil, lifecycleOwner, listener)) + .addDelegate(searchSuggestionSourceTipAD(coil, lifecycleOwner, listener)) .addDelegate(searchSuggestionTagsAD(listener)) .addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener)) .addDelegate(searchSuggestionQueryHintAD(listener)) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceTipAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceTipAD.kt new file mode 100644 index 000000000..55a71a06d --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/adapter/SearchSuggestionSourceTipAD.kt @@ -0,0 +1,43 @@ +package org.koitharu.kotatsu.search.ui.suggestion.adapter + +import androidx.lifecycle.LifecycleOwner +import coil.ImageLoader +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.getSummary +import org.koitharu.kotatsu.core.model.getTitle +import org.koitharu.kotatsu.core.parser.favicon.faviconUri +import org.koitharu.kotatsu.core.ui.image.FaviconDrawable +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.newImageRequest +import org.koitharu.kotatsu.core.util.ext.source +import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceTipBinding +import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener +import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem + +fun searchSuggestionSourceTipAD( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + listener: SearchSuggestionListener, +) = + adapterDelegateViewBinding( + { inflater, parent -> ItemSearchSuggestionSourceTipBinding.inflate(inflater, parent, false) }, + ) { + + binding.root.setOnClickListener { + listener.onSourceClick(item.source) + } + + bind { + binding.textViewTitle.text = item.source.getTitle(context) + binding.textViewSubtitle.text = item.source.getSummary(context) + val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) + binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { + fallback(fallbackIcon) + placeholder(fallbackIcon) + error(fallbackIcon) + source(item.source) + enqueueWith(coil) + } + } + } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt index 5493fb920..f1c997956 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/suggestion/model/SearchSuggestionItem.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.search.ui.suggestion.model +import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.model.ListModel @@ -69,6 +70,18 @@ sealed interface SearchSuggestionItem : ListModel { } } + data class SourceTip( + val source: MangaSource, + ) : SearchSuggestionItem { + + val isNsfw: Boolean + get() = source.isNsfw() + + override fun areItemsTheSame(other: ListModel): Boolean { + return other is Source && other.source == source + } + } + data class Tags( val tags: List, ) : SearchSuggestionItem { diff --git a/app/src/main/res/layout/item_search_suggestion_source_tip.xml b/app/src/main/res/layout/item_search_suggestion_source_tip.xml new file mode 100644 index 000000000..f3a9da423 --- /dev/null +++ b/app/src/main/res/layout/item_search_suggestion_source_tip.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1300322d5..08495a58b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -663,4 +663,5 @@ Source unpinned Sources unpinned Sources pinned + Recent sources From ee85ef50f4eee5c141b2a11c4a999c3b58d11db3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 11:50:08 +0300 Subject: [PATCH 12/26] Use image proxy for downloading #897 --- .../ui/pager/pages/MangaPageFetcher.kt | 2 +- .../download/ui/worker/DownloadWorker.kt | 42 +++++++++---------- .../reader/domain/DetectReaderModeUseCase.kt | 2 +- .../kotatsu/reader/domain/PageLoader.kt | 6 +-- 4 files changed, 25 insertions(+), 27 deletions(-) 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 3bfe70c52..e954b630e 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 @@ -92,7 +92,7 @@ class MangaPageFetcher( } else -> { - val request = PageLoader.createPageRequest(page, pageUrl) + val request = PageLoader.createPageRequest(pageUrl, page.source) imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response -> if (!response.isSuccessful) { throw HttpException(response) 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 fb0fea709..2c1ae89ac 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 @@ -35,16 +35,16 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import okhttp3.OkHttpClient -import okhttp3.Request import okhttp3.internal.closeQuietly import okio.IOException import okio.buffer import okio.sink +import okio.use import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions import org.koitharu.kotatsu.core.model.ids -import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.MangaHttpClient +import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings @@ -55,6 +55,7 @@ import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.deleteWork import org.koitharu.kotatsu.core.util.ext.deleteWorks +import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getWorkInputData import org.koitharu.kotatsu.core.util.ext.getWorkSpec @@ -73,9 +74,9 @@ import org.koitharu.kotatsu.local.domain.model.LocalManga 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.await import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.reader.domain.PageLoader import java.io.File import java.util.UUID import java.util.concurrent.TimeUnit @@ -93,6 +94,7 @@ class DownloadWorker @AssistedInject constructor( private val mangaRepositoryFactory: MangaRepository.Factory, private val settings: AppSettings, @LocalStorageChanges private val localStorageChanges: MutableSharedFlow, + private val imageProxyInterceptor: ImageProxyInterceptor, notificationFactoryFactory: DownloadNotificationFactory.Factory, ) : CoroutineWorker(appContext, params) { @@ -327,28 +329,24 @@ class DownloadWorker @AssistedInject constructor( destination: File, source: MangaSource, ): File { - val request = Request.Builder() - .url(url) - .tag(MangaSource::class.java, source) - .header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8") - .cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE) - .get() - .build() + val request = PageLoader.createPageRequest(url, source) slowdownDispatcher.delay(source) - val call = okHttp.newCall(request) - val file = File(destination, UUID.randomUUID().toString() + ".tmp") - try { - val response = call.clone().await() - checkNotNull(response.body).use { body -> - file.sink(append = false).buffer().use { - it.writeAllCancellable(body.source()) + return imageProxyInterceptor.interceptPageRequest(request, okHttp) + .ensureSuccess() + .use { response -> + val file = File(destination, UUID.randomUUID().toString() + ".tmp") + try { + checkNotNull(response.body).use { body -> + file.sink(append = false).buffer().use { + it.writeAllCancellable(body.source()) + } + } + } catch (e: CancellationException) { + file.delete() + throw e } + file } - } catch (e: CancellationException) { - file.delete() - throw e - } - return file } private suspend fun publishState(state: DownloadState) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt index 1be842980..7f8989ece 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt @@ -71,7 +71,7 @@ class DetectReaderModeUseCase @Inject constructor( } } } else { - val request = PageLoader.createPageRequest(page, url) + val request = PageLoader.createPageRequest(url, page.source) imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { runInterruptible(Dispatchers.IO) { getBitmapSize(it.body?.byteStream()) 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 08d1e537b..68d4adb55 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 @@ -230,7 +230,7 @@ class PageLoader @Inject constructor( uri.isFileUri() -> uri else -> { - val request = createPageRequest(page, pageUrl) + 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 { @@ -265,12 +265,12 @@ class PageLoader @Inject constructor( private const val PREFETCH_LIMIT_DEFAULT = 6 private const val PREFETCH_MIN_RAM_MB = 80L - fun createPageRequest(page: MangaPage, pageUrl: String) = Request.Builder() + fun createPageRequest(pageUrl: String, mangaSource: MangaSource) = Request.Builder() .url(pageUrl) .get() .header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8") .cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE) - .tag(MangaSource::class.java, page.source) + .tag(MangaSource::class.java, mangaSource) .build() } } From 484914b2dca717cc6bd24e088d0e59212f618613 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 11:54:24 +0300 Subject: [PATCH 13/26] Fix detection webtoon mode for local manga --- .../reader/domain/DetectReaderModeUseCase.kt | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt index 7f8989ece..21c8cea06 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/DetectReaderModeUseCase.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.domain import android.graphics.BitmapFactory import android.net.Uri import android.util.Size +import androidx.core.net.toFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import okhttp3.OkHttpClient @@ -14,6 +15,8 @@ import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.local.data.isFileUri +import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -61,20 +64,28 @@ class DetectReaderModeUseCase @Inject constructor( val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" } val url = repository.getPageUrl(page) val uri = Uri.parse(url) - // TODO file support - val size = if (uri.scheme == "cbz") { - runInterruptible(Dispatchers.IO) { + + val size = when { + uri.isZipUri() -> runInterruptible(Dispatchers.IO) { val zip = ZipFile(uri.schemeSpecificPart) val entry = zip.getEntry(uri.fragment) zip.getInputStream(entry).use { getBitmapSize(it) } } - } else { - val request = PageLoader.createPageRequest(url, page.source) - imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { - runInterruptible(Dispatchers.IO) { - getBitmapSize(it.body?.byteStream()) + + uri.isFileUri() -> runInterruptible(Dispatchers.IO) { + uri.toFile().inputStream().use { + getBitmapSize(it) + } + } + + else -> { + val request = PageLoader.createPageRequest(url, page.source) + imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { + runInterruptible(Dispatchers.IO) { + getBitmapSize(it.body?.byteStream()) + } } } } From 675ef0e629335c0537a9f46be2bd4bbae64d2412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D0=B0=D1=80=20=D0=A0=D0=B0=D0=B7=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Sun, 7 Jul 2024 11:05:06 +0200 Subject: [PATCH 14/26] Translated using Weblate (Ukrainian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (648 of 648 strings) Translated using Weblate (Russian) Currently translated at 100.0% (648 of 648 strings) Translated using Weblate (Belarusian) Currently translated at 100.0% (648 of 648 strings) Co-authored-by: Макар Разин Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/ Translation: Kotatsu/Strings --- app/src/main/res/values-be/strings.xml | 74 +++++++++++++------------- app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-uk/strings.xml | 2 + 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 93fbfac62..a9a5121bc 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -22,7 +22,7 @@ Гісторыя пустая Чытаць Дадайце цікавую для вас мангу ў абранае, каб не страціць яе - Дадаць ў абраныя + Дадаць у абраныя Стварыць катэгорыю Дадаць Захаваць @@ -45,7 +45,7 @@ Тэма Светлая Цёмная - Як ў сістэме + Як у сістэме Старонкi Ачысціць Выдаліць @@ -78,7 +78,7 @@ Знешняе сховішча Дамен Даступна абнаўленне праграмы - Адкрыць ў браўзеры + Адкрыць у браўзеры Паведамленні Ўключана %1$d з %2$d Новыя раздзелы @@ -125,7 +125,7 @@ Справа налева Стварыць катэгорыю Маштабаванне - Ўмясціць ў экран + Умясціць у экран Падагнаць па вышыні Падагнаць па шырыні Зыходны памер @@ -137,7 +137,7 @@ Данныя адноўлены Падрыхтоўка… Файл не знойдзены - Ўсе данныя паспяхова адноўлены + Усе данныя паспяхова адноўлены Данныя адноўлены, але ўзніклі некаторыя памылкі Вы можаце стварыць рэзервовую копію абранага і гісторыі і потым аднавіць іх Толькі што @@ -153,7 +153,7 @@ Ачысціць кукi Ўсе кукi выдалены Ачысціць стужку - Ўся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя? + Уся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя? Праверка новых глаў Ў адваротным парадку Ўвайсці @@ -163,14 +163,14 @@ Калі ласка, увядзіце пароль, які спатрэбіцца пры запуску праграмы Пацвердзіць Пароль павінен змяшчаць не менш за 4 сімвалы - Вы сапраўды хочаце выдаліць ўсе апошнія пошукавыя запыты? + Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты? Падрабязна Некаторыя вытворцы могуць змяняць паводзіны сістэмы, што можа парушаць выкананне фонавых задач. Рэзервовая копія паспяхова захавана Вітаю Захавайце што-небудзь з інтэрнэт-каталога або імпартуйце гэта з файла. Спачатку захавайце што-небудзь - Знайдзіце, што пачытаць, ў раздзеле «Агляд» + Знайдзіце, што пачытаць, у раздзеле «Агляд» Тут будзе паказана манга, якую вы чытаеце Паспрабуйце перафармуляваць запыт. Неяк тут пуста… @@ -178,7 +178,7 @@ Ў чарзе Дапамагчы з перакладам праграмы Пераклад - Вы выйдзеце з усіх крыніц, ў якіх вы аўтарызаваны + Вы выйдзеце з усіх крыніц, у якіх вы аўтарызаваны Аўтарызацыя на %s не падтрымліваецца Аўтарызацыя выканана Жанры @@ -196,7 +196,7 @@ Ўключаны Ня прапаноўваць NSFW мангу Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы - Ўсе даныя аналізуюцца толькі лакальна на гэтай прыладзе і нікуды не адпраўляюцца. + Усе даныя аналізуюцца толькі лакальна на гэтай прыладзе і нікуды не адпраўляюцца. Прапануеце мангу, заснаваную на вашых перавагах Ўключыць прапановы Прапанова @@ -211,10 +211,10 @@ Розныя мовы Знайсці главу %1$s%% - Ў гэтай манге няма раздзелаў + У гэтай манге няма раздзелаў Схаваць Знешні выгляд - Выключыць ўсё + Адключыць усе Выкарыстоўваць адбітак пальца, калі даступна Манга з абраных Манга, якую вы нядаўна чыталі @@ -235,7 +235,7 @@ Даступныя новыя крыніцы мангі Запавольванне спампоўкі Выключыць жанры - Укажыце жанры, якія вы не хочаце бачыць ў рэкамендацыях + Укажыце жанры, якія вы не хочаце бачыць у рэкамендацыях Выдаліць выбраныя элементы з прылады назаўжды\? Выдаленне завершана Дапамагае пазбегнуць блакіроўкі па IP-адрасе @@ -263,20 +263,20 @@ Выйсці Паказваць працэнт прачытанага ў гісторыі і абраных Выдаленне даных - Паказаць ўсе + Паказаць усе Манга, пазначаная як NSFW, не будзе дададзеная ў гісторыю і ваш прагрэс не будзе захаваны - Можа дапамагчы з некаторымі праблемам. Ўсе аўтарызацыі будуць ануляваныя + Можа дапамагчы з некаторымі праблемам. Усе аўтарызацыі будуць ануляваныя Змесціва не знойдзена ці выдалена Перачытваю Выберыце дыяпазон Тут нічога няма Службы Канадзе - Ачысціць ўсю гісторыю + Ачысціць усю гісторыю Гісторыя ачышчана Рэжым інкогніта Вы ўпэўнены, што хочаце выдаліць выбраныя абраныя катэгорыі? -\nЎся манга ў ім будзе страчана, і гэта нельга будзе адрабіць. +\nУся манга ў ім будзе страчана, і гэта нельга будзе адрабіць. Вы можаце стварыць закладку падчас чытання мангі Захаваная манга Мамімі @@ -288,7 +288,7 @@ Пазначыць як бягучы На прыладзе не засталося месца Сетка недаступная - Каб чытаць мангу онлайн, ўключыце Wi-Fi або мабільную сетку + Каб чытаць мангу онлайн, уключыце Wi-Fi або мабільную сетку Маштабаванне ў рэжыме манхвы Дынамічны Каляровая гама @@ -304,7 +304,7 @@ Паказаць апошнія ярлыкі мангі Зрабіце нядаўнюю мангу даступнай, доўга націскаючы на значок праграмы Націск на правы край або націск правай клавішы заўсёды перамыкае на наступную старонку. - Эрганамічны упраўленне чытаннем + Эрганамічнае кіраванне рэжымам чытання Карэкцыя колеру Яркасць Кантраст @@ -321,7 +321,7 @@ Стужка Паказаць паўзунок пераключэння старонак Крыніца адключана - Паказаць ў выглядзе сеткі + Паказаць у выглядзе сеткі Міку Аска Міён @@ -330,7 +330,7 @@ Няма раздзелаў Аўтаматычная пракрутка Разд. %1$d/%2$d Стар. %3$d/%4$d - Паказаць інфармацыйную панэль ў праграме чытання + Паказаць інфармацыйную панэль у праграме чытання Архіў коміксаў Тэчка з малюнкамі Імпарт мангі @@ -339,7 +339,7 @@ Закладак пакуль няма Закладкі выдалены Няма крыніц мангі - Каб чытаць мангу онлайн, ўключыце крыніцы мангі + Каб чытаць мангу онлайн, уключыце крыніцы мангі Выпадковы Змяніць парадак Пуста @@ -373,7 +373,7 @@ Ігнараваць памылкі SSL Аднавіць Прыпынена - Адмяніць ўсё + Адмяніць усе Спампаваць толькі праз Wi-Fi Вы можаце выкарыстоўваць уласны сервер сінхранізацыі або сервер па змаўчанні. Не змяняйце гэта, калі вы не ўпэўненыя, што робіце. Паўза @@ -382,7 +382,7 @@ Часам паказваць апавяшчэнні з прапанаванай мангай Больш Ўключыць - Ўсе актыўныя спампоўкі будуць адменены, часткова спампаваныя даныя будуць страчаны + Усе актыўныя спампоўкі будуць адменены, часткова спампаваныя даныя будуць страчаны Хочаце атрымліваць персаналізаваныя прапановы мангі\? Прапанова: %s Не, дзякуй @@ -407,13 +407,13 @@ Выкарыстоўвайце службу wsrv.nl, каб паменшыць выкарыстанне трафіку і паскорыць загрузку малюнкаў, калі гэта магчыма Пароль Інвертаваць колеры - Паказаць нумары старонак ў ніжнім куце + Паказаць нумары старонак у ніжнім куце Сетка Дадзеныя і канфідэнцыяльнасць Дазволіць жэсты маштабавання ў рэжыме манхвы Аднавіць раней створаную рэзервовую копію Паказаць бягучы час і ход чытання ў верхняй частцы экрана - Выдаліць файлы cookie толькі для вызначанага дамена. Ў большасці выпадкаў гэта робіць аўтарызацыю несапраўднай + Выдаліць файлы cookie толькі для вызначанага дамена. У большасці выпадкаў гэта робіць аўтарызацыю несапраўднай Манга цалкам Лакальныя каталогі мангі Ўсе раздзелы з перакладам %s @@ -459,10 +459,10 @@ Раздзелы галоўнага экрана Ўверх Павялічыць - Ці паказваць кнопкі кіравання маштабаваннем ў правым ніжнім куце + Ці паказваць кнопкі кіравання маштабаваннем у правым ніжнім куце Паказаць кнопкі маштабавання Зменшыць - Трымаць экран ўключаным + Трымаць экран уключаным Ня выключаць экран падчас чытання мангі Кінута Катэгорыі @@ -480,7 +480,7 @@ Раз на тыдзень Перыядычнае рэзервовае капіраванне Два разы на месяц - Адзін раз ў месяц + Адзін раз у месяц Апошняе паспяховае рэзервовае капіраванне: %s Вывадны каталог рэзервовых копій x%.1f @@ -496,7 +496,7 @@ Ўручную Крыніца ўключана Адключыць крыніцы NSFW і схавайць мангу для дарослых са спісу, калі гэта магчыма - Ў гэтым раздзеле няма даступных крыніц, ці ўсе яны маглі быць ўжо дададзены. + У гэтым раздзеле няма даступных крыніц, ці ўсе яны маглі быць ужо дададзены. \nСачыце за абнаўленнямі Даступна: %1$d Іншае @@ -517,18 +517,18 @@ Прапусціць Гэтыя налады могуць прымяняцца глабальна або толькі да бягучай мангі. Пры глабальным прымяненні індывідуальныя налады не будуць перавызначаны. Адценні шэрага - Можа дапамагчы з пачаткам загрузкі, калі у вас ўзнікаюць з ёй праблемы + Можа дапамагчы з пачаткам загрузкі, калі у вас узнікаюць з ёй праблемы Выберыце, якія крыніцы змесціва вы хочаце ўключыць. Гэта таксама можна наладзіць пазней ў наладах Аднавіць Дата стварэння рэзервовай копіі: %s - Ўвайдзіце, каб сінхранізаваць ўліковы запіс + Увайсці ў акаўнт сінхранізацыі Імя (зваротнае) Рэйтынг кантэнту Выключыць жанры Бяспечны З падказкамі Дарослы - Ўкладка па змаўчанні + Укладка па змаўчанні Чакаецца Том %d Невядомы том @@ -554,7 +554,7 @@ \nУвага: бягучы ход чытання будзе страчаны. Налада дзеянняў для сэнсарных абласцей экрана %1$s %2$s - Каб працягнуць, ўвядзіце адрас электроннай пошты і пароль + Каб працягнуць, увядзіце адрас электроннай пошты і пароль Скінуць налады да значэнняў па змаўчанні? Гэта дзеянне нельга адмяніць. Выкарыстоўвайце двухстаронкавы макет у альбомнай арыентацыі (бэта) Аддаленне ў рэжыме манхвы @@ -589,7 +589,7 @@ Адзін файл CBZ Некалькі файлаў CBZ Альтэрнатывы - Манга «%1$s» з «%2$s» будзе заменена на «%3$s» з «%4$s» ў вашай гісторыі і ў абраных (калі ёсць) + Манга «%1$s» з «%2$s» будзе заменена на «%3$s» з «%4$s» у вашай гісторыі і ў абраных (калі ёсць) Перанос завершаны Перанос мангі Перанесці @@ -609,7 +609,7 @@ Ўключыць крыніцу Гэтая крыніца мангі не падтрымліваецца Паказаць мініяцюры старонак - Ўключыце ўкладку «Старонкі» на экране звестак + Уключыце ўкладку «Старонкі» на экране звестак Ніякія дадзеныя не былі атрыманы з сервера Абярыце правільны файл рэзервовай копіі Kotatsu Апошні раз выкарыстоўваўся @@ -642,4 +642,6 @@ Адкл. Крыніцы адключаны Новае + Усе мовы + Блакіраваць у рэжыме інкогніта \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b09a90c14..e4a8231df 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -642,4 +642,6 @@ Откл. Источники отключены Новое + Все языки + Блокировать в режиме инкогнито \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2a7ebd848..5de454ea9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -642,4 +642,6 @@ Вимкнути Джерела вимкнено Нове + Всі мови + Блокувати в режимі інкогніто \ No newline at end of file From 6e9c934912f19a9269504170a84f76b0d0af72ea Mon Sep 17 00:00:00 2001 From: Biagio Ricci Date: Sun, 7 Jul 2024 11:05:06 +0200 Subject: [PATCH 15/26] Translated using Weblate (Italian) Currently translated at 94.5% (613 of 648 strings) Co-authored-by: Biagio Ricci Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/ Translation: Kotatsu/Strings --- app/src/main/res/values-it/strings.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 432cbd0f3..685cc35e8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -593,4 +593,20 @@ Più frequente Frequenza di controllo %1$s: %2$d + Nuovi + Automatico + Tutte le lingue + Cancella capitoli letti + Migrazione manga + Migrazione completata + Nessun capitolo è stato cancellato + Giorno + Tre mesi + Non ci sono statistiche per il periodo selezionato + Chiedi per la cartella di destinazione ogni volta + Salva pagine + Formato download preferito + File CBZ singolo + Molti file CBZ + Lettura statistiche \ No newline at end of file From 28a4a3849c0ea8ee0ef717def567f008e5c577f9 Mon Sep 17 00:00:00 2001 From: Zhafran Aziz Date: Sun, 7 Jul 2024 11:05:06 +0200 Subject: [PATCH 16/26] Translated using Weblate (Indonesian) Currently translated at 99.2% (643 of 648 strings) Co-authored-by: Zhafran Aziz Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/ Translation: Kotatsu/Strings --- app/src/main/res/values-in/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 71287cdb7..c18822f71 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -641,4 +641,6 @@ Anda diblokir oleh server. Coba gunakan koneksi jaringan yang berbeda (VPN, Proxy, dll.) Lebih jarang Jangan sembunyikan bilah navigasi dan tampilan pencarian saat menggulir + Dihapus, dibersihkan + Pertanyaan yang disarankan \ No newline at end of file From c2e7325876275e6e930bbed96a09013e2e1fab66 Mon Sep 17 00:00:00 2001 From: Abay Emes Date: Sun, 7 Jul 2024 11:05:06 +0200 Subject: [PATCH 17/26] Translated using Weblate (Kazakh) Currently translated at 84.4% (547 of 648 strings) Co-authored-by: Abay Emes Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/ Translation: Kotatsu/Strings --- app/src/main/res/values-kk/strings.xml | 40 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 39d6be40a..f88a5102d 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -17,8 +17,8 @@ Тарау Егжей-тегжейлі тізім Тізім түрі - Маңга қайнары - Жүктеу… + Маңга дереккөзі + Жүктеліп жатыр… Есептеу… Таңдаулы Желі қатесі @@ -34,7 +34,7 @@ Таңбаша жасау… %s бөлісу Іздеу - Жүктеу… + Жүктеліп жатыр… Үдерісте… Жүктелді Атауы @@ -107,7 +107,7 @@ Әзірге тарих жоқ Маңга іздеу Таңдаулыға - Жүктелгендер + Жүктелген Қолжетімсіз амал Жою CBZ не ZIP пішімде таңдаңыз. @@ -272,7 +272,7 @@ Ешқашан Қуат оңтайлығын өшіру Жоспарланған - Бетбелгілер + Бетбелгі Бәрін көрсету Таңдаулы санатыңыз жоқ Қате дәмейін @@ -303,7 +303,7 @@ Құпиясөз Маңганы толықтай Өзгерту іске қосылуы үшін қолданбаны өшіріп қосыңыз - Дереккөз сөніп жатыр + Дереккөз өшіп жатыр Сақтық көшірмесінің жиілігі Дерек пен құпиялық Қате болса көмектесе алады. Түгел тіркелгінің күші жойылады @@ -381,7 +381,7 @@ Жүктеп алу тоқтап қалды Тым көп сұрату. Біраздан соң қайталап көріңіз Wi-Fi арқылы ғана жүктеу - Белсенді жүктеудің бәрі жойылып, жартылай жүктелгендер жоғалып кетеді + Жүктеліп жатқанның бәрі жойылып, жартылай жүктелгендер жоғалып кетеді Өзектілігі Ұқсас маңга Сақтамау @@ -471,7 +471,7 @@ Шығып кету үшін «Кері» батырмасын екі рет басыңыз Бетбелгілер жойылды Мион - Дереккөз дәмейіннің қатесі пайда болып, айнасы қолжетімді болса, өздігінен соған ауыстыру + Дереккөз дәмейнінің қатесі пайда болып, айнасы қолжетімді болса, өздігінен соған ауыстыру Бетбелгі жоқ Жоқ, рақмет Қолданбаның жаңа нұсқасында пайда болған дереккөзді ұсыну @@ -514,4 +514,28 @@ Бұл маңга Бұл дереккөзде жанр бойынша және локал файлдар бойынша сүзуге болмайды Бұл дереккөзде бірнеше жанр бойынша сүзуге болмайды + Жанр алып тастау + Қауіпсіз + Кесте түрі + Бұл дереккөзде жанр мен күні бойынша сүзу жоқ + Жанр атауын тере бастаңыз + Жүктеу дұрыс басталмай жүрсе көмектесе алады + Қалпына келтіру + Сақтық көшірме күні: %s + Жақында + Теріс ат + Контент рейтиңі + Алдыңғы тарау + Келесі тарау + Алдыңғы бет + Дыбыс батырмасын қосу + Дыбыс батырмасы арқылы парақтау + Басқандағы әрекет + Басып тұрғандағы әрекет + Түк + Қандай дереккөз қосқыңыз келетінін таңдаңыз. Кейін баптап алуға болады + Келесі бет + Оқымадағы әрекет + Синхрондау тіркелгісіне кіру + Әдепкі баптауға қайтайық па? Әрекетті қайтаруға болмайды. \ No newline at end of file From 36472998eed10197dfee403926a8d1986e6d171f Mon Sep 17 00:00:00 2001 From: Tawsif Date: Sun, 7 Jul 2024 11:05:07 +0200 Subject: [PATCH 18/26] Translated using Weblate (Bengali) Currently translated at 24.5% (159 of 648 strings) Co-authored-by: Tawsif Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/ Translation: Kotatsu/Strings --- app/src/main/res/values-bn/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index ff06c4710..d4245aceb 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -11,7 +11,7 @@ ডোমেইন অ্যাপের নতুন ভার্সন পাওয়া গেছে ব্রাউজারে খুলুন - কিছু একটা গন্ডগোল হয়েছে + কিছু একটা সমস্যা হয়েছে খুঁটিনাটি পর্ব সমূহ তালিকা @@ -102,7 +102,7 @@ ডাউনলোডের জন্য ফোল্ডার কখনও কখনও প্রস্তাবিত মাঙ্গা সহ বিজ্ঞপ্তিগুলি দেখান৷ সাফ করা হয়েছে - তালিকা মোড + তালিকার ধরন ডাউনলোড করা হয়েছে হালনাগাদ ফিড আপডেট শীঘ্রই শুরু হবে @@ -151,4 +151,4 @@ পরামর্শ: %s এখানে খালি… সম্পন্ন - + \ No newline at end of file From 0b821db04603106f5628ad94c8271003bf422535 Mon Sep 17 00:00:00 2001 From: Eno Date: Sun, 7 Jul 2024 11:05:07 +0200 Subject: [PATCH 19/26] Translated using Weblate (Arabic) Currently translated at 60.4% (392 of 648 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (9 of 9 strings) Co-authored-by: Eno Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ar/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/ Translation: Kotatsu/Strings Translation: Kotatsu/plurals --- app/src/main/res/values-ar/plurals.xml | 8 ++++++++ app/src/main/res/values-ar/strings.xml | 2 ++ 2 files changed, 10 insertions(+) diff --git a/app/src/main/res/values-ar/plurals.xml b/app/src/main/res/values-ar/plurals.xml index 670c3d980..2fd1eee47 100644 --- a/app/src/main/res/values-ar/plurals.xml +++ b/app/src/main/res/values-ar/plurals.xml @@ -64,4 +64,12 @@ إحدى عشر دقيقة مئة دقيقة + + العربية + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index be2998822..892de7473 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -383,4 +383,6 @@ متوقف مؤقتاً التحميل عبر شبكة الوايفاي فقط إظهار الإشعارات أحيانًا بالمانغا المقترحة + اللغة العربية + ‌‌‍‎‎‍ \ No newline at end of file From 2f58f32bdd7b4825c9f871df3a2fe8813bb81272 Mon Sep 17 00:00:00 2001 From: Akhil Raj Date: Sun, 7 Jul 2024 11:05:07 +0200 Subject: [PATCH 20/26] Translated using Weblate (Malayalam) Currently translated at 2.6% (17 of 648 strings) Co-authored-by: Akhil Raj Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ml/ Translation: Kotatsu/Strings --- app/src/main/res/values-ml/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index fb7acd25c..baf8a8e35 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,4 +3,6 @@ ചരിത്രം അദ്ധ്യായങ്ങൾ ക്രമീകരണങ്ങൾ + പ്രിയപ്പെട്ടവ + വിശദാംശങ്ങൾ \ No newline at end of file From 6edda72d615c8fff7d148dd92c27974b3581f3c9 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sun, 7 Jul 2024 11:05:07 +0200 Subject: [PATCH 21/26] Translated using Weblate (Spanish) Currently translated at 100.0% (651 of 651 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (650 of 650 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 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index fe0651fe3..950531d70 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -644,4 +644,6 @@ Nuevos Todos los idiomas Bloquear en modo incógnito + Servidor de imágenes preferido + Páginas de recortes \ No newline at end of file From ae7aa5217778c46e743fc07b699b4777c02cde38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Hern=C3=A1ndez=20santillan?= Date: Sun, 7 Jul 2024 11:05:07 +0200 Subject: [PATCH 22/26] Added translation using Weblate (Abkhazian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jesús Hernández santillan --- app/src/main/res/values-ab/plurals.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-ab/plurals.xml diff --git a/app/src/main/res/values-ab/plurals.xml b/app/src/main/res/values-ab/plurals.xml new file mode 100644 index 000000000..a6b3daec9 --- /dev/null +++ b/app/src/main/res/values-ab/plurals.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 064c0ae4256ed9f6c125299e0371f81709cbc1c3 Mon Sep 17 00:00:00 2001 From: Scrambled777 Date: Sun, 7 Jul 2024 11:05:07 +0200 Subject: [PATCH 23/26] Translated using Weblate (Hindi) Currently translated at 100.0% (651 of 651 strings) Co-authored-by: Scrambled777 Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/ Translation: Kotatsu/Strings --- app/src/main/res/values-hi/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index c92721ea0..8576f23ce 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -644,4 +644,6 @@ नया सभी भाषाएं गुप्त मोड में ब्लॉक करें + पसंदीदा छवि सर्वर + पृष्ठ काटें \ No newline at end of file From 97bc638f5ff38ceed9b1b8cc2d19f651e7607227 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 11:05:07 +0200 Subject: [PATCH 24/26] Translated using Weblate (Ukrainian) Currently translated at 100.0% (658 of 658 strings) Translated using Weblate (Russian) Currently translated at 100.0% (658 of 658 strings) Co-authored-by: Koitharu Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/ Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/ Translation: Kotatsu/Strings --- app/src/main/res/values-ru/strings.xml | 9 +++++++++ app/src/main/res/values-uk/strings.xml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e4a8231df..2e3849546 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -644,4 +644,13 @@ Новое Все языки Блокировать в режиме инкогнито + Источник закреплён + Источники откреплены + Источники закреплены + Недавние источники + Обрезать страницы + Закрепить + Открепить + Источник откреплён + Сервер изображений \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5de454ea9..e14bee103 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -644,4 +644,13 @@ Нове Всі мови Блокувати в режимі інкогніто + Сервер зображень + Відкріпити + Джерело закріплено + Джерела закріплені + Нещодавні джерела + Обрізати сторінки + Джерела відкріплені + Закріпити + Джерело відкріплено \ No newline at end of file From d8fa0e33f13266be5978bdeb325b32eac2ada3cf Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sun, 7 Jul 2024 12:29:08 +0300 Subject: [PATCH 25/26] Update parsers --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f26e16455..bb59d1276 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 34 - versionCode = 650 - versionName = '7.2.1' + versionCode = 651 + versionName = '7.3' generatedDensities = [] testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' ksp { @@ -82,7 +82,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:7433fb8fa0') { + implementation('com.github.KotatsuApp:kotatsu-parsers:74b8aaa94e') { exclude group: 'org.json', module: 'json' } From 4102c4a0aeb60a3ac1a12fdf8587f258767b07c7 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 13 Jul 2024 06:16:26 +0300 Subject: [PATCH 26/26] Show last error in tracker log --- .../org/koitharu/kotatsu/tracker/ui/debug/TrackDebugAD.kt | 2 +- .../org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt | 1 + .../koitharu/kotatsu/tracker/ui/debug/TrackerDebugViewModel.kt | 1 + build.gradle | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) 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 9590267eb..e1a62459a 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 @@ -58,7 +58,7 @@ fun trackDebugAD( append(" - ") bold { color(context.getThemeColor(materialR.attr.colorError, Color.RED)) { - append(getString(R.string.error)) + append(item.lastError ?: getString(R.string.error)) } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt index 0e59c15ef..851c39427 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/ui/debug/TrackDebugItem.kt @@ -11,6 +11,7 @@ data class TrackDebugItem( val lastCheckTime: Instant?, val lastChapterDate: Instant?, val lastResult: Int, + val lastError: String?, ) : ListModel { override fun areItemsTheSame(other: ListModel): Boolean { 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 942741882..cf591c685 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 @@ -31,6 +31,7 @@ class TrackerDebugViewModel @Inject constructor( lastCheckTime = it.track.lastCheckTime.toInstantOrNull(), lastChapterDate = it.track.lastChapterDate.toInstantOrNull(), lastResult = it.track.lastResult, + lastError = it.track.lastError, ) } } diff --git a/build.gradle b/build.gradle index e4c4dcc09..daa9263a6 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.5.0' + classpath 'com.android.tools.build:gradle:8.5.1' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24' classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1' classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.24-1.0.20'