@@ -3,9 +3,6 @@ package org.koitharu.kotatsu.base.domain
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.util.Size
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
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.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.medianOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
object MangaUtils : KoinComponent {
|
||||
|
||||
private const val MIN_WEBTOON_RATIO = 2
|
||||
|
||||
/**
|
||||
* Automatic determine type of manga by page size
|
||||
* @return ReaderMode.WEBTOON if page is wide
|
||||
*/
|
||||
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
|
||||
try {
|
||||
val page = pages.medianOrNull() ?: return null
|
||||
val url = MangaRepository(page.source).getPageUrl(page)
|
||||
val uri = Uri.parse(url)
|
||||
val size = if (uri.scheme == "cbz") {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry).use {
|
||||
getBitmapSize(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.header(CommonHeaders.REFERER, page.referer)
|
||||
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
||||
.build()
|
||||
get<OkHttpClient>().newCall(request).await().use {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
getBitmapSize(it.body?.byteStream())
|
||||
}
|
||||
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean {
|
||||
val pageIndex = (pages.size * 0.3).roundToInt()
|
||||
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
|
||||
val url = MangaRepository(page.source).getPageUrl(page)
|
||||
val uri = Uri.parse(url)
|
||||
val size = if (uri.scheme == "cbz") {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
zip.getInputStream(entry).use {
|
||||
getBitmapSize(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.header(CommonHeaders.REFERER, page.referer)
|
||||
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
||||
.build()
|
||||
get<OkHttpClient>().newCall(request).await().use {
|
||||
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) {
|
||||
|
||||
@@ -99,8 +99,11 @@ class AppSettings(context: Context) {
|
||||
val readerAnimation: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_ANIMATION, false)
|
||||
|
||||
val isPreferRtlReader: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_PREFER_RTL, false)
|
||||
val defaultReaderMode: ReaderMode
|
||||
get() = prefs.getEnumValue(KEY_READER_MODE, ReaderMode.STANDARD)
|
||||
|
||||
val isReaderModeDetectionEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_MODE_DETECT, true)
|
||||
|
||||
var historyGrouping: Boolean
|
||||
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_INFO = "tracker_notifications_info"
|
||||
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_PROTECT_APP = "protect_app"
|
||||
const val KEY_APP_VERSION = "app_version"
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
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.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
import java.util.*
|
||||
|
||||
private const val BOUNDS_PAGE_OFFSET = 2
|
||||
private const val PAGES_TRIM_THRESHOLD = 120
|
||||
@@ -264,15 +264,7 @@ class ReaderViewModel(
|
||||
chapters.put(it.id, it)
|
||||
}
|
||||
// determine mode
|
||||
val mode = dataRepository.getReaderMode(manga.id) ?: manga.chapters?.randomOrNull()?.let {
|
||||
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")
|
||||
val mode = detectReaderMode(manga, repo)
|
||||
// obtain state
|
||||
if (currentState.value == null) {
|
||||
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> {
|
||||
val manga = checkNotNull(mangaData.value) { "Manga is null" }
|
||||
val chapter = checkNotNull(chapters[chapterId]) { "Requested chapter not found" }
|
||||
@@ -358,6 +344,26 @@ class ReaderViewModel(
|
||||
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.core.prefs.AppSettings
|
||||
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.utils.SliderPreference
|
||||
import org.koitharu.kotatsu.utils.ext.names
|
||||
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
||||
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.prefs.AppSettings
|
||||
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.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.names
|
||||
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
@@ -41,7 +41,12 @@ class ContentSettingsFragment :
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
bindRemoteSourcesSummary()
|
||||
|
||||
@@ -1,26 +1,68 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
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 org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
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.parsers.util.names
|
||||
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
|
||||
import org.koitharu.kotatsu.utils.ext.names
|
||||
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?) {
|
||||
addPreferencesFromResource(R.xml.pref_reader)
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.let {
|
||||
it.summaryProvider = MultiSummaryProvider(R.string.gestures_only)
|
||||
findPreference<ListPreference>(AppSettings.KEY_READER_MODE)?.run {
|
||||
entryValues = arrayOf(
|
||||
ReaderMode.STANDARD,
|
||||
ReaderMode.REVERSED,
|
||||
ReaderMode.WEBTOON,
|
||||
).names()
|
||||
setDefaultValueCompat(ReaderMode.STANDARD.name)
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_ZOOM_MODE)?.let {
|
||||
it.entryValues = ZoomMode.values().names()
|
||||
it.setDefaultValueCompat(ZoomMode.FIT_CENTER.name)
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.run {
|
||||
summaryProvider = MultiSummaryProvider(R.string.gestures_only)
|
||||
}
|
||||
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 java.util.*
|
||||
|
||||
fun <T : Enum<T>> Array<T>.names() = Array(size) { i ->
|
||||
this[i].name
|
||||
}
|
||||
|
||||
fun <T> MutableList<T>.move(sourceIndex: Int, targetIndex: Int) {
|
||||
if (sourceIndex <= targetIndex) {
|
||||
Collections.rotate(subList(sourceIndex, targetIndex + 1), -1)
|
||||
|
||||
@@ -40,4 +40,9 @@
|
||||
<item>CloudFlare</item>
|
||||
<item>AdGuard</item>
|
||||
</string-array>
|
||||
<string-array name="reader_modes">
|
||||
<item>@string/standard</item>
|
||||
<item>@string/right_to_left</item>
|
||||
<item>@string/webtoon</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -160,8 +160,6 @@
|
||||
<string name="update_check_failed">Could not look for updates</string>
|
||||
<string name="no_update_available">No updates available</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="scale_mode">Scale mode</string>
|
||||
<string name="zoom_mode_fit_center">Fit center</string>
|
||||
@@ -296,4 +294,7 @@
|
||||
<string name="undo">Undo</string>
|
||||
<string name="removed_from_history">Removed from history</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>
|
||||
@@ -3,11 +3,17 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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
|
||||
android:defaultValue="false"
|
||||
android:key="reader_prefer_rtl"
|
||||
android:summary="@string/prefer_rtl_reader_summary"
|
||||
android:title="@string/prefer_rtl_reader" />
|
||||
android:defaultValue="true"
|
||||
android:key="reader_mode_detect"
|
||||
android:summary="@string/detect_reader_mode_summary"
|
||||
android:title="@string/detect_reader_mode" />
|
||||
|
||||
<ListPreference
|
||||
android:entries="@array/zoom_modes"
|
||||
|
||||
Reference in New Issue
Block a user