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