Add image server option to reader config sheet

This commit is contained in:
Koitharu
2024-07-06 14:21:46 +03:00
parent 1f03e0a84b
commit dfb50fbddc
12 changed files with 206 additions and 17 deletions

View File

@@ -48,7 +48,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean) is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean)
is ConfigKey.UserAgent -> putString(key.key, (value as String?)?.sanitizeHeaderValue()) is ConfigKey.UserAgent -> putString(key.key, (value as String?)?.sanitizeHeaderValue())
is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean) 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? ?: "")
} }
} }

View File

@@ -69,4 +69,11 @@ fun <T> Iterable<T>.sortedWithSafe(comparator: Comparator<in T>): List<T> = try
} }
} }
fun Collection<*>?.sizeOrZero() = if (this == null) 0 else size fun Collection<*>?.sizeOrZero() = this?.size ?: 0
@Suppress("UNCHECKED_CAST")
inline fun <T, reified R> Collection<T>.mapToArray(transform: (T) -> R): Array<R> {
val result = arrayOfNulls<R>(size)
forEachIndexed { index, t -> result[index] = transform(t) }
return result as Array<R>
}

View File

@@ -12,12 +12,16 @@ import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.parsers.util.cancelAll
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@@ -90,3 +94,10 @@ fun <T> Deferred<T>.peek(): T? = if (isCompleted) {
} else { } else {
null null
} }
@Suppress("SuspendFunctionOnCoroutineScope")
suspend fun CoroutineScope.cancelChildrenAndJoin(cause: CancellationException? = null) {
val jobs = coroutineContext[Job]?.children?.toList() ?: return
jobs.cancelAll(cause)
jobs.joinAll()
}

View File

