Pages crop proof-of-concept

master
Koitharu 2 years ago
parent 77bb5c2fcd
commit 77e393ae48
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -93,7 +93,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.0' 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.transition:transition-ktx:1.5.0'
implementation 'androidx.collection:collection-ktx:1.4.0' implementation 'androidx.collection:collection-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2' 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-base:2.6.0'
implementation 'io.coil-kt:coil-svg: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 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'
@ -154,10 +154,10 @@ dependencies {
testImplementation 'org.json:json:20240303' testImplementation 'org.json:json:20240303'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.6.1'
androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test:core-ktx:1.6.1'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.1'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'

@ -33,7 +33,6 @@ import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import java.io.File import java.io.File
import java.net.Proxy import java.net.Proxy
import java.util.EnumSet import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -485,6 +484,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isAutoLocalChaptersCleanupEnabled: Boolean val isAutoLocalChaptersCleanupEnabled: Boolean
get() = prefs.getBoolean(KEY_CHAPTERS_CLEAR_AUTO, false) 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 { fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true 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_ANIMATION = "reader_animation2"
const val KEY_READER_MODE = "reader_mode" const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect" 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 = "app_password"
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num" const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
const val KEY_PROTECT_APP = "protect_app" const val KEY_PROTECT_APP = "protect_app"
@ -698,5 +707,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
// old keys are for migration only // old keys are for migration only
private const val KEY_IMAGES_PROXY_OLD = "images_proxy" private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
// values
private const val READER_CROP_PAGED = 1
private const val READER_CROP_WEBTOON = 2
} }
} }

@ -61,6 +61,7 @@ class DetectReaderModeUseCase @Inject constructor(
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" } val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = repository.getPageUrl(page) val url = repository.getPageUrl(page)
val uri = Uri.parse(url) val uri = Uri.parse(url)
// TODO file support
val size = if (uri.scheme == "cbz") { val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart) val zip = ZipFile(uri.schemeSpecificPart)

@ -2,12 +2,14 @@ package org.koitharu.kotatsu.reader.domain
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Rect
import android.net.Uri import android.net.Uri
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.set import androidx.collection.set
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import com.davemorrissey.labs.subscaleview.ImageSource
import dagger.hilt.android.ActivityRetainedLifecycle import dagger.hilt.android.ActivityRetainedLifecycle
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped 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.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import java.util.LinkedList import java.util.LinkedList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -83,6 +86,7 @@ class PageLoader @Inject constructor(
private val prefetchQueue = LinkedList<MangaPage>() private val prefetchQueue = LinkedList<MangaPage>()
private val counter = AtomicInteger(0) private val counter = AtomicInteger(0)
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
private val whitespaceDetector = WhitespaceDetector(context)
fun isPrefetchApplicable(): Boolean { fun isPrefetchApplicable(): Boolean {
return repository is RemoteMangaRepository 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 { suspend fun getPageUrl(page: MangaPage): String {
return getRepository(page.source).getPageUrl(page) return getRepository(page.source).getPageUrl(page)
} }

@ -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 <R> Bitmap.use(block: (Bitmap) -> R) = try {
block(this)
} finally {
recycle()
}
}

@ -18,6 +18,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings 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.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
@ -54,6 +55,10 @@ class ReaderSettings(
view.background = bg.resolve(view.context) view.background = bg.resolve(view.context)
} }
fun isPagesCropEnabled(isWebtoon: Boolean) = settings.isPagesCropEnabled(
if (isWebtoon) ReaderMode.WEBTOON else ReaderMode.STANDARD,
)
@CheckResult @CheckResult
fun applyBitmapConfig(ssiv: SubsamplingScaleImageView): Boolean { fun applyBitmapConfig(ssiv: SubsamplingScaleImageView): Boolean {
val config = bitmapConfig val config = bitmapConfig

@ -13,6 +13,7 @@ import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.PageHolderDelegate.State import org.koitharu.kotatsu.reader.ui.pager.PageHolderDelegate.State
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonHolder
abstract class BasePageHolder<B : ViewBinding>( abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B, protected val binding: B,
@ -24,7 +25,14 @@ abstract class BasePageHolder<B : ViewBinding>(
) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), PageHolderDelegate.Callback { ) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), PageHolderDelegate.Callback {
@Suppress("LeakingThis") @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) protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
val context: Context val context: Context
@ -70,7 +78,7 @@ abstract class BasePageHolder<B : ViewBinding>(
delegate.onRecycle() delegate.onRecycle()
} }
protected fun SubsamplingScaleImageView.applyDownsampling(isForeground: Boolean) { protected fun SubsamplingScaleImageView.applyDownSampling(isForeground: Boolean) {
downSampling = when { downSampling = when {
isForeground || !settings.isReaderOptimizationEnabled -> 1 isForeground || !settings.isReaderOptimizationEnabled -> 1
context.isLowRamDevice() -> 8 context.isLowRamDevice() -> 8

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.ui.pager package org.koitharu.kotatsu.reader.ui.pager
import android.graphics.Rect
import android.net.Uri import android.net.Uri
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
@ -32,6 +33,7 @@ class PageHolderDelegate(
private val callback: Callback, private val callback: Callback,
private val networkState: NetworkState, private val networkState: NetworkState,
private val exceptionResolver: ExceptionResolver, private val exceptionResolver: ExceptionResolver,
private val isWebtoon: Boolean,
) : DefaultOnImageEventListener, Observer<ReaderSettings> { ) : DefaultOnImageEventListener, Observer<ReaderSettings> {
private val scope = loader.loaderScope + Dispatchers.Main.immediate private val scope = loader.loaderScope + Dispatchers.Main.immediate
@ -39,6 +41,7 @@ class PageHolderDelegate(
private set private set
private var job: Job? = null private var job: Job? = null
private var uri: Uri? = null private var uri: Uri? = null
private var cachedBounds: Rect? = null
private var error: Throwable? = null private var error: Throwable? = null
init { init {
@ -88,6 +91,7 @@ class PageHolderDelegate(
fun onRecycle() { fun onRecycle() {
state = State.EMPTY state = State.EMPTY
uri = null uri = null
cachedBounds = null
error = null error = null
job?.cancel() job?.cancel()
} }
@ -95,7 +99,7 @@ class PageHolderDelegate(
fun reload() { fun reload() {
if (state == State.SHOWN) { if (state == State.SHOWN) {
uri?.let { uri?.let {
callback.onImageReady(it) callback.onImageReady(it, cachedBounds)
} }
} }
} }
@ -138,8 +142,13 @@ class PageHolderDelegate(
state = State.CONVERTING state = State.CONVERTING
try { try {
val newUri = loader.convertBimap(uri) val newUri = loader.convertBimap(uri)
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
loader.getTrimmedBounds(newUri)
} else {
null
}
state = State.CONVERTED state = State.CONVERTED
callback.onImageReady(newUri) callback.onImageReady(newUri, cachedBounds)
} catch (ce: CancellationException) { } catch (ce: CancellationException) {
throw ce throw ce
} catch (e2: Throwable) { } catch (e2: Throwable) {
@ -166,7 +175,12 @@ class PageHolderDelegate(
file file
} }
state = State.LOADED 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) { } catch (e: CancellationException) {
throw e throw e
} catch (e: Throwable) { } catch (e: Throwable) {
@ -196,7 +210,7 @@ class PageHolderDelegate(
fun onError(e: Throwable) fun onError(e: Throwable)
fun onImageReady(uri: Uri) fun onImageReady(uri: Uri, bounds: Rect?)
fun onImageShowing(settings: ReaderSettings) fun onImageShowing(settings: ReaderSettings)

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.PointF import android.graphics.PointF
import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
@ -46,12 +47,12 @@ open class PageHolder(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.ssiv.applyDownsampling(isForeground = true) binding.ssiv.applyDownSampling(isForeground = true)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
binding.ssiv.applyDownsampling(isForeground = false) binding.ssiv.applyDownSampling(isForeground = false)
} }
override fun onConfigChanged() { override fun onConfigChanged() {
@ -59,7 +60,7 @@ open class PageHolder(
if (settings.applyBitmapConfig(binding.ssiv)) { if (settings.applyBitmapConfig(binding.ssiv)) {
delegate.reload() delegate.reload()
} }
binding.ssiv.applyDownsampling(isResumed()) binding.ssiv.applyDownSampling(isResumed())
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
} }
@ -89,8 +90,12 @@ open class PageHolder(
} }
} }
override fun onImageReady(uri: Uri) { override fun onImageReady(uri: Uri, bounds: Rect?) {
binding.ssiv.setImage(ImageSource.Uri(uri)) val source = ImageSource.Uri(uri)
if (bounds != null) {
source.region(bounds)
}
binding.ssiv.setImage(source)
} }
override fun onImageShowing(settings: ReaderSettings) { override fun onImageShowing(settings: ReaderSettings) {

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -39,12 +40,12 @@ class WebtoonHolder(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.ssiv.applyDownsampling(isForeground = true) binding.ssiv.applyDownSampling(isForeground = true)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
binding.ssiv.applyDownsampling(isForeground = false) binding.ssiv.applyDownSampling(isForeground = false)
} }
override fun onConfigChanged() { override fun onConfigChanged() {
@ -52,7 +53,7 @@ class WebtoonHolder(
if (settings.applyBitmapConfig(binding.ssiv)) { if (settings.applyBitmapConfig(binding.ssiv)) {
delegate.reload() delegate.reload()
} }
binding.ssiv.applyDownsampling(isResumed()) binding.ssiv.applyDownSampling(isResumed())
} }
override fun onBind(data: ReaderPage) { override fun onBind(data: ReaderPage) {
@ -89,8 +90,12 @@ class WebtoonHolder(
} }
} }
override fun onImageReady(uri: Uri) { override fun onImageReady(uri: Uri, bounds: Rect?) {
binding.ssiv.setImage(ImageSource.Uri(uri)) val source = ImageSource.Uri(uri)
if (bounds != null) {
source.region(bounds)
}
binding.ssiv.setImage(source)
} }
override fun onImageShowing(settings: ReaderSettings) { override fun onImageShowing(settings: ReaderSettings) {

@ -5,6 +5,7 @@ import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R 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.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.parsers.util.names import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity 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.PercentSummaryProvider
import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.settings.utils.SliderPreference
@ -48,6 +50,9 @@ class ReaderSettingsFragment :
entryValues = ZoomMode.entries.names() entryValues = ZoomMode.entries.names()
setDefaultValueCompat(ZoomMode.FIT_CENTER.name) setDefaultValueCompat(ZoomMode.FIT_CENTER.name)
} }
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_CROP)?.run {
summaryProvider = MultiSummaryProvider(R.string.disabled)
}
findPreference<SliderPreference>(AppSettings.KEY_WEBTOON_ZOOM_OUT)?.summaryProvider = PercentSummaryProvider() findPreference<SliderPreference>(AppSettings.KEY_WEBTOON_ZOOM_OUT)?.summaryProvider = PercentSummaryProvider()
updateReaderModeDependency() updateReaderModeDependency()
} }

@ -97,4 +97,8 @@
<item>@string/system_default</item> <item>@string/system_default</item>
<item>@string/more_frequently</item> <item>@string/more_frequently</item>
</string-array> </string-array>
<string-array name="reader_crop" translatable="false">
<item>@string/pages</item>
<item>@string/webtoon</item>
</string-array>
</resources> </resources>

@ -68,4 +68,8 @@
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>
</string-array> </string-array>
<string-array name="values_reader_crop" translatable="false">
<item>1</item>
<item>2</item>
</string-array>
</resources> </resources>

@ -88,6 +88,12 @@
android:summary="@string/reader_optimize_summary" android:summary="@string/reader_optimize_summary"
android:title="@string/reader_optimize" /> android:title="@string/reader_optimize" />
<MultiSelectListPreference
android:entries="@array/reader_crop"
android:entryValues="@array/values_reader_crop"
android:key="reader_crop"
android:title="Crop pages (beta)" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"
android:key="reader_fullscreen" android:key="reader_fullscreen"

Loading…
Cancel
Save