Merge branch 'devel' into feature/anilist

This commit is contained in:
Koitharu
2022-11-17 19:20:24 +02:00
56 changed files with 831 additions and 435 deletions

View File

@@ -26,7 +26,7 @@ Download APK directly from GitHub:
* Notifications about new chapters with updates feed * Notifications about new chapters with updates feed
* Shikimori integration (manga tracking) * Shikimori integration (manga tracking)
* Password/fingerprint protect access to the app * 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 ### Screenshots

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 502 versionCode 503
versionName '4.0.2' versionName '4.0.3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -85,15 +85,15 @@ afterEvaluate {
} }
} }
dependencies { dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:a1441e7ed7') { implementation('com.github.KotatsuApp:kotatsu-parsers:bf8a1f3db2') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.activity:activity-ktx:1.6.0' implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.fragment:fragment-ktx:1.5.3' implementation 'androidx.fragment:fragment-ktx:1.5.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-service:2.5.1' implementation 'androidx.lifecycle:lifecycle-service:2.5.1'

View File

@@ -28,6 +28,7 @@
android:fullBackupOnly="true" android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
@@ -108,8 +109,7 @@
<activity <activity
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity" android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:exported="true" android:exported="true"
android:label="@string/manga_shelf" android:label="@string/manga_shelf">
android:theme="@style/Theme.Kotatsu.DialogWhenLarge">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter> </intent-filter>
@@ -127,18 +127,18 @@
<activity <activity
android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity" android:name="org.koitharu.kotatsu.download.ui.DownloadsActivity"
android:label="@string/downloads" android:label="@string/downloads"
android:launchMode="singleTop" android:launchMode="singleTop" />
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
<activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" /> <activity android:name="org.koitharu.kotatsu.image.ui.ImageActivity" />
<activity <activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" />
android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity"
android:theme="@style/Theme.Kotatsu.DialogWhenLarge" />
<activity <activity
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity" android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
android:label="@string/sync" /> android:label="@string/sync" />
<activity <activity
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity" android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
android:label="@string/color_correction" /> android:label="@string/color_correction" />
<activity
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
android:label="@string/settings" />
<service <service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService" 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) EntryPointAccessors.fromApplication(this, BaseActivityEntryPoint::class.java).inject(this)
val isAmoled = settings.isAmoledTheme val isAmoled = settings.isAmoledTheme
val isDynamic = settings.isDynamicTheme val isDynamic = settings.isDynamicTheme
// TODO support DialogWhenLarge theme
when { when {
isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled) isAmoled && isDynamic -> setTheme(R.style.Theme_Kotatsu_Monet_Amoled)
isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled) isAmoled -> setTheme(R.style.Theme_Kotatsu_Amoled)

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import org.koitharu.kotatsu.utils.ext.connectivityManager import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
import javax.inject.Inject import javax.inject.Inject
@@ -37,6 +38,13 @@ class NetworkStateObserver @Inject constructor(
} }
} }
suspend fun awaitForConnection(): Unit {
if (value) {
return
}
first { it }
}
private fun observeImpl() = callbackFlow<Boolean> { private fun observeImpl() = callbackFlow<Boolean> {
val request = NetworkRequest.Builder().build() val request = NetworkRequest.Builder().build()
val callback = FlowNetworkCallback(this) val callback = FlowNetworkCallback(this)

View File

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

View File

@@ -16,7 +16,6 @@ import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.shelf.domain.ShelfSection import org.koitharu.kotatsu.shelf.domain.ShelfSection
import org.koitharu.kotatsu.utils.ext.getEnumValue import org.koitharu.kotatsu.utils.ext.getEnumValue
import org.koitharu.kotatsu.utils.ext.observe import org.koitharu.kotatsu.utils.ext.observe
@@ -46,17 +45,20 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val remoteMangaSources: Set<MangaSource> val remoteMangaSources: Set<MangaSource>
get() = Collections.unmodifiableSet(remoteSources) get() = Collections.unmodifiableSet(remoteSources)
var shelfSections: Set<ShelfSection> var shelfSections: List<ShelfSection>
get() { get() {
val raw = prefs.getStringSet(KEY_SHELF_SECTIONS, null) val raw = prefs.getString(KEY_SHELF_SECTIONS, null)
if (raw == null) { val values = enumValues<ShelfSection>()
return EnumSet.allOf(ShelfSection::class.java) if (raw.isNullOrEmpty()) {
return values.toList()
} }
return raw.mapTo(EnumSet.noneOf(ShelfSection::class.java)) { ShelfSection.valueOf(it) } return raw.split('|')
.mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) }
.distinct()
} }
set(value) { set(value) {
val raw = value.mapToSet { it.name } val raw = value.joinToString("|") { it.ordinal.toString() }
prefs.edit { putStringSet(KEY_SHELF_SECTIONS, raw) } prefs.edit { putString(KEY_SHELF_SECTIONS, raw) }
} }
var listMode: ListMode var listMode: ListMode
@@ -353,7 +355,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READER_TAPS_LTR = "reader_taps_ltr" const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_LOCAL_LIST_ORDER = "local_order" const val KEY_LOCAL_LIST_ORDER = "local_order"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom" const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_SHELF_SECTIONS = "shelf_sections" const val KEY_SHELF_SECTIONS = "shelf_sections_2"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