@@ -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 { private suspend fun getAvailableSize(): Long = runCatchingCancellable {
val statFs = StatFs(cacheDir.get().absolutePath) val statFs = StatFs(cacheDir.get().absolutePath)
statFs.availableBytes statFs.availableBytes

View File

@@ -37,6 +37,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP 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.compressToPNG
import org.koitharu.kotatsu.core.util.ext.ensureRamAtLeast import org.koitharu.kotatsu.core.util.ext.ensureRamAtLeast
import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.core.util.ext.ensureSuccess
@@ -168,6 +169,14 @@ class PageLoader @Inject constructor(
return getRepository(page.source).getPageUrl(page) return getRepository(page.source).getPageUrl(page)
} }
suspend fun invalidate(clearCache: Boolean) {
tasks.clear()
loaderScope.cancelChildrenAndJoin()
if (clearCache) {
cache.clear()
}
}
private fun onIdle() = loaderScope.launch { private fun onIdle() = loaderScope.launch {
prefetchLock.withLock { prefetchLock.withLock {
while (prefetchQueue.isNotEmpty()) { while (prefetchQueue.isNotEmpty()) {

View File

@@ -291,17 +291,28 @@ constructor(
val prevJob = loadingJob val prevJob = loadingJob
loadingJob = launchLoadingJob(Dispatchers.Default) { loadingJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin() prevJob?.cancelAndJoin()
val currentChapterId = currentState.requireValue().chapterId val prevState = currentState.requireValue()
val allChapters = checkNotNull(manga).allChapters val newChapterId = if (delta != 0) {
var index = allChapters.indexOfFirst { x -> x.id == currentChapterId } val allChapters = checkNotNull(manga).allChapters
if (index < 0) { var index = allChapters.indexOfFirst { x -> x.id == prevState.chapterId }
return@launchLoadingJob 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) content.value = ReaderContent(emptyList(), null)
chaptersLoader.loadSingleChapter(newChapterId) 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,
),
)
} }
} }

View File

@@ -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
}
}

View File

@@ -16,8 +16,10 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R 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.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow 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.viewLifecycleScope
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding 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.ReaderViewModel
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
@@ -47,7 +50,14 @@ class ReaderConfigSheet :
@Inject @Inject
lateinit var orientationHelper: ScreenOrientationHelper lateinit var orientationHelper: ScreenOrientationHelper
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
@Inject
lateinit var pageLoader: PageLoader
private lateinit var mode: ReaderMode private lateinit var mode: ReaderMode
private lateinit var imageServerDelegate: ImageServerDelegate
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
@@ -57,6 +67,10 @@ class ReaderConfigSheet :
mode = arguments?.getInt(ARG_MODE) mode = arguments?.getInt(ARG_MODE)
?.let { ReaderMode.valueOf(it) } ?.let { ReaderMode.valueOf(it) }
?: ReaderMode.STANDARD ?: ReaderMode.STANDARD
imageServerDelegate = ImageServerDelegate(
mangaRepositoryFactory = mangaRepositoryFactory,
mangaSource = viewModel.manga?.toManga()?.source,
)
} }
override fun onCreateViewBinding( override fun onCreateViewBinding(
@@ -83,11 +97,20 @@ class ReaderConfigSheet :
binding.buttonSavePage.setOnClickListener(this) binding.buttonSavePage.setOnClickListener(this)
binding.buttonScreenRotate.setOnClickListener(this) binding.buttonScreenRotate.setOnClickListener(this)
binding.buttonSettings.setOnClickListener(this) binding.buttonSettings.setOnClickListener(this)
binding.buttonImageServer.setOnClickListener(this)
binding.buttonColorFilter.setOnClickListener(this) binding.buttonColorFilter.setOnClickListener(this)
binding.sliderTimer.addOnChangeListener(this) binding.sliderTimer.addOnChangeListener(this)
binding.switchScrollTimer.setOnCheckedChangeListener(this) binding.switchScrollTimer.setOnCheckedChangeListener(this)
binding.switchDoubleReader.setOnCheckedChangeListener(this) binding.switchDoubleReader.setOnCheckedChangeListener(this)
viewLifecycleScope.launch {
val isAvailable = imageServerDelegate.isAvailable()
if (isAvailable) {
bindImageServerTitle()
}
binding.buttonImageServer.isVisible = isAvailable
}
settings.observeAsStateFlow( settings.observeAsStateFlow(
scope = lifecycleScope + Dispatchers.Default, scope = lifecycleScope + Dispatchers.Default,
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED, key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
@@ -124,6 +147,14 @@ class ReaderConfigSheet :
val manga = viewModel.manga?.toManga() ?: return val manga = viewModel.manga?.toManga() ?: return
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page)) 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) 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 { interface Callback {
var isAutoScrollEnabled: Boolean var isAutoScrollEnabled: Boolean

View File

@@ -8,6 +8,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository 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.config.ConfigKey
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.settings.utils.AutoCompleteTextViewPreference import org.koitharu.kotatsu.settings.utils.AutoCompleteTextViewPreference
@@ -102,10 +103,3 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
private fun Array<out String>.toStringArray(): Array<String> { private fun Array<out String>.toStringArray(): Array<String> {
return Array(size) { i -> this[i] as? String ?: "" } return Array(size) { i -> this[i] as? String ?: "" }
} }
@Suppress("UNCHECKED_CAST")
private inline fun <T, reified R> Collection<T>.mapToArray(transform: (T) -> R): Array<R> {
val result = arrayOfNulls<R>(size)
forEachIndexed { index, t -> result[index] = transform(t) }
return result as Array<R>
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3M15.96,10.29L13.21,13.83L11.25,11.47L8.5,15H19.5L15.96,10.29Z" />
</vector>

View File

@@ -210,6 +210,19 @@
android:textAppearance="?attr/textAppearanceButton" android:textAppearance="?attr/textAppearanceButton"
app:drawableStartCompat="@drawable/ic_appearance" /> app:drawableStartCompat="@drawable/ic_appearance" />
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
android:id="@+id/button_image_server"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/image_server"
android:textAppearance="?attr/textAppearanceButton"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_images"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView <org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
android:id="@+id/button_settings" android:id="@+id/button_settings"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -655,4 +655,5 @@
<string name="all_languages">All languages</string> <string name="all_languages">All languages</string>
<string name="screenshots_block_incognito">Block when incognito mode</string> <string name="screenshots_block_incognito">Block when incognito mode</string>
<string name="image_server">Preferred image server</string> <string name="image_server">Preferred image server</string>
<string name="inline_preference_pattern" translatable="false">%1$s: %2$s</string>
</resources> </resources>