Configure default reader mode #160 #142

pull/168/head
Koitharu 4 years ago
parent 317252e1dd
commit 95d7ca5264
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -3,9 +3,6 @@ package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.Size import android.util.Size
import java.io.File
import java.io.InputStream
import java.util.zip.ZipFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -16,46 +13,46 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.medianOrNull import java.io.File
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import java.io.InputStream
import java.util.zip.ZipFile
import kotlin.math.roundToInt
object MangaUtils : KoinComponent { object MangaUtils : KoinComponent {
private const val MIN_WEBTOON_RATIO = 2
/** /**
* Automatic determine type of manga by page size * Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide * @return ReaderMode.WEBTOON if page is wide
*/ */
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? { suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean {
try { val pageIndex = (pages.size * 0.3).roundToInt()
val page = pages.medianOrNull() ?: return null val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = MangaRepository(page.source).getPageUrl(page) val url = MangaRepository(page.source).getPageUrl(page)
val uri = Uri.parse(url) val uri = Uri.parse(url)
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)
val entry = zip.getEntry(uri.fragment) val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use { zip.getInputStream(entry).use {
getBitmapSize(it) getBitmapSize(it)
}
} }
} else { }
val request = Request.Builder() } else {
.url(url) val request = Request.Builder()
.get() .url(url)
.header(CommonHeaders.REFERER, page.referer) .get()
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED) .header(CommonHeaders.REFERER, page.referer)
.build() .cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
get<OkHttpClient>().newCall(request).await().use { .build()
runInterruptible(Dispatchers.IO) { get<OkHttpClient>().newCall(request).await().use {
getBitmapSize(it.body?.byteStream()) runInterruptible(Dispatchers.IO) {
} getBitmapSize(it.body?.byteStream())
} }
} }
return size.width * 2 < size.height
} catch (e: Exception) {
e.printStackTraceDebug()
return null
} }
return size.width * MIN_WEBTOON_RATIO < size.height
} }
suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) { suspend fun getImageMimeType(file: File): String? = runInterruptible(Dispatchers.IO) {

@ -99,8 +99,11 @@ class AppSettings(context: Context) {
val readerAnimation: Boolean val readerAnimation: Boolean
get() = prefs.getBoolean(KEY_READER_ANIMATION, false) get() = prefs.getBoolean(KEY_READER_ANIMATION, false)
val isPreferRtlReader: Boolean val defaultReaderMode: ReaderMode
get() = prefs.getBoolean(KEY_READER_PREFER_RTL, false) get() = prefs.getEnumValue(KEY_READER_MODE, ReaderMode.STANDARD)
val isReaderModeDetectionEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_MODE_DETECT, true)
var historyGrouping: Boolean var historyGrouping: Boolean
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true) get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
@ -287,7 +290,8 @@ class AppSettings(context: Context) {
const val KEY_NOTIFICATIONS_LIGHT = "notifications_light" const val KEY_NOTIFICATIONS_LIGHT = "notifications_light"
const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info" const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info"
const val KEY_READER_ANIMATION = "reader_animation" const val KEY_READER_ANIMATION = "reader_animation"
const val KEY_READER_PREFER_RTL = "reader_prefer_rtl" const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_APP_PASSWORD = "app_password" const val KEY_APP_PASSWORD = "app_password"
const val KEY_PROTECT_APP = "protect_app" const val KEY_PROTECT_APP = "protect_app"
const val KEY_APP_VERSION = "app_version" const val KEY_APP_VERSION = "app_version"

@ -6,6 +6,7 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -31,7 +32,6 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import java.util.*
private const val BOUNDS_PAGE_OFFSET = 2 private const val BOUNDS_PAGE_OFFSET = 2
private const val PAGES_TRIM_THRESHOLD = 120 private const val PAGES_TRIM_THRESHOLD = 120
@ -264,15 +264,7 @@ class ReaderViewModel(
chapters.put(it.id, it) chapters.put(it.id, it)
} }
// determine mode // determine mode
val mode = dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let { val mode = detectReaderMode(manga, repo)
val pages = repo.getPages(it)
val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
val newMode = getReaderMode(isWebtoon)
if (isWebtoon != null) {
dataRepository.savePreferences(manga, newMode)
}
newMode
} ?: error("There are no chapters in this manga")
// obtain state // obtain state
if (currentState.value == null) { if (currentState.value == null) {
currentState.value = historyRepository.getOne(manga)?.let { currentState.value = historyRepository.getOne(manga)?.let {
@ -295,12 +287,6 @@ class ReaderViewModel(
} }
} }
private fun getReaderMode(isWebtoon: Boolean?) = when {
isWebtoon == true -> ReaderMode.WEBTOON
settings.isPreferRtlReader -> ReaderMode.REVERSED
else -> ReaderMode.STANDARD
}
private suspend fun loadChapter(chapterId: Long): List<ReaderPage> { private suspend fun loadChapter(chapterId: Long): List<ReaderPage> {
val manga = checkNotNull(mangaData.value) { "Manga is null" } val manga = checkNotNull(mangaData.value) { "Manga is null" }
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" } val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
@ -358,6 +344,26 @@ class ReaderViewModel(
subList(fromIndexBounded, toIndexBounded) subList(fromIndexBounded, toIndexBounded)
} }
} }
private suspend fun detectReaderMode(manga: Manga, repo: MangaRepository): ReaderMode {
dataRepository.getReaderMode(manga.id)?.let { return it }
val defaultMode = settings.defaultReaderMode
if (!settings.isReaderModeDetectionEnabled || defaultMode == ReaderMode.WEBTOON) {
return defaultMode
}
val chapter = currentState.value?.chapterId?.let(chapters::get)
?: manga.chapters?.randomOrNull()
?: error("There are no chapters in this manga")
val pages = repo.getPages(chapter)
return runCatching {
val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess {
dataRepository.savePreferences(manga, it)
}.onFailure {
it.printStackTraceDebug()
}.getOrDefault(defaultMode)
}
} }
/** /**

@ -12,9 +12,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.settings.utils.SliderPreference
import org.koitharu.kotatsu.utils.ext.names
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import java.util.* import java.util.*

@ -13,9 +13,9 @@ import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.settings.utils.SliderPreference
import org.koitharu.kotatsu.utils.ext.getStorageName import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.names
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import java.io.File import java.io.File
@ -41,7 +41,12 @@ class ContentSettingsFragment :
} }
} }
findPreference<ListPreference>(AppSettings.KEY_DOH)?.run { findPreference<ListPreference>(AppSettings.KEY_DOH)?.run {
entryValues = enumValues<DoHProvider>().names() entryValues = arrayOf(
DoHProvider.NONE,
DoHProvider.GOOGLE,
DoHProvider.CLOUDFLARE,
DoHProvider.ADGUARD,
).names()
setDefaultValueCompat(DoHProvider.NONE.name) setDefaultValueCompat(DoHProvider.NONE.name)
} }
bindRemoteSourcesSummary() bindRemoteSourcesSummary()

@ -1,26 +1,68 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
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.parsers.util.names
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
import org.koitharu.kotatsu.utils.ext.names
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
class ReaderSettingsFragment : BasePreferenceFragment(R.string.reader_settings) { class ReaderSettingsFragment :
BasePreferenceFragment(R.string.reader_settings),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_reader) addPreferencesFromResource(R.xml.pref_reader)
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.let { findPreference<ListPreference>(AppSettings.KEY_READER_MODE)?.run {
it.summaryProvider = MultiSummaryProvider(R.string.gestures_only) entryValues = arrayOf(
ReaderMode.STANDARD,
ReaderMode.REVERSED,
ReaderMode.WEBTOON,
).names()
setDefaultValueCompat(ReaderMode.STANDARD.name)
} }
findPreference<ListPreference>(AppSettings.KEY_ZOOM_MODE)?.let { findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.run {
it.entryValues = ZoomMode.values().names() summaryProvider = MultiSummaryProvider(R.string.gestures_only)
it.setDefaultValueCompat(ZoomMode.FIT_CENTER.name) }
findPreference<ListPreference>(AppSettings.KEY_ZOOM_MODE)?.run {
entryValues = arrayOf(
ZoomMode.FIT_CENTER,
ZoomMode.FIT_HEIGHT,
ZoomMode.FIT_WIDTH,
ZoomMode.KEEP_START,
).names()
setDefaultValueCompat(ZoomMode.FIT_CENTER.name)
}
updateReaderModeDependency()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settings.subscribe(this)
}
override fun onDestroyView() {
settings.unsubscribe(this)
super.onDestroyView()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_READER_MODE -> updateReaderModeDependency()
}
}
private fun updateReaderModeDependency() {
findPreference<Preference>(AppSettings.KEY_READER_MODE_DETECT)?.run {
isEnabled = settings.defaultReaderMode != ReaderMode.WEBTOON
} }
} }
} }

@ -3,10 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import androidx.collection.ArraySet import androidx.collection.ArraySet
import java.util.* import java.util.*
fun <T : Enum<T>> Array<T>.names() = Array(size) { i ->
this[i].name
}
fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) { fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
if (sourceIndex <= targetIndex) { if (sourceIndex <= targetIndex) {
Collections.rotate(subList(sourceIndex, targetIndex + 1), -1) Collections.rotate(subList(sourceIndex, targetIndex + 1), -1)

@ -40,4 +40,9 @@
<item>CloudFlare</item> <item>CloudFlare</item>
<item>AdGuard</item> <item>AdGuard</item>
</string-array> </string-array>
<string-array name="reader_modes">
<item>@string/standard</item>
<item>@string/right_to_left</item>
<item>@string/webtoon</item>
</string-array>
</resources> </resources>

@ -160,8 +160,6 @@
<string name="update_check_failed">Could not look for updates</string> <string name="update_check_failed">Could not look for updates</string>
<string name="no_update_available">No updates available</string> <string name="no_update_available">No updates available</string>
<string name="right_to_left">Right-to-left (←)</string> <string name="right_to_left">Right-to-left (←)</string>
<string name="prefer_rtl_reader">Prefer right-to-left (←) reader</string>
<string name="prefer_rtl_reader_summary">Reading mode can be set up separately for each series</string>
<string name="create_category">New category</string> <string name="create_category">New category</string>
<string name="scale_mode">Scale mode</string> <string name="scale_mode">Scale mode</string>
<string name="zoom_mode_fit_center">Fit center</string> <string name="zoom_mode_fit_center">Fit center</string>
@ -296,4 +294,7 @@
<string name="undo">Undo</string> <string name="undo">Undo</string>
<string name="removed_from_history">Removed from history</string> <string name="removed_from_history">Removed from history</string>
<string name="dns_over_https">DNS over HTTPS</string> <string name="dns_over_https">DNS over HTTPS</string>
<string name="default_mode">Default mode</string>
<string name="detect_reader_mode">Autodetect reader mode</string>
<string name="detect_reader_mode_summary">Automatically detect if manga is webtoon</string>
</resources> </resources>

@ -3,11 +3,17 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<ListPreference
android:entries="@array/reader_modes"
android:key="reader_mode"
android:title="@string/default_mode"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="true"
android:key="reader_prefer_rtl" android:key="reader_mode_detect"
android:summary="@string/prefer_rtl_reader_summary" android:summary="@string/detect_reader_mode_summary"
android:title="@string/prefer_rtl_reader" /> android:title="@string/detect_reader_mode" />
<ListPreference <ListPreference
android:entries="@array/zoom_modes" android:entries="@array/zoom_modes"

Loading…
Cancel
Save