View File

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

View File

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

View File

@@ -3,15 +3,17 @@ package org.koitharu.kotatsu.local.data
import android.content.Context import android.content.Context
import com.tomclaw.cache.DiskLruCache import com.tomclaw.cache.DiskLruCache
import dagger.hilt.android.qualifiers.ApplicationContext 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.File
import java.io.InputStream import java.io.InputStream
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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 @Singleton
class PagesCache @Inject constructor(@ApplicationContext context: Context) { class PagesCache @Inject constructor(@ApplicationContext context: Context) {
@@ -26,42 +28,15 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
return lruCache.get(url)?.takeIfReadable() 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()) val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use { out -> try {
inputStream.copyTo(out) file.outputStream().use { out ->
} inputStream.copyToSuspending(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)
} }
} lruCache.put(url, file)
val res = lruCache.put(url, file) } finally {
file.delete() file.delete()
return res
}
private fun publishProgress(contentLength: Long, bytesCopied: Long, progress: MutableStateFlow<Float>) {
if (contentLength > 0) {
progress.value = (bytesCopied.toDouble() / contentLength.toDouble()).toFloat()
} }
} }
} }

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import androidx.core.graphics.Insets
import androidx.core.util.size import androidx.core.util.size
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@@ -24,6 +25,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenResumed
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS 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.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
viewModel.counters.observe(this, ::onCountersChanged) viewModel.counters.observe(this, ::onCountersChanged)
viewModel.isFeedAvailable.observe(this, ::onFeedAvailabilityChanged) viewModel.isFeedAvailable.observe(this, ::onFeedAvailabilityChanged)
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
} }
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -274,6 +277,16 @@ class MainActivity :
navigationDelegate.setItemVisibility(R.id.nav_feed, isFeedAvailable) 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) { private fun onLoadingStateChanged(isLoading: Boolean) {
binding.fab?.isEnabled = !isLoading binding.fab?.isEnabled = !isLoading
} }
@@ -309,8 +322,13 @@ class MainActivity :
private fun onFirstStart() { private fun onFirstStart() {
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
when { when {
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager) !settings.isSourcesSelected -> whenResumed {
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager) OnboardDialogFragment.showWelcome(supportFragmentManager)
}
settings.newSources.isNotEmpty() -> whenResumed {
NewSourcesDialogFragment.show(supportFragmentManager)
}
} }
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext) 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.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.connectivityManager import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.withProgress
import org.koitharu.kotatsu.utils.progress.ProgressDeferred import org.koitharu.kotatsu.utils.progress.ProgressDeferred
import java.io.File import java.io.File
import java.util.LinkedList import java.util.LinkedList
@@ -66,7 +67,9 @@ class PageLoader @Inject constructor(
override fun close() { override fun close() {
loaderScope.cancel() loaderScope.cancel()
tasks.clear() synchronized(tasks) {
tasks.clear()
}
} }
fun isPrefetchApplicable(): Boolean { fun isPrefetchApplicable(): Boolean {
@@ -103,7 +106,9 @@ class PageLoader @Inject constructor(
return task return task
} }
task = loadPageAsyncImpl(page) task = loadPageAsyncImpl(page)
tasks[page.id] = task synchronized(tasks) {
tasks[page.id] = task
}
return task return task
} }
@@ -135,7 +140,9 @@ class PageLoader @Inject constructor(
while (prefetchQueue.isNotEmpty()) { while (prefetchQueue.isNotEmpty()) {
val page = prefetchQueue.pollFirst() ?: return val page = prefetchQueue.pollFirst() ?: return
if (cache[page.url] == null) { if (cache[page.url] == null) {
tasks[page.id] = loadPageAsyncImpl(page) synchronized(tasks) {
tasks[page.id] = loadPageAsyncImpl(page)
}
return return
} }
} }
@@ -173,9 +180,12 @@ class PageLoader @Inject constructor(
val uri = Uri.parse(pageUrl) val uri = Uri.parse(pageUrl)
return if (uri.scheme == "cbz") { return if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart) ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment) }.use { zip ->
zip.getInputStream(entry).use { runInterruptible(Dispatchers.IO) {
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry)
}.use {
cache.put(pageUrl, it) cache.put(pageUrl, it)
} }
} }
@@ -194,10 +204,8 @@ class PageLoader @Inject constructor(
val body = checkNotNull(response.body) { val body = checkNotNull(response.body) {
"Null response" "Null response"
} }
runInterruptible(Dispatchers.IO) { body.withProgress(progress).byteStream().use {
body.byteStream().use { cache.put(pageUrl, it)
cache.put(pageUrl, it, body.contentLength(), progress)
}
} }
} }
} }

View File

@@ -6,10 +6,6 @@ import android.webkit.MimeTypeMap
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.core.net.toUri import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext 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.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.suspendCancellableCoroutine 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.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.reader.domain.PageLoader 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 MAX_FILENAME_LENGTH = 10
private const val EXTENSION_FALLBACK = "png" private const val EXTENSION_FALLBACK = "png"
@@ -48,12 +49,12 @@ class PageSaveHelper @Inject constructor(
} }
} }
runInterruptible(Dispatchers.IO) { runInterruptible(Dispatchers.IO) {
contentResolver.openOutputStream(destination)?.use { output -> contentResolver.openOutputStream(destination)
pageFile.inputStream().use { input -> }?.use { output ->
input.copyTo(output) pageFile.inputStream().use { input ->
} input.copyToSuspending(output)
} ?: throw IOException("Output stream is null") }
} } ?: throw IOException("Output stream is null")
return destination return destination
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import dagger.hilt.android.AndroidEntryPoint 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.parsers.model.MangaSource
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.utils.ext.showKeyboard import org.koitharu.kotatsu.utils.ext.showKeyboard
import kotlin.text.Typography.dagger
@AndroidEntryPoint @AndroidEntryPoint
class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener { class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQueryTextListener {
@@ -32,6 +32,7 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQuery
} }
val query = intent.getStringExtra(EXTRA_QUERY) val query = intent.getStringExtra(EXTRA_QUERY)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this, this::onIncognitoModeChanged)
with(binding.searchView) { with(binding.searchView) {
queryHint = getString(R.string.search_on_s, source.title) queryHint = getString(R.string.search_on_s, source.title)
setOnQueryTextListener(this@SearchActivity) setOnQueryTextListener(this@SearchActivity)
@@ -72,6 +73,16 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>(), SearchView.OnQuery
override fun onQueryTextChange(newText: String?): Boolean = false 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 { companion object {
private const val EXTRA_SOURCE = "source" 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.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.* import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* 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.BaseViewModel
import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow 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.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import javax.inject.Inject
private const val DEBOUNCE_TIMEOUT = 500L private const val DEBOUNCE_TIMEOUT = 500L
private const val MAX_MANGA_ITEMS = 6 private const val MAX_MANGA_ITEMS = 6
@@ -30,6 +41,12 @@ class SearchSuggestionViewModel @Inject constructor(
private val query = MutableStateFlow("") private val query = MutableStateFlow("")
private var suggestionJob: Job? = null 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>>() val suggestion = MutableLiveData<List<SearchSuggestionItem>>()
init { init {
@@ -41,7 +58,11 @@ class SearchSuggestionViewModel @Inject constructor(
} }
fun saveQuery(query: String) { fun saveQuery(query: String) {
repository.saveSearchQuery(query) launchJob(Dispatchers.Default) {
if (!settings.isIncognitoModeEnabled) {
repository.saveSearchQuery(query)
}
}
} }
fun clearSearchHistory() { fun clearSearchHistory() {

View File

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

View File

@@ -10,7 +10,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.local.ui.ImportDialogFragment import org.koitharu.kotatsu.local.ui.ImportDialogFragment
import org.koitharu.kotatsu.shelf.ui.config.ShelfConfigSheet import org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity
import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
import org.koitharu.kotatsu.utils.ext.startOfDay import org.koitharu.kotatsu.utils.ext.startOfDay
import java.util.Date import java.util.Date
@@ -45,7 +45,7 @@ class ShelfMenuProvider(
} }
R.id.action_categories -> { R.id.action_categories -> {
ShelfConfigSheet.show(fragmentManager) context.startActivity(ShelfSettingsActivity.newIntent(context))
true true
} }

View File

@@ -135,22 +135,18 @@ class ShelfViewModel @Inject constructor(
private suspend fun mapList( private suspend fun mapList(
content: ShelfContent, content: ShelfContent,
sections: Set<ShelfSection>, sections: List<ShelfSection>,
isNetworkAvailable: Boolean, isNetworkAvailable: Boolean,
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(content.favourites.keys.size + 3) val result = ArrayList<ListModel>(content.favourites.keys.size + 3)
if (isNetworkAvailable) { if (isNetworkAvailable) {
if (content.history.isNotEmpty() && ShelfSection.HISTORY in sections) { for (section in sections) {
mapHistory(result, content.history) when (section) {
} ShelfSection.HISTORY -> mapHistory(result, content.history)
if (content.local.isNotEmpty() && ShelfSection.LOCAL in sections) { ShelfSection.LOCAL -> mapLocal(result, content.local)
mapLocal(result, content.local) ShelfSection.UPDATED -> mapUpdated(result, content.updated)
} ShelfSection.FAVORITES -> mapFavourites(result, content.favourites)
if (content.updated.isNotEmpty() && ShelfSection.UPDATED in sections) { }
mapUpdated(result, content.updated)
}
if (content.favourites.isNotEmpty() && ShelfSection.FAVORITES in sections) {
mapFavourites(result, content.favourites)
} }
} else { } else {
result += EmptyHint( result += EmptyHint(
@@ -159,12 +155,17 @@ class ShelfViewModel @Inject constructor(
textSecondary = R.string.network_unavailable_hint, textSecondary = R.string.network_unavailable_hint,
actionStringRes = R.string.manage, actionStringRes = R.string.manage,
) )
val offlineHistory = content.history.filter { it.manga.source == MangaSource.LOCAL } for (section in sections) {
if (offlineHistory.isNotEmpty() && ShelfSection.HISTORY in sections) { when (section) {
mapHistory(result, offlineHistory) ShelfSection.HISTORY -> mapHistory(
} result,
if (content.local.isNotEmpty() && ShelfSection.LOCAL in sections) { content.history.filter { it.manga.source == MangaSource.LOCAL },
mapLocal(result, content.local) )
ShelfSection.LOCAL -> mapLocal(result, content.local)
ShelfSection.UPDATED -> Unit
ShelfSection.FAVORITES -> Unit
}
} }
} }
if (result.isEmpty()) { if (result.isEmpty()) {
@@ -187,6 +188,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList<in ShelfSectionModel.History>, destination: MutableList<in ShelfSectionModel.History>,
list: List<MangaWithHistory>, list: List<MangaWithHistory>,
) { ) {
if (list.isEmpty()) {
return
}
val showPercent = settings.isReadingIndicatorsEnabled val showPercent = settings.isReadingIndicatorsEnabled
destination += ShelfSectionModel.History( destination += ShelfSectionModel.History(
items = list.map { (manga, history) -> items = list.map { (manga, history) ->
@@ -202,6 +206,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList<in ShelfSectionModel.Updated>, destination: MutableList<in ShelfSectionModel.Updated>,
updated: Map<Manga, Int>, updated: Map<Manga, Int>,
) { ) {
if (updated.isEmpty()) {
return
}
val showPercent = settings.isReadingIndicatorsEnabled val showPercent = settings.isReadingIndicatorsEnabled
destination += ShelfSectionModel.Updated( destination += ShelfSectionModel.Updated(
items = updated.map { (manga, counter) -> items = updated.map { (manga, counter) ->
@@ -216,6 +223,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList<in ShelfSectionModel.Local>, destination: MutableList<in ShelfSectionModel.Local>,
local: List<Manga>, local: List<Manga>,
) { ) {
if (local.isEmpty()) {
return
}
destination += ShelfSectionModel.Local( destination += ShelfSectionModel.Local(
items = local.toUi(ListMode.GRID, this), items = local.toUi(ListMode.GRID, this),
showAllButtonText = R.string.show_all, showAllButtonText = R.string.show_all,
@@ -226,6 +236,9 @@ class ShelfViewModel @Inject constructor(
destination: MutableList<in ShelfSectionModel.Favourites>, destination: MutableList<in ShelfSectionModel.Favourites>,
favourites: Map<FavouriteCategory, List<Manga>>, favourites: Map<FavouriteCategory, List<Manga>>,
) { ) {
if (favourites.isEmpty()) {
return
}
for ((category, list) in favourites) { for ((category, list) in favourites) {
if (list.isNotEmpty()) { if (list.isNotEmpty()) {
destination += ShelfSectionModel.Favourites( destination += ShelfSectionModel.Favourites(

View File

@@ -1,51 +0,0 @@
package org.koitharu.kotatsu.shelf.ui.config
import androidx.core.view.updatePaddingRelative
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
import org.koitharu.kotatsu.shelf.domain.ShelfSection
fun shelfSectionAD(
listener: OnListItemClickListener<ShelfConfigModel>,
) = adapterDelegateViewBinding<ShelfConfigModel.Section, ShelfConfigModel, ItemCategoryCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
itemView.setOnClickListener(eventListener)
bind {
binding.root.setText(item.section.titleResId)
binding.root.isChecked = item.isChecked
}
}
fun shelfCategoryAD(
listener: OnListItemClickListener<ShelfConfigModel>,
) =
adapterDelegateViewBinding<ShelfConfigModel.FavouriteCategory, ShelfConfigModel, ItemCategoryCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, listener)
itemView.setOnClickListener(eventListener)
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

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

View File

@@ -1,53 +0,0 @@
package org.koitharu.kotatsu.shelf.ui.config
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.databinding.SheetBaseBinding
@AndroidEntryPoint
class ShelfConfigSheet :
BaseBottomSheet<SheetBaseBinding>(),
OnListItemClickListener<ShelfConfigModel>,
View.OnClickListener {
private val viewModel by viewModels<ShelfConfigViewModel>()
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.setTitle(R.string.settings)
binding.buttonDone.isVisible = true
binding.buttonDone.setOnClickListener(this)
val adapter = ShelfConfigAdapter(this)
binding.recyclerView.adapter = adapter
viewModel.content.observe(viewLifecycleOwner) { adapter.items = it }
}
override fun onItemClick(item: ShelfConfigModel, view: View) {
viewModel.toggleItem(item)
}
override fun onClick(v: View?) {
dismiss()
}
companion object {
private const val TAG = "ShelfCategoriesConfigSheet"
fun show(fm: FragmentManager) = ShelfConfigSheet().show(fm, TAG)
}
}

View File

@@ -1,74 +0,0 @@
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 javax.inject.Inject
@HiltViewModel
class ShelfConfigViewModel @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 toggleItem(item: ShelfConfigModel) {
val prevJob = updateJob
updateJob = launchJob(Dispatchers.Default) {
prevJob?.join()
when (item) {
is ShelfConfigModel.FavouriteCategory -> {
favouritesRepository.updateCategory(item.id, !item.isChecked)
}
is ShelfConfigModel.Section -> {
val sections = settings.shelfSections
settings.shelfSections = if (item.isChecked) {
if (sections.size > 1) {
sections - item.section
} else {
return@launchJob
}
} else {
sections + item.section
}
}
}
}
}
private fun buildList(sections: Set<ShelfSection>, categories: List<FavouriteCategory>): List<ShelfConfigModel> {
val result = ArrayList<ShelfConfigModel>()
for (section in ShelfSection.values()) {
val isEnabled = section in sections
result.add(ShelfConfigModel.Section(section, isEnabled))
if (section == ShelfSection.FAVORITES && isEnabled) {
categories.mapTo(result) {
ShelfConfigModel.FavouriteCategory(
id = it.id,
title = it.title,
isChecked = it.isVisibleInLibrary,
)
}
}
}
return result
}
}

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

@@ -3,14 +3,14 @@ package org.koitharu.kotatsu.shelf.ui.config
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.shelf.domain.ShelfSection import org.koitharu.kotatsu.shelf.domain.ShelfSection
sealed interface ShelfConfigModel : ListModel { sealed interface ShelfSettingsItemModel : ListModel {
val isChecked: Boolean val isChecked: Boolean
class Section( class Section(
val section: ShelfSection, val section: ShelfSection,
override val isChecked: Boolean, override val isChecked: Boolean,
) : ShelfConfigModel { ) : ShelfSettingsItemModel {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@@ -35,7 +35,7 @@ sealed interface ShelfConfigModel : ListModel {
val id: Long, val id: Long,
val title: String, val title: String,
override val isChecked: Boolean, override val isChecked: Boolean,
) : ShelfConfigModel { ) : ShelfSettingsItemModel {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

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

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

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

@@ -266,7 +266,7 @@
<string name="suggestions_excluded_genres">Türleri hariç tut</string> <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="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="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="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="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> <string name="parallel_downloads">Paralel indirmeler</string>
@@ -372,7 +372,7 @@
<string name="import_completed">İçe aktarım tamamlandı</string> <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_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="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="history_shortcuts">En son manga kısayollarını göster</string>
<string name="reader_control_ltr">Ergonomik okuyucu kontrol</string> <string name="reader_control_ltr">Ergonomik okuyucu kontrol</string>
<string name="color_correction">Renk düzeltme</string> <string name="color_correction">Renk düzeltme</string>
@@ -380,10 +380,19 @@
<string name="contrast">Kontrast</string> <string name="contrast">Kontrast</string>
<string name="reset">Sıfırla</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="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="discard">Yoksay</string>
<string name="error_no_space_left">Cihazda yer yok</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">Webtoon yakınlaştırma</string>
<string name="webtoon_zoom_summary">Webtoon modunda yakınlaştırma hareketine izin ver (beta)</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="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> </resources>

View File

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

View File

@@ -86,8 +86,6 @@
<!--== Default Theme ==--> <!--== Default Theme ==-->
<style name="Theme.Kotatsu" parent="Base.Theme.Kotatsu" /> <style name="Theme.Kotatsu" parent="Base.Theme.Kotatsu" />
<style name="Theme.Kotatsu.DialogWhenLarge" />
<!-- Monet theme only support S+ --> <!-- Monet theme only support S+ -->
<style name="Theme.Kotatsu.Monet" /> <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)
}