Compare commits

...

17 Commits

Author SHA1 Message Date
Koitharu
d224cd99bb Update parsers 2022-11-21 09:18:09 +02:00
Blagoje Nikolić
b955d31770 Translated using Weblate (Serbian)
Currently translated at 4.2% (17 of 398 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (8 of 8 strings)

Added translation using Weblate (Serbian)

Translated using Weblate (Serbian)

Currently translated at 12.5% (1 of 8 strings)

Added translation using Weblate (Serbian)

Co-authored-by: Blagoje Nikolić <blagojenikolic006@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2022-11-21 09:17:33 +02:00
Koitharu
b4eb8d56a6 Auto refresh page on connection restored 2022-11-17 19:02:20 +02:00
Koitharu
c896ac72e8 Improve page loading progress displaying 2022-11-12 10:28:47 +02:00
Koitharu
b599cb33ff Improve pages loading #256 2022-11-11 20:09:00 +02:00
Koitharu
b3eab1a2a0 Fix crash on app start in background 2022-11-06 10:38:55 +02:00
Koitharu
79d9dc7b24 Use DummyParser as fallback in release builds 2022-11-06 10:26:14 +02:00
Koitharu
7b573f8e6b Update parsers 2022-11-06 10:14:09 +02:00
Dpper
7bd769e294 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (398 of 398 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2022-11-03 18:11:37 +02:00
Oğuz Ersen
fde5f86313 Translated using Weblate (Turkish)
Currently translated at 100.0% (398 of 398 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2022-11-03 18:11:37 +02:00
Koitharu
3c23bf7ec9 Remove DialogWhenLarge theme 2022-11-03 18:00:19 +02:00
Koitharu
4665f8b74e Add syncronized to PageLoader.tasks (fix crash) 2022-11-03 17:57:18 +02:00
Koitharu
5a43e677c5 Respect incognito mode in search 2022-10-28 08:21:13 +03:00
Koitharu
38d4274ece Shelf settings 2022-10-28 07:56:29 +03:00
Koitharu
0e5221fa6e Fix NetworkStateObserver 2022-10-24 19:52:30 +03:00
Koitharu
b458bde8a1 Configure shelf sections (2) 2022-10-24 19:40:08 +03:00
Koitharu
c663d10515 Configure shelf sections 2022-10-24 19:32:28 +03:00
65 changed files with 1042 additions and 379 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,2 @@
ko_fi: xtimms
custom: ["https://yoomoney.ru/to/410012543938752"]
ko_fi: koitharu

View File

@@ -26,7 +26,7 @@ Download APK directly from GitHub:
* Notifications about new chapters with updates feed
* Shikimori integration (manga tracking)
* Password/fingerprint protect access to the app
* History and favourites synchronization across devices (coming soon)
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
### Screenshots

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 502
versionName '4.0.2'
versionCode 504
versionName '4.0.4'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -83,15 +83,15 @@ afterEvaluate {
}
}
dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:a1441e7ed7') {
implementation('com.github.KotatsuApp:kotatsu-parsers:1e49d4095b') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.activity:activity-ktx:1.6.0'
implementation 'androidx.fragment:fragment-ktx:1.5.3'
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
@@ -137,10 +137,10 @@ dependencies {
testImplementation 'org.json:json:20220924'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
androidTestImplementation 'androidx.test:runner:1.5.1'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.4'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'

View File

@@ -28,6 +28,7 @@
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@@ -108,8 +109,7 @@
<activity
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:exported="true"
android:label="@string/manga_shelf"
android:theme="@style/Theme.Kotatsu.DialogWhenLarge">
android:label="@string/manga_shelf">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
@@ -127,18 +127,18 @@
<activity
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
android:label="@string/downloads"
android:launchMode="singleTop"
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
android:launchMode="singleTop" />
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
<activity
android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity"
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
<activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" />
<activity
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
android:label="@string/sync" />
<activity
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
android:label="@string/color_correction" />
<activity
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
android:label="@string/settings" />
<service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"

View File

@@ -53,7 +53,6 @@ abstract class BaseActivity<B : ViewBinding> :
EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
val isAmoled = settings.isAmoledTheme
val isDynamic = settings.isDynamicTheme
// TODO support DialogWhenLarge theme
when {
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)

View File

@@ -6,7 +6,7 @@ import android.view.View.OnLongClickListener
import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder
class AdapterDelegateClickListenerAdapter<I>(
private val adapterDelegate: AdapterDelegateViewBindingViewHolder<I, *>,
private val adapterDelegate: AdapterDelegateViewBindingViewHolder<out I, *>,
private val clickListener: OnListItemClickListener<I>,
) : OnClickListener, OnLongClickListener {
@@ -17,4 +17,4 @@ class AdapterDelegateClickListenerAdapter<I>(
override fun onLongClick(v: View): Boolean {
return clickListener.onItemLongClick(adapterDelegate.item, v)
}
}
}

View File

@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.utils.ext.longHashCode
fun TagEntity.toMangaTag() = MangaTag(
key = this.key,
title = this.title.toTitleCase(),
source = MangaSource(this.source) ?: MangaSource.DUMMY,
source = MangaSource(this.source),
)
fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
@@ -28,7 +28,7 @@ fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
author = this.author,
source = MangaSource(this.source) ?: MangaSource.DUMMY,
source = MangaSource(this.source),
tags = tags,
)

View File

@@ -6,9 +6,6 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity
@@ -20,6 +17,9 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.utils.TaggedActivityResult
import org.koitharu.kotatsu.utils.isSuccess
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ExceptionResolver private constructor(
private val activity: FragmentActivity?,
@@ -49,6 +49,7 @@ class ExceptionResolver private constructor(
openInBrowser(e.url)
false
}
else -> false
}

View File

@@ -1,18 +1,17 @@
package org.koitharu.kotatsu.core.model
import java.util.*
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.Locale
fun MangaSource.getLocaleTitle(): String? {
val lc = Locale(locale ?: return null)
return lc.getDisplayLanguage(lc).toTitleCase(lc)
}
@Suppress("FunctionName")
fun MangaSource(name: String): MangaSource? {
fun MangaSource(name: String): MangaSource {
MangaSource.values().forEach {
if (it.name == name) return it
}
return null
}
return MangaSource.DUMMY
}

View File

@@ -7,10 +7,12 @@ import android.net.NetworkRequest
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onSuccess
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
import javax.inject.Inject
@@ -26,7 +28,8 @@ class NetworkStateObserver @Inject constructor(
override val replayCache: List<Boolean>
get() = listOf(value)
override var value: Boolean = connectivityManager.isNetworkAvailable
override val value: Boolean
get() = connectivityManager.isNetworkAvailable
override suspend fun collect(collector: FlowCollector<Boolean>): Nothing {
collector.emit(value)
@@ -35,6 +38,13 @@ class NetworkStateObserver @Inject constructor(
}
}
suspend fun awaitForConnection(): Unit {
if (value) {
return
}
first { it }
}
private fun observeImpl() = callbackFlow<Boolean> {
val request = NetworkRequest.Builder().build()
val callback = FlowNetworkCallback(this)
@@ -44,9 +54,12 @@ class NetworkStateObserver @Inject constructor(
}
}
inner class FlowNetworkCallback(
private inner class FlowNetworkCallback(
private val producerScope: ProducerScope<Boolean>,
) : NetworkCallback() {
private var prevValue = value
override fun onAvailable(network: Network) = update()
override fun onLost(network: Network) = update()
@@ -55,9 +68,10 @@ class NetworkStateObserver @Inject constructor(
private fun update() {
val newValue = connectivityManager.isNetworkAvailable
if (value != newValue) {
value = newValue
producerScope.trySendBlocking(newValue)
if (newValue != prevValue) {
producerScope.trySendBlocking(newValue).onSuccess {
prevValue = newValue
}
}
}
}

View File

@@ -14,7 +14,6 @@ import coil.network.HttpException
import coil.request.Options
import coil.size.Size
import coil.size.pxOrElse
import java.net.HttpURLConnection
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
@@ -27,6 +26,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import java.net.HttpURLConnection
private const val FALLBACK_SIZE = 9999 // largest icon
@@ -150,7 +150,7 @@ class FaviconFetcher(
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
return if (data.scheme == URI_SCHEME_FAVICON) {
val mangaSource = MangaSource(data.schemeSpecificPart) ?: return null
val mangaSource = MangaSource(data.schemeSpecificPart)
FaviconFetcher(okHttpClient, diskCache, mangaSource, options, mangaRepositoryFactory)
} else {
null

View File

@@ -1,6 +0,0 @@
package org.koitharu.kotatsu.core.prefs
enum class AppSection {
LOCAL, FAVOURITES, HISTORY, FEED, SUGGESTIONS
}

View File

@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.shelf.domain.ShelfSection
import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.observe
import org.koitharu.kotatsu.utils.ext.putEnumValue
@@ -44,14 +45,26 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val remoteMangaSources: Set<MangaSource>
get() = Collections.unmodifiableSet(remoteSources)
var shelfSections: List<ShelfSection>
get() {
val raw = prefs.getString(KEY_SHELF_SECTIONS, null)
val values = enumValues<ShelfSection>()
if (raw.isNullOrEmpty()) {
return values.toList()
}
return raw.split('|')
.mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) }
.distinct()
}
set(value) {
val raw = value.joinToString("|") { it.ordinal.toString() }
prefs.edit { putString(KEY_SHELF_SECTIONS, raw) }
}
var listMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
var defaultSection: AppSection
get() = prefs.getEnumValue(KEY_APP_SECTION, AppSection.HISTORY)
set(value) = prefs.edit { putEnumValue(KEY_APP_SECTION, value) }
val theme: Int
get() = prefs.getString(KEY_THEME, null)?.toIntOrNull() ?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
@@ -341,6 +354,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_LOCAL_LIST_ORDER = "local_order"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_SHELF_SECTIONS = "shelf_sections_2"
// About
const val KEY_APP_UPDATE = "app_update"

View File

@@ -115,4 +115,4 @@ class ZipOutput(
closeEntry()
return true
}
}
}

View File

@@ -17,7 +17,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
@@ -36,6 +35,7 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
@@ -219,10 +219,8 @@ class DownloadManager @AssistedInject constructor(
val call = okHttp.newCall(request)
val file = File(destination, tempFileName)
val response = call.clone().await()
runInterruptible(Dispatchers.IO) {
file.outputStream().use { out ->
checkNotNull(response.body).byteStream().copyTo(out)
}
file.outputStream().use { out ->
checkNotNull(response.body).byteStream().copyToSuspending(out)
}
return file
}

View File

@@ -3,15 +3,17 @@ package org.koitharu.kotatsu.local.data
import android.content.Context
import com.tomclaw.cache.DiskLruCache
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
import java.io.InputStream
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable
@Singleton
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
@@ -26,42 +28,15 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
return lruCache.get(url)?.takeIfReadable()
}
fun put(url: String, inputStream: InputStream): File {
suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) {
val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use { out ->
inputStream.copyTo(out)
}
val res = lruCache.put(url, file)
file.delete()
return res
}
fun put(
url: String,
inputStream: InputStream,
contentLength: Long,
progress: MutableStateFlow<Float>,
): File {
val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use { out ->
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = inputStream.read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
publishProgress(contentLength, bytesCopied, progress)
bytes = inputStream.read(buffer)
try {
file.outputStream().use { out ->
inputStream.copyToSuspending(out)
}
}
val res = lruCache.put(url, file)
file.delete()
return res
}
private fun publishProgress(contentLength: Long, bytesCopied: Long, progress: MutableStateFlow<Float>) {
if (contentLength > 0) {
progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat()
lruCache.put(url, file)
} finally {
file.delete()
}
}
}

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.documentfile.provider.DocumentFile
import java.io.File
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.local.data.LocalStorageManager
@@ -14,8 +13,10 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.longOf
import java.io.File
// TODO: Add support for chapters in cbz
// https://github.com/KotatsuApp/Kotatsu/issues/31
@@ -62,6 +63,7 @@ class DirMangaImporter(
file.isDirectory -> {
addPages(output, file, path + "/" + file.name, state)
}
file.isFile -> {
val tempFile = file.asTempFile()
if (!state.hasCover) {
@@ -86,7 +88,7 @@ class DirMangaImporter(
"Cannot open input stream for $uri"
}.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
input.copyToSuspending(output)
}
}
return file

View File

@@ -1,8 +1,6 @@
package org.koitharu.kotatsu.local.domain.importer
import android.net.Uri
import java.io.File
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
@@ -11,7 +9,10 @@ import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.resolveName
import java.io.File
import java.io.IOException
class ZipMangaImporter(
storageManager: LocalStorageManager,
@@ -27,10 +28,10 @@ class ZipMangaImporter(
}
val dest = File(getOutputDir(), name)
runInterruptible {
contentResolver.openInputStream(uri)?.use { source ->
dest.outputStream().use { output ->
source.copyTo(output)
}
contentResolver.openInputStream(uri)
}?.use { source ->
dest.outputStream().use { output ->
source.copyToSuspending(output)
}
} ?: throw IOException("Cannot open input stream: $uri")
localMangaRepository.getFromFile(dest)

View File

@@ -17,6 +17,7 @@ import androidx.core.graphics.Insets
import androidx.core.util.size
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
@@ -24,6 +25,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenResumed
import androidx.transition.TransitionManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
@@ -129,6 +131,7 @@ class MainActivity :
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
viewModel.counters.observe(this, ::onCountersChanged)
viewModel.isFeedAvailable.observe(this, ::onFeedAvailabilityChanged)
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -274,6 +277,16 @@ class MainActivity :
navigationDelegate.setItemVisibility(R.id.nav_feed, isFeedAvailable)
}
private fun onIncognitoModeChanged(isIncognito: Boolean) {
var options = binding.searchView.imeOptions
options = if (isIncognito) {
options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
} else {
options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
}
binding.searchView.imeOptions = options
}
private fun onLoadingStateChanged(isLoading: Boolean) {
binding.fab?.isEnabled = !isLoading
}
@@ -309,8 +322,13 @@ class MainActivity :
private fun onFirstStart() {
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
!settings.isSourcesSelected -> whenResumed {
OnboardDialogFragment.showWelcome(supportFragmentManager)
}
settings.newSources.isNotEmpty() -> whenResumed {
NewSourcesDialogFragment.show(supportFragmentManager)
}
}
withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext)

View File

@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.withProgress
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
import java.io.File
import java.util.LinkedList
@@ -66,7 +67,9 @@ class PageLoader @Inject constructor(
override fun close() {
loaderScope.cancel()
tasks.clear()
synchronized(tasks) {
tasks.clear()
}
}
fun isPrefetchApplicable(): Boolean {
@@ -103,7 +106,9 @@ class PageLoader @Inject constructor(
return task
}
task = loadPageAsyncImpl(page)
tasks[page.id] = task
synchronized(tasks) {
tasks[page.id] = task
}
return task
}
@@ -135,7 +140,9 @@ class PageLoader @Inject constructor(
while (prefetchQueue.isNotEmpty()) {
val page = prefetchQueue.pollFirst() ?: return
if (cache[page.url] == null) {
tasks[page.id] = loadPageAsyncImpl(page)
synchronized(tasks) {
tasks[page.id] = loadPageAsyncImpl(page)
}
return
}
}
@@ -173,9 +180,12 @@ class PageLoader @Inject constructor(
val uri = Uri.parse(pageUrl)
return if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
ZipFile(uri.schemeSpecificPart)
}.use { zip ->
runInterruptible(Dispatchers.IO) {
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry)
}.use {
cache.put(pageUrl, it)
}
}
@@ -194,10 +204,8 @@ class PageLoader @Inject constructor(
val body = checkNotNull(response.body) {
"Null response"
}
runInterruptible(Dispatchers.IO) {
body.byteStream().use {
cache.put(pageUrl, it, body.contentLength(), progress)
}
body.withProgress(progress).byteStream().use {
cache.put(pageUrl, it)
}
}
}

View File

@@ -6,10 +6,6 @@ import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -20,6 +16,11 @@ import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
private const val MAX_FILENAME_LENGTH = 10
private const val EXTENSION_FALLBACK = "png"
@@ -48,12 +49,12 @@ class PageSaveHelper @Inject constructor(
}
}
runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.use { output ->
pageFile.inputStream().use { input ->
input.copyTo(output)
}
} ?: throw IOException("Output stream is null")
}
contentResolver.openOutputStream(destination)
}?.use { output ->
pageFile.inputStream().use { input ->
input.copyToSuspending(output)
}
} ?: throw IOException("Output stream is null")
return destination
}

View File

@@ -5,6 +5,7 @@ import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -13,11 +14,12 @@ abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B,
loader: PageLoader,
settings: ReaderSettings,
networkStateObserver: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
@Suppress("LeakingThis")
protected val delegate = PageHolderDelegate(loader, settings, this, exceptionResolver)
protected val delegate = PageHolderDelegate(loader, settings, this, networkStateObserver, exceptionResolver)
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
val context: Context

View File

@@ -4,17 +4,19 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.utils.ext.resetTransformations
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@Suppress("LeakingThis")
abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
private val loader: PageLoader,
private val readerSettings: ReaderSettings,
private val networkState: NetworkStateObserver,
private val exceptionResolver: ExceptionResolver,
) : RecyclerView.Adapter<H>() {
@@ -56,9 +58,9 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
final override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): H = onCreateViewHolder(parent, loader, readerSettings, exceptionResolver)
): H = onCreateViewHolder(parent, loader, readerSettings, networkState, exceptionResolver)
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine<Unit> { cont ->
suspend fun setItems(items: List<ReaderPage>) = suspendCoroutine { cont ->
differ.submitList(items) {
cont.resume(Unit)
}
@@ -68,6 +70,7 @@ abstract class BaseReaderAdapter<H : BasePageHolder<*>>(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
): H

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
@@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -26,6 +28,7 @@ class PageHolderDelegate(
private val loader: PageLoader,
private val readerSettings: ReaderSettings,
private val callback: Callback,
private val networkState: NetworkStateObserver,
private val exceptionResolver: ExceptionResolver,
) : DefaultOnImageEventListener, Observer<ReaderSettings> {
@@ -118,29 +121,35 @@ class PageHolderDelegate(
}
}
private suspend fun CoroutineScope.doLoad(data: MangaPage, force: Boolean) {
private suspend fun doLoad(data: MangaPage, force: Boolean) {
state = State.LOADING
error = null
callback.onLoadingStarted()
try {
val task = loader.loadPageAsync(data, force)
val progressObserver = observeProgress(this, task.progressAsFlow())
val file = task.await()
progressObserver.cancel()
this@PageHolderDelegate.file = file
file = coroutineScope {
val progressObserver = observeProgress(this, task.progressAsFlow())
val file = task.await()
progressObserver.cancel()
file
}
state = State.LOADED
callback.onImageReady(file.toUri())
callback.onImageReady(checkNotNull(file).toUri())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
state = State.ERROR
error = e
callback.onError(e)
if (e is IOException && !networkState.value) {
networkState.awaitForConnection()
retry(data)
}
}
}
private fun observeProgress(scope: CoroutineScope, progress: Flow<Float>) = progress
.debounce(500)
.debounce(250)
.onEach { callback.onProgressChanged((100 * it).toInt()) }
.launchIn(scope)

View File

@@ -6,6 +6,7 @@ import android.widget.FrameLayout
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -15,8 +16,9 @@ class ReversedPageHolder(
binding: ItemPageBinding,
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) : PageHolder(binding, loader, settings, exceptionResolver) {
) : PageHolder(binding, loader, settings, networkState, exceptionResolver) {
init {
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
@@ -35,6 +37,7 @@ class ReversedPageHolder(
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
resetScaleAndCenter()
}
ZoomMode.FIT_HEIGHT -> {
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
minScale = height / sHeight.toFloat()
@@ -43,6 +46,7 @@ class ReversedPageHolder(
PointF(sWidth.toFloat(), sHeight / 2f),
)
}
ZoomMode.FIT_WIDTH -> {
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
minScale = width / sWidth.toFloat()
@@ -51,6 +55,7 @@ class ReversedPageHolder(
PointF(sWidth / 2f, 0f),
)
}
ZoomMode.KEEP_START -> {
minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
setScaleAndCenter(

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.view.LayoutInflater
import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -11,18 +12,21 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class ReversedPagesAdapter(
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, exceptionResolver) {
) : BaseReaderAdapter<ReversedPageHolder>(loader, settings, networkState, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) = ReversedPageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader,
settings = settings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
}

View File

@@ -7,8 +7,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.absoluteValue
import kotlinx.coroutines.async
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -19,10 +19,15 @@ import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.recyclerView
import org.koitharu.kotatsu.utils.ext.resetTransformations
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import javax.inject.Inject
import kotlin.math.absoluteValue
@AndroidEntryPoint
class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@Inject
lateinit var networkStateObserver: NetworkStateObserver
private var pagerAdapter: ReversedPagesAdapter? = null
override fun onInflateView(
@@ -33,7 +38,12 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagerAdapter = ReversedPagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
pagerAdapter = ReversedPagesAdapter(
viewModel.pageLoader,
viewModel.readerSettings,
networkStateObserver,
exceptionResolver,
)
with(binding.pager) {
adapter = pagerAdapter
offscreenPageLimit = 2
@@ -44,8 +54,8 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
val transformer = if (it) ReversedPageAnimTransformer() else null
binding.pager.setPageTransformer(transformer)
if (transformer == null) {
binding.pager.recyclerView?.children?.forEach {
it.resetTransformations()
binding.pager.recyclerView?.children?.forEach { v ->
v.resetTransformations()
}
}
}

View File

@@ -10,6 +10,7 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -21,8 +22,9 @@ open class PageHolder(
binding: ItemPageBinding,
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, exceptionResolver),
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, networkState, exceptionResolver),
View.OnClickListener {
init {
@@ -74,6 +76,7 @@ open class PageHolder(
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
binding.ssiv.resetScaleAndCenter()
}
ZoomMode.FIT_HEIGHT -> {
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
binding.ssiv.minScale = binding.ssiv.height / binding.ssiv.sHeight.toFloat()
@@ -82,6 +85,7 @@ open class PageHolder(
PointF(0f, binding.ssiv.sHeight / 2f),
)
}
ZoomMode.FIT_WIDTH -> {
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CUSTOM
binding.ssiv.minScale = binding.ssiv.width / binding.ssiv.sWidth.toFloat()
@@ -90,6 +94,7 @@ open class PageHolder(
PointF(binding.ssiv.sWidth / 2f, 0f),
)
}
ZoomMode.KEEP_START -> {
binding.ssiv.minimumScaleType = SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
binding.ssiv.setScaleAndCenter(

View File

@@ -7,8 +7,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.absoluteValue
import kotlinx.coroutines.async
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -18,10 +18,15 @@ import org.koitharu.kotatsu.utils.ext.doOnPageChanged
import org.koitharu.kotatsu.utils.ext.recyclerView
import org.koitharu.kotatsu.utils.ext.resetTransformations
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import javax.inject.Inject
import kotlin.math.absoluteValue
@AndroidEntryPoint
class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@Inject
lateinit var networkStateObserver: NetworkStateObserver
private var pagesAdapter: PagesAdapter? = null
override fun onInflateView(
@@ -32,7 +37,12 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagesAdapter = PagesAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
pagesAdapter = PagesAdapter(
viewModel.pageLoader,
viewModel.readerSettings,
networkStateObserver,
exceptionResolver,
)
with(binding.pager) {
adapter = pagesAdapter
offscreenPageLimit = 2

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.LayoutInflater
import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -11,18 +12,21 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class PagesAdapter(
loader: PageLoader,
settings: ReaderSettings,
networkStateObserver: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter<PageHolder>(loader, settings, exceptionResolver) {
) : BaseReaderAdapter<PageHolder>(loader, settings, networkStateObserver, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) = PageHolder(
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader,
settings = settings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
}

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.view.LayoutInflater
import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -12,13 +12,15 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class WebtoonAdapter(
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, exceptionResolver) {
) : BaseReaderAdapter<WebtoonHolder>(loader, settings, networkState, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) = WebtoonHolder(
binding = ItemPageWebtoonBinding.inflate(
@@ -28,6 +30,7 @@ class WebtoonAdapter(
),
loader = loader,
settings = settings,
networkState = networkState,
exceptionResolver = exceptionResolver,
)
}

View File

@@ -8,20 +8,26 @@ import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.GoneOnInvisibleListener
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hideCompat
import org.koitharu.kotatsu.utils.ext.ifZero
import org.koitharu.kotatsu.utils.ext.setProgressCompat
import org.koitharu.kotatsu.utils.ext.showCompat
class WebtoonHolder(
binding: ItemPageWebtoonBinding,
loader: PageLoader,
settings: ReaderSettings,
networkState: NetworkStateObserver,
exceptionResolver: ExceptionResolver,
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, exceptionResolver),
) : BasePageHolder<ItemPageWebtoonBinding>(binding, loader, settings, networkState, exceptionResolver),
View.OnClickListener {
private var scrollToRestore = 0

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -15,10 +16,14 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.findCenterViewPosition
import org.koitharu.kotatsu.utils.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import javax.inject.Inject
@AndroidEntryPoint
class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
@Inject
lateinit var networkStateObserver: NetworkStateObserver
private val scrollInterpolator = AccelerateDecelerateInterpolator()
private var webtoonAdapter: WebtoonAdapter? = null
@@ -29,7 +34,12 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
webtoonAdapter = WebtoonAdapter(viewModel.pageLoader, viewModel.readerSettings, exceptionResolver)
webtoonAdapter = WebtoonAdapter(
viewModel.pageLoader,
viewModel.readerSettings,
networkStateObserver,
exceptionResolver,
)
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = webtoonAdapter

View File

@@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint
@@ -15,7 +16,6 @@ import org.koitharu.kotatsu.databinding.ActivitySearchBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.utils.ext.showKeyboard
import kotlin.text.Typography.dagger
@AndroidEntryPoint
class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
@@ -32,6 +32,7 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQuery
}
val query = intent.getStringExtra(EXTRA_QUERY)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
with(binding.searchView) {
queryHint = getString(R.string.search_on_s, source.title)
setOnQueryTextListener(this@SearchActivity)
@@ -72,6 +73,16 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQuery
override fun onQueryTextChange(newText: String?): Boolean = false
private fun onIncognitoModeChanged(isIncognito: Boolean) {
var options = binding.searchView.imeOptions
options = if (isIncognito) {
options or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING
} else {
options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
}
binding.searchView.imeOptions = options
}
companion object {
private const val EXTRA_SOURCE = "source"

View File

@@ -3,17 +3,28 @@ package org.koitharu.kotatsu.search.ui.suggestion
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import javax.inject.Inject
private const val DEBOUNCE_TIMEOUT = 500L
private const val MAX_MANGA_ITEMS = 6
@@ -30,6 +41,12 @@ class SearchSuggestionViewModel @Inject constructor(
private val query = MutableStateFlow("")
private var suggestionJob: Job? = null
val isIncognitoModeEnabled = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
key = AppSettings.KEY_INCOGNITO_MODE,
valueProducer = { isIncognitoModeEnabled },
)
val suggestion = MutableLiveData<List<SearchSuggestionItem>>()
init {
@@ -41,7 +58,11 @@ class SearchSuggestionViewModel @Inject constructor(
}
fun saveQuery(query: String) {
repository.saveSearchQuery(query)
launchJob(Dispatchers.Default) {
if (!settings.isIncognitoModeEnabled) {
repository.saveSearchQuery(query)
}
}
}
fun clearSearchHistory() {

View File

@@ -30,7 +30,7 @@ class OnboardViewModel @Inject constructor(
init {
if (settings.isSourcesSelected) {
selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x)?.locale })
selectedLocales.removeAll(settings.hiddenSources.mapNotNullToSet { x -> MangaSource(x).locale })
} else {
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
x.language

View File

@@ -0,0 +1,35 @@
package org.koitharu.kotatsu.shelf.domain
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.history.domain.MangaWithHistory
import org.koitharu.kotatsu.parsers.model.Manga
class ShelfContent(
val history: List<MangaWithHistory>,
val favourites: Map<FavouriteCategory, List<Manga>>,
val updated: Map<Manga, Int>,
val local: List<Manga>,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ShelfContent
if (history != other.history) return false
if (favourites != other.favourites) return false
if (updated != other.updated) return false
if (local != other.local) return false
return true
}
override fun hashCode(): Int {
var result = history.hashCode()
result = 31 * result + favourites.hashCode()
result = 31 * result + updated.hashCode()
result = 31 * result + local.hashCode()
return result
}
}

View File

@@ -21,15 +21,26 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
class ShelfRepository @Inject constructor(
private val localMangaRepository: LocalMangaRepository,
private val historyRepository: HistoryRepository,
private val trackingRepository: TrackingRepository,
private val db: MangaDatabase,
) {
fun observeShelfContent(): Flow<ShelfContent> = combine(
historyRepository.observeAllWithHistory(),
observeLocalManga(SortOrder.UPDATED),
observeFavourites(),
trackingRepository.observeUpdatedManga(),
) { history, local, favorites, updated ->
ShelfContent(history, favorites, updated, local)
}
fun observeLocalManga(sortOrder: SortOrder): Flow<List<Manga>> {
return flow {
emit(null)

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.shelf.domain
enum class ShelfSection {
HISTORY, LOCAL, UPDATED, FAVORITES;
}

View File

@@ -6,16 +6,16 @@ import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import androidx.fragment.app.FragmentManager
import com.google.android.material.R as materialR
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.util.*
import java.util.concurrent.TimeUnit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.shelf.ui.config.categories.ShelfCategoriesConfigSheet
import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
import org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity
import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
import org.koitharu.kotatsu.utils.ext.startOfDay
import java.util.Date
import java.util.concurrent.TimeUnit
import com.google.android.material.R as materialR
class ShelfMenuProvider(
private val context: Context,
@@ -33,18 +33,22 @@ class ShelfMenuProvider(
showClearHistoryDialog()
true
}
R.id.action_grid_size -> {
ShelfSizeBottomSheet.show(fragmentManager)
true
}
R.id.action_import -> {
ImportDialogFragment.show(fragmentManager)
true
}
R.id.action_categories -> {
ShelfCategoriesConfigSheet.show(fragmentManager)
context.startActivity(ShelfSettingsActivity.newIntent(context))
true
}
else -> false
}
}

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.os.NetworkStateObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory
@@ -29,8 +30,9 @@ import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.shelf.domain.ShelfContent
import org.koitharu.kotatsu.shelf.domain.ShelfRepository
import org.koitharu.kotatsu.shelf.domain.ShelfSection
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
@@ -44,19 +46,17 @@ class ShelfViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
private val networkStateObserver: NetworkStateObserver,
networkStateObserver: NetworkStateObserver,
) : BaseViewModel(), ListExtraProvider {
val onActionDone = SingleLiveEvent<ReversibleAction>()
val content: LiveData<List<ListModel>> = combine(
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
networkStateObserver,
historyRepository.observeAllWithHistory(),
repository.observeLocalManga(SortOrder.UPDATED),
repository.observeFavourites(),
trackingRepository.observeUpdatedManga(),
) { isConnected, history, local, favourites, updated ->
mapList(history, favourites, updated, local, isConnected)
repository.observeShelfContent(),
) { sections, isConnected, content ->
mapList(content, sections, isConnected)
}.debounce(500)
.catch { e ->
emit(listOf(e.toErrorState(canRetry = false)))
@@ -134,25 +134,19 @@ class ShelfViewModel @Inject constructor(
}
private suspend fun mapList(
history: List<MangaWithHistory>,
favourites: Map<FavouriteCategory, List<Manga>>,
updated: Map<Manga, Int>,
local: List<Manga>,
content: ShelfContent,
sections: List<ShelfSection>,
isNetworkAvailable: Boolean,
): List<ListModel> {
val result = ArrayList<ListModel>(favourites.keys.size + 3)
val result = ArrayList<ListModel>(content.favourites.keys.size + 3)
if (isNetworkAvailable) {
if (history.isNotEmpty()) {
mapHistory(result, history)
}
if (local.isNotEmpty()) {
mapLocal(result, local)
}
if (updated.isNotEmpty()) {
mapUpdated(result, updated)
}
if (favourites.isNotEmpty()) {
mapFavourites(result, favourites)
for (section in sections) {
when (section) {
ShelfSection.HISTORY -> mapHistory(result, content.history)
ShelfSection.LOCAL -> mapLocal(result, content.local)
ShelfSection.UPDATED -> mapUpdated(result, content.updated)
ShelfSection.FAVORITES -> mapFavourites(result, content.favourites)
}
}
} else {
result += EmptyHint(
@@ -161,12 +155,17 @@ class ShelfViewModel @Inject constructor(
textSecondary = R.string.network_unavailable_hint,
actionStringRes = R.string.manage,
)
val offlineHistory = history.filter { it.manga.source == MangaSource.LOCAL }
if (offlineHistory.isNotEmpty()) {
mapHistory(result, offlineHistory)
}
if (local.isNotEmpty()) {
mapLocal(result, local)
for (section in sections) {
when (section) {
ShelfSection.HISTORY -> mapHistory(
result,
content.history.filter { it.manga.source == MangaSource.LOCAL },
)
ShelfSection.LOCAL -> mapLocal(result, content.local)
ShelfSection.UPDATED -> Unit
ShelfSection.FAVORITES -> Unit
}
}
}
if (result.isEmpty()) {
@@ -189,6 +188,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList<in ShelfSectionModel.History>,
list: List<MangaWithHistory>,
) {
if (list.isEmpty()) {
return
}
val showPercent = settings.isReadingIndicatorsEnabled
destination += ShelfSectionModel.History(
items = list.map { (manga, history) ->
@@ -204,6 +206,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList<in ShelfSectionModel.Updated>,
updated: Map<Manga, Int>,
) {
if (updated.isEmpty()) {
return
}
val showPercent = settings.isReadingIndicatorsEnabled
destination += ShelfSectionModel.Updated(
items = updated.map { (manga, counter) ->
@@ -218,6 +223,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList<in ShelfSectionModel.Local>,
local: List<Manga>,
) {
if (local.isEmpty()) {
return
}
destination += ShelfSectionModel.Local(
items = local.toUi(ListMode.GRID, this),
showAllButtonText = R.string.show_all,
@@ -228,6 +236,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList<in ShelfSectionModel.Favourites>,
favourites: Map<FavouriteCategory, List<Manga>>,
) {
if (favourites.isEmpty()) {
return
}
for ((category, list) in favourites) {
if (list.isNotEmpty()) {
destination += ShelfSectionModel.Favourites(

View File

@@ -0,0 +1,101 @@
package org.koitharu.kotatsu.shelf.ui.config
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityShelfSettingsBinding
@AndroidEntryPoint
class ShelfSettingsActivity :
BaseActivity<ActivityShelfSettingsBinding>(),
View.OnClickListener, ShelfSettingsListener {
private val viewModel by viewModels<ShelfSettingsViewModel>()
private lateinit var reorderHelper: ItemTouchHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityShelfSettingsBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(com.google.android.material.R.drawable.abc_ic_clear_material)
}
binding.buttonDone.setOnClickListener(this)
val settingsAdapter = ShelfSettingsAdapter(this)
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = settingsAdapter
reorderHelper = ItemTouchHelper(SectionsReorderCallback()).also {
it.attachToRecyclerView(this)
}
}
viewModel.content.observe(this) { settingsAdapter.items = it }
}
override fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean) {
viewModel.setItemChecked(item, isChecked)
}
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) {
reorderHelper.startDrag(holder)
}
override fun onClick(v: View?) {
finishAfterTransition()
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
binding.recyclerView.updatePadding(
bottom = insets.bottom,
)
binding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
private inner class SectionsReorderCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
0,
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean = viewHolder.itemViewType == target.itemViewType && viewModel.reorderSections(
viewHolder.bindingAdapterPosition,
target.bindingAdapterPosition,
)
override fun canDropOver(
recyclerView: RecyclerView,
current: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder,
): Boolean = current.itemViewType == target.itemViewType
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun isLongPressDragEnabled() = false
}
companion object {
fun newIntent(context: Context) = Intent(context, ShelfSettingsActivity::class.java)
}
}

View File

@@ -0,0 +1,41 @@
package org.koitharu.kotatsu.shelf.ui.config
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
class ShelfSettingsAdapter(
listener: ShelfSettingsListener,
) : AsyncListDifferDelegationAdapter<ShelfSettingsItemModel>(DiffCallback()) {
init {
delegatesManager.addDelegate(shelfCategoryAD(listener))
.addDelegate(shelfSectionAD(listener))
}
class DiffCallback : DiffUtil.ItemCallback<ShelfSettingsItemModel>() {
override fun areItemsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean {
return when {
oldItem is ShelfSettingsItemModel.Section && newItem is ShelfSettingsItemModel.Section -> {
oldItem.section == newItem.section
}
oldItem is ShelfSettingsItemModel.FavouriteCategory && newItem is ShelfSettingsItemModel.FavouriteCategory -> {
oldItem.id == newItem.id
}
else -> false
}
}
override fun areContentsTheSame(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: ShelfSettingsItemModel, newItem: ShelfSettingsItemModel): Any? {
return if (oldItem.isChecked == newItem.isChecked) {
super.getChangePayload(oldItem, newItem)
} else Unit
}
}
}

View File

@@ -0,0 +1,75 @@
package org.koitharu.kotatsu.shelf.ui.config
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
import android.widget.CompoundButton
import androidx.core.view.updatePaddingRelative
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
import org.koitharu.kotatsu.databinding.ItemShelfSectionDraggableBinding
import org.koitharu.kotatsu.shelf.domain.ShelfSection
@SuppressLint("ClickableViewAccessibility")
fun shelfSectionAD(
listener: ShelfSettingsListener,
) =
adapterDelegateViewBinding<ShelfSettingsItemModel.Section, ShelfSettingsItemModel, ItemShelfSectionDraggableBinding>(
{ layoutInflater, parent -> ItemShelfSectionDraggableBinding.inflate(layoutInflater, parent, false) },
) {
val eventListener = object :
View.OnTouchListener,
CompoundButton.OnCheckedChangeListener {
override fun onTouch(v: View?, event: MotionEvent): Boolean {
return if (event.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onDragHandleTouch(this@adapterDelegateViewBinding)
true
} else {
false
}
}
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
listener.onItemCheckedChanged(item, isChecked)
}
}
binding.switchToggle.setOnCheckedChangeListener(eventListener)
binding.imageViewHandle.setOnTouchListener(eventListener)
bind {
binding.textViewTitle.setText(item.section.titleResId)
binding.switchToggle.isChecked = item.isChecked
}
}
fun shelfCategoryAD(
listener: ShelfSettingsListener,
) =
adapterDelegateViewBinding<ShelfSettingsItemModel.FavouriteCategory, ShelfSettingsItemModel, ItemCategoryCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {
itemView.setOnClickListener {
listener.onItemCheckedChanged(item, !item.isChecked)
}
binding.root.updatePaddingRelative(
start = binding.root.paddingStart * 2,
end = binding.root.paddingStart,
)
bind {
binding.root.text = item.title
binding.root.isChecked = item.isChecked
}
}
private val ShelfSection.titleResId: Int
get() = when (this) {
ShelfSection.HISTORY -> R.string.history
ShelfSection.LOCAL -> R.string.local_storage
ShelfSection.UPDATED -> R.string.updated
ShelfSection.FAVORITES -> R.string.favourites
}

View File

@@ -0,0 +1,60 @@
package org.koitharu.kotatsu.shelf.ui.config
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.shelf.domain.ShelfSection
sealed interface ShelfSettingsItemModel : ListModel {
val isChecked: Boolean
class Section(
val section: ShelfSection,
override val isChecked: Boolean,
) : ShelfSettingsItemModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Section
if (section != other.section) return false
if (isChecked != other.isChecked) return false
return true
}
override fun hashCode(): Int {
var result = section.hashCode()
result = 31 * result + isChecked.hashCode()
return result
}
}
class FavouriteCategory(
val id: Long,
val title: String,
override val isChecked: Boolean,
) : ShelfSettingsItemModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FavouriteCategory
if (id != other.id) return false
if (title != other.title) return false
if (isChecked != other.isChecked) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + isChecked.hashCode()
return result
}
}
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.shelf.ui.config
import androidx.recyclerview.widget.RecyclerView
interface ShelfSettingsListener {
fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean)
fun onDragHandleTouch(holder: RecyclerView.ViewHolder)
}

View File

@@ -0,0 +1,101 @@
package org.koitharu.kotatsu.shelf.ui.config
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.shelf.domain.ShelfSection
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.move
import javax.inject.Inject
@HiltViewModel
class ShelfSettingsViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
private val settings: AppSettings,
) : BaseViewModel() {
val content = combine(
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
favouritesRepository.observeCategories(),
) { sections, categories ->
buildList(sections, categories)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
private var updateJob: Job? = null
fun setItemChecked(item: ShelfSettingsItemModel, isChecked: Boolean) {
val prevJob = updateJob
updateJob = launchJob(Dispatchers.Default) {
prevJob?.join()
when (item) {
is ShelfSettingsItemModel.FavouriteCategory -> {
favouritesRepository.updateCategory(item.id, isChecked)
}
is ShelfSettingsItemModel.Section -> {
val sections = settings.shelfSections
settings.shelfSections = if (isChecked) {
sections + item.section
} else {
if (sections.size > 1) {
sections - item.section
} else {
return@launchJob
}
}
}
}
}
}
fun reorderSections(oldPos: Int, newPos: Int): Boolean {
val snapshot = content.value?.toMutableList() ?: return false
snapshot.move(oldPos, newPos)
settings.shelfSections = snapshot.sections()
return true
}
private fun buildList(
sections: List<ShelfSection>,
categories: List<FavouriteCategory>
): List<ShelfSettingsItemModel> {
val result = ArrayList<ShelfSettingsItemModel>()
val sectionsList = ShelfSection.values().toMutableList()
for (section in sections) {
sectionsList.remove(section)
result.addSection(section, true, categories)
}
for (section in sectionsList) {
result.addSection(section, false, categories)
}
return result
}
private fun MutableList<in ShelfSettingsItemModel>.addSection(
section: ShelfSection,
isEnabled: Boolean,
favouriteCategories: List<FavouriteCategory>,
) {
add(ShelfSettingsItemModel.Section(section, isEnabled))
if (isEnabled && section == ShelfSection.FAVORITES) {
favouriteCategories.mapTo(this) {
ShelfSettingsItemModel.FavouriteCategory(
id = it.id,
title = it.title,
isChecked = it.isVisibleInLibrary,
)
}
}
}
private fun List<ShelfSettingsItemModel>.sections(): List<ShelfSection> {
return mapNotNull { (it as? ShelfSettingsItemModel.Section)?.takeIf { x -> x.isChecked }?.section }
}
}

View File

@@ -1,32 +0,0 @@
package org.koitharu.kotatsu.shelf.ui.config.categories
import androidx.recyclerview.widget.DiffUtil
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
class ShelfCategoriesConfigAdapter(
listener: OnListItemClickListener<FavouriteCategory>,
) : AsyncListDifferDelegationAdapter<FavouriteCategory>(DiffCallback()) {
init {
delegatesManager.addDelegate(shelfCategoryAD(listener))
}
class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary && oldItem.title == newItem.title
}
override fun getChangePayload(oldItem: FavouriteCategory, newItem: FavouriteCategory): Any? {
return if (oldItem.isVisibleInLibrary == newItem.isVisibleInLibrary) {
super.getChangePayload(oldItem, newItem)
} else Unit
}
}
}

View File

@@ -1,54 +0,0 @@
package org.koitharu.kotatsu.shelf.ui.config.categories
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.SheetBaseBinding
@AndroidEntryPoint
class ShelfCategoriesConfigSheet :
BaseBottomSheet<SheetBaseBinding>(),
OnListItemClickListener<FavouriteCategory>,
View.OnClickListener {
private val viewModel by viewModels<ShelfCategoriesConfigViewModel>()
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetBaseBinding {
return SheetBaseBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.headerBar.toolbar.setTitle(R.string.favourites_categories)
binding.buttonDone.isVisible = true
binding.buttonDone.setOnClickListener(this)
val adapter = ShelfCategoriesConfigAdapter(this)
binding.recyclerView.adapter = adapter
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
}
override fun onItemClick(item: FavouriteCategory, view: View) {
viewModel.toggleItem(item)
}
override fun onClick(v: View?) {
dismiss()
}
companion object {
private const val TAG = "ShelfCategoriesConfigSheet"
fun show(fm: FragmentManager) = ShelfCategoriesConfigSheet().show(fm, TAG)
}
}

View File

@@ -1,30 +0,0 @@
package org.koitharu.kotatsu.shelf.ui.config.categories
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@HiltViewModel
class ShelfCategoriesConfigViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() {
val content = favouritesRepository.observeCategories()
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
private var updateJob: Job? = null
fun toggleItem(category: FavouriteCategory) {
val prevJob = updateJob
updateJob = launchJob(Dispatchers.Default) {
prevJob?.join()
favouritesRepository.updateCategory(category.id, !category.isVisibleInLibrary)
}
}
}

View File

@@ -1,21 +0,0 @@
package org.koitharu.kotatsu.shelf.ui.config.categories
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
fun shelfCategoryAD(
listener: OnListItemClickListener<FavouriteCategory>,
) = adapterDelegateViewBinding<FavouriteCategory, FavouriteCategory, ItemCategoryCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
itemView.setOnClickListener(eventListener)
bind {
binding.root.text = item.title
binding.root.isChecked = item.isVisibleInLibrary
}
}

View File

@@ -0,0 +1,34 @@
package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import okhttp3.ResponseBody
import org.koitharu.kotatsu.utils.progress.ProgressResponseBody
import java.io.InputStream
import java.io.OutputStream
suspend fun InputStream.copyToSuspending(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE
): Long = withContext(Dispatchers.IO) {
val job = currentCoroutineContext()[Job]
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
job?.ensureActive()
bytes = read(buffer)
job?.ensureActive()
}
bytesCopied
}
fun ResponseBody.withProgress(progressState: MutableStateFlow<Float>): ResponseBody {
return ProgressResponseBody(this, progressState)
}

View File

@@ -0,0 +1,51 @@
package org.koitharu.kotatsu.utils.progress
import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.MediaType
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSource
import okio.ForwardingSource
import okio.Source
import okio.buffer
class ProgressResponseBody(
private val delegate: ResponseBody,
private val progressState: MutableStateFlow<Float>,
) : ResponseBody() {
private var bufferedSource: BufferedSource? = null
override fun close() {
super.close()
delegate.close()
}
override fun contentLength(): Long = delegate.contentLength()
override fun contentType(): MediaType? = delegate.contentType()
override fun source(): BufferedSource {
return bufferedSource ?: ProgressSource(delegate.source(), contentLength(), progressState).buffer().also {
bufferedSource = it
}
}
private class ProgressSource(
delegate: Source,
private val contentLength: Long,
private val progressState: MutableStateFlow<Float>,
) : ForwardingSource(delegate) {
private var totalBytesRead = 0L
override fun read(sink: Buffer, byteCount: Long): Long {
val bytesRead = super.read(sink, byteCount)
if (contentLength > 0) {
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
progressState.value = (totalBytesRead.toDouble() / contentLength.toDouble()).toFloat()
}
return bytesRead
}
}
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
tools:navigationIcon="@drawable/abc_ic_clear_material"
tools:title="@string/settings">
<Button
android:id="@+id/button_done"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:text="@string/done" />
</com.google.android.material.appbar.MaterialToolbar>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="ifContentScrolls"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:gravity="center_vertical"
android:minHeight="58dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/imageView_handle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingHorizontal="?listPreferredItemPaddingStart"
android:scaleType="center"
android:src="@drawable/ic_reorder_handle" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge"
tools:text="@tools:sample/lorem[15]" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="?listPreferredItemPaddingEnd" />
</LinearLayout>

View File

@@ -12,7 +12,7 @@
<item
android:id="@+id/action_categories"
android:orderInCategory="50"
android:title="@string/categories_"
android:title="@string/settings"
app:showAsAction="never" />
<item

View File

@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Kotatsu.DialogWhenLarge">
<item name="windowFixedWidthMajor">@dimen/abc_dialog_fixed_width_major</item>
<item name="windowFixedWidthMinor">@dimen/abc_dialog_fixed_width_minor</item>
<item name="windowFixedHeightMajor">@dimen/abc_dialog_fixed_height_major</item>
<item name="windowFixedHeightMinor">@dimen/abc_dialog_fixed_height_minor</item>
<item name="android:windowElevation">@dimen/abc_floating_window_z</item>
<item name="android:colorBackground">?attr/colorBackgroundFloating</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowTitleStyle">@style/RtlOverlay.DialogWindowTitle.AppCompat</item>
<item name="android:windowTitleBackgroundStyle">@style/Base.DialogWindowTitleBackground.AppCompat</item>
<item name="android:windowBackground">@drawable/abc_dialog_material_background</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowAnimationStyle">@style/Animation.AppCompat.Dialog</item>
<item name="android:windowSoftInputMode">stateUnspecified|adjustResize</item>
<item name="windowActionBar">false</item>
<item name="windowActionModeOverlay">true</item>
<item name="listPreferredItemPaddingLeft">24dip</item>
<item name="listPreferredItemPaddingRight">24dip</item>
<item name="android:listDivider">@null</item>
<item name="android:buttonBarStyle">@style/Widget.AppCompat.ButtonBar.AlertDialog</item>
<item name="android:borderlessButtonStyle">@style/Widget.AppCompat.Button.Borderless</item>
<item name="android:windowCloseOnTouchOutside">true</item>
</style>
</resources>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="pages">
<item quantity="one">Тотално %1$d странa</item>
<item quantity="few">Тотално %1$d странице</item>
<item quantity="other">Тотално %1$d странице</item>
</plurals>
<plurals name="items">
<item quantity="one">%1$d ставке</item>
<item quantity="few">%1$d ставки</item>
<item quantity="other">%1$d ставка</item>
</plurals>
<plurals name="chapters_from_x">
<item quantity="one">%1$d поглавља од %2$d</item>
<item quantity="few">%1$d поглавља од %2$d</item>
<item quantity="other">%1$d поглавља од %2$d</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">пре %1$d минута</item>
<item quantity="few">пре %1$d минута</item>
<item quantity="other">пре %1$d минута</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">пре %1$d сата</item>
<item quantity="few">пре %1$d сата</item>
<item quantity="other">пре %1$d сата</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">пре %1$d дана</item>
<item quantity="few">пре %1$d дана</item>
<item quantity="other">пре %1$d дана</item>
</plurals>
<plurals name="new_chapters">
<item quantity="one">%1$d нова поглавља</item>
<item quantity="few">%1$d нових поглавља</item>
<item quantity="other">%1$d нових поглавља</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d поглављe</item>
<item quantity="few">%1$d поглавља</item>
<item quantity="other">%1$d поглавља</item>
</plurals>
</resources>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="local_storage">Локално складиште</string>
<string name="close_menu">Затвори мени</string>
<string name="error_occurred">Грешка се појавила</string>
<string name="open_menu">Отвори мени</string>
<string name="favourites">Фаворити</string>
<string name="history">Историја</string>
<string name="network_error">Неуспешно повезивање са интернетом</string>
<string name="details">Детаљи</string>
<string name="chapters">Поглавља</string>
<string name="list">Листа</string>
<string name="detailed_list">Детаљна листа</string>
<string name="grid">Табла</string>
</resources>

View File

@@ -266,7 +266,7 @@
<string name="suggestions_excluded_genres">Türleri hariç tut</string>
<string name="suggestions_excluded_genres_summary">Önerilerde görmek istemediğiniz türleri belirtin</string>
<string name="text_delete_local_manga_batch">Seçilen ögeler aygıttan kalıcı olarak silinsin mi\?</string>
<string name="batch_manga_save_confirm">Seçilen tüm mangaları tüm bölümleriyle birlikte indirmek istediğinizden emin misiniz\? Bu işlem çok fazla trafik ve depolama alanı tüketebilir</string>
<string name="batch_manga_save_confirm">Seçilen tüm manga ve bölümleri indirilsin mi\? Bu, çok fazla trafik ve depolama tüketebilir.</string>
<string name="removal_completed">Kaldırma tamamlandı</string>
<string name="chapters_will_removed_background">Bölümler arka planda kaldırılacaktır. Bu biraz zaman alabilir</string>
<string name="parallel_downloads">Paralel indirmeler</string>
@@ -372,7 +372,7 @@
<string name="import_completed">İçe aktarım tamamlandı</string>
<string name="import_completed_hint">Yer açmak için orijinal dosyayı depolamadan silebilirsiniz</string>
<string name="import_will_start_soon">İçe aktarım birazdan başlayacak</string>
<string name="feed">Feed</string>
<string name="feed">Akış</string>
<string name="history_shortcuts">En son manga kısayollarını göster</string>
<string name="reader_control_ltr">Ergonomik okuyucu kontrol</string>
<string name="color_correction">Renk düzeltme</string>
@@ -380,10 +380,19 @@
<string name="contrast">Kontrast</string>
<string name="reset">Sıfırla</string>
<string name="color_correction_hint">Seçilen renk ayarları bu manga için hatırlanacaktır</string>
<string name="text_unsaved_changes_prompt">Kaydedilmemiş değişiklikleriniz var, kaydetmek mi istersiniz yoksa, yoksaymak mı istersiniz\?</string>
<string name="text_unsaved_changes_prompt">Kaydedilmeyen değişiklikler kaydedilsin mi yoksa atılsın mı\?</string>
<string name="discard">Yoksay</string>
<string name="error_no_space_left">Cihazda yer yok</string>
<string name="webtoon_zoom">Webtoon yakınlaştırma</string>
<string name="webtoon_zoom_summary">Webtoon modunda yakınlaştırma hareketine izin ver (beta)</string>
<string name="reader_slider">Sayfa değiştirme kaydırıcısını göster</string>
<string name="clear_new_chapters_counters">Ayrıca yeni bölümler hakkındaki bilgileri temizle</string>
<string name="compact">Sıkı</string>
<string name="different_languages">Farklı diller</string>
<string name="network_unavailable">Ağ kullanılamıyor</string>
<string name="network_unavailable_hint">Çevrim içi manga okumak için Wi-Fi veya mobil ağıın</string>
<string name="server_error">Sunucu tarafı hatası (%1$d). Lütfen daha sonra tekrar deneyin</string>
<string name="saved_manga">Kaydedilen mangalar</string>
<string name="history_shortcuts_summary">Uygulama simgesine uzun basarak son mangaları kullanılabilir hale getirin</string>
<string name="reader_control_ltr_summary">Sağ kenara dokunulduğunda veya sağ tuşa basıldığında her zaman bir sonraki sayfaya geçilir</string>
</resources>

View File

@@ -394,4 +394,5 @@
<string name="webtoon_zoom">Масштабування в режимі вебтуну</string>
<string name="webtoon_zoom_summary">Дозволити жести збільшення/зменшення масштабу в режимі вебтуну (бета)</string>
<string name="reader_slider">Відображати повзунок перемикання сторінок</string>
<string name="compact">Компактно</string>
</resources>

View File

@@ -86,8 +86,6 @@
<!--== Default Theme ==-->
<style name="Theme.Kotatsu" parent="Base.Theme.Kotatsu" />
<style name="Theme.Kotatsu.DialogWhenLarge" />
<!-- Monet theme only support S+ -->
<style name="Theme.Kotatsu.Monet" />

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.EnumSet
class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("localhost", null)
override val sortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java)
override suspend fun getDetails(manga: Manga): Manga = stub()
override suspend fun getList(
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> = stub()
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub()
override suspend fun getTags(): Set<MangaTag> = stub()
private fun stub(): Nothing {
throw NotFoundException("Usage of Dummy parser in release build", "")
}
}

View File

@@ -1,10 +0,0 @@
package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.newParser
fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaParser {
return source.newParser(loaderContext)
}