Compare commits

...

31 Commits

Author SHA1 Message Date
Koitharu
8a26587250 Database migration for downgrade from 24 to 23 (#1269, #1270) 2025-02-09 10:15:24 +02:00
Koitharu
bb68869fe1 Fix crashes 2025-02-09 10:11:24 +02:00
Koitharu
e60ca7115a Update parsers 2025-02-09 10:01:28 +02:00
Koitharu
e03a200c32 Fix build 2025-01-25 12:51:13 +02:00
Koitharu
8713faa487 Update parsers 2025-01-25 12:14:01 +02:00
Koitharu
15e99c03a9 Update parsers and adjust imports
(cherry picked from commit 5e8aa4cec7)
2025-01-22 20:21:43 +02:00
Koitharu
b425f3e779 Fix nullability for ParcelableManga
(cherry picked from commit b8b601821a)
2025-01-19 12:00:38 +02:00
Koitharu
c6a51d4d08 Increase version 2025-01-19 11:56:26 +02:00
Koitharu
503bff292c Made SyncAuthActivity exported
(cherry picked from commit 663602282a)
2025-01-19 08:28:02 +02:00
Koitharu
0aa78c0d7e Adjust manga fields nullability 2025-01-19 08:24:25 +02:00
Koitharu
8e1d02f356 Update parsers 2025-01-19 08:01:09 +02:00
Koitharu
1e90d5541b Update parsers 2025-01-11 15:09:21 +02:00
Koitharu
04c7ca7291 Improve local manga chapter names
(cherry picked from commit dddb00d5ef)
2025-01-11 14:59:38 +02:00
Koitharu
8d52cab6d8 Fix manga importing
(cherry picked from commit dcb92ed1af)
2025-01-11 14:59:33 +02:00
Koitharu
efa13df106 Fix crashes 2025-01-11 14:59:25 +02:00
Koitharu
8bc29ac331 Fix local chapters deletion
(cherry picked from commit 25eb05d305)
2025-01-11 14:58:53 +02:00
Koitharu
7991f9ca97 Skip description for ParcelableManga
(cherry picked from commit bf217b3cbf)
2025-01-11 14:58:44 +02:00
Koitharu
eb1eee1681 Fix pages cache usage
(cherry picked from commit 9e2b60e15e)
2025-01-11 14:57:56 +02:00
Koitharu
b3f748c000 Fix crashes
(cherry picked from commit 4dba90361c)
2025-01-11 14:57:49 +02:00
Koitharu
58a9f7b25a Fix settings menu
(cherry picked from commit c51218240e)
2025-01-11 14:56:13 +02:00
Koitharu
fc1d704f6f Fix build 2025-01-01 14:24:43 +02:00
Koitharu
c2c3b0f757 Fix details cover corners 2025-01-01 14:00:04 +02:00
Koitharu
8d519dd80f Fix settings search 2025-01-01 13:59:59 +02:00
Koitharu
3b5a9cd2b4 Skip non-existing local chapters 2025-01-01 13:59:54 +02:00
Koitharu
95f4d39893 Update parsers 2025-01-01 13:53:17 +02:00
Koitharu
f3f269c7fa Fix NPE in SyncSettings 2024-12-30 10:01:08 +02:00
Koitharu
40f262b0ef Update parsers 2024-12-19 17:28:12 +02:00
Koitharu
0f68be9663 Use advanced bitmap decoder for MangaLoaderContext 2024-12-19 17:10:01 +02:00
Koitharu
0b8afe9c40 Fix checking for new chapters in some cases (#1212, #1195, #1190) 2024-12-18 18:26:27 +02:00
Koitharu
754ccc4197 Added url for NoDataReceivedException 2024-12-18 16:26:49 +02:00
Koitharu
ef691b1aed Update parsers 2024-12-18 15:48:57 +02:00
95 changed files with 379 additions and 300 deletions

View File

@@ -18,8 +18,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 35 targetSdk = 35
versionCode = 694 versionCode = 702
versionName = '7.7.2' versionName = '7.7.10'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {

View File

@@ -46,7 +46,7 @@
android:allowBackup="true" android:allowBackup="true"
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent" android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
android:dataExtractionRules="@xml/backup_rules" android:dataExtractionRules="@xml/backup_rules"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="@bool/is_predictive_back_enabled"
android:fullBackupContent="@xml/backup_content" android:fullBackupContent="@xml/backup_content"
android:fullBackupOnly="true" android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@@ -209,6 +209,7 @@
<activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" /> <activity android:name="org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity" />
<activity <activity
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity" android:name="org.koitharu.kotatsu.sync.ui.SyncAuthActivity"
android:exported="true"
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"
@@ -279,6 +280,10 @@
<service <service
android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService" android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService"
android:label="@string/local_manga_processing" /> android:label="@string/local_manga_processing" />
<service
android:name="org.koitharu.kotatsu.local.ui.ImportService"
android:foregroundServiceType="dataSync"
android:label="@string/importing_manga" />
<service <service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:label="@string/manga_shelf" android:label="@string/manga_shelf"

View File

@@ -8,13 +8,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.almostEquals
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaParserSource
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.almostEquals
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject

View File

@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
@@ -25,6 +26,7 @@ class BookmarksAdapter(
init { init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener)) addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener)) addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(null))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD()) addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null)) addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))

View File

@@ -36,6 +36,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration20To21 import org.koitharu.kotatsu.core.db.migrations.Migration20To21
import org.koitharu.kotatsu.core.db.migrations.Migration21To22 import org.koitharu.kotatsu.core.db.migrations.Migration21To22
import org.koitharu.kotatsu.core.db.migrations.Migration22To23 import org.koitharu.kotatsu.core.db.migrations.Migration22To23
import org.koitharu.kotatsu.core.db.migrations.Migration24To23
import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5 import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -128,6 +129,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration20To21(), Migration20To21(),
Migration21To22(), Migration21To22(),
Migration22To23(), Migration22To23(),
Migration24To23(),
) )
fun MangaDatabase(context: Context): MangaDatabase = Room fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -1,11 +1,11 @@
package org.koitharu.kotatsu.core.db.entity package org.koitharu.kotatsu.core.db.entity
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
@@ -49,7 +49,7 @@ fun Manga.toEntity() = MangaEntity(
publicUrl = publicUrl, publicUrl = publicUrl,
source = source.name, source = source.name,
largeCoverUrl = largeCoverUrl, largeCoverUrl = largeCoverUrl,
coverUrl = coverUrl, coverUrl = coverUrl.orEmpty(),
altTitle = altTitle, altTitle = altTitle,
rating = rating, rating = rating,
isNsfw = isNsfw, isNsfw = isNsfw,

View File

@@ -14,7 +14,7 @@ data class MangaEntity(
@ColumnInfo(name = "url") val url: String, @ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "public_url") val publicUrl: String, @ColumnInfo(name = "public_url") val publicUrl: String,
@ColumnInfo(name = "rating") val rating: Float, // normalized value [0..1] or -1 @ColumnInfo(name = "rating") val rating: Float, // normalized value [0..1] or -1
@ColumnInfo(name = "nsfw") val isNsfw: Boolean, @ColumnInfo(name = "nsfw") val isNsfw: Boolean, // TODO change to contentRating
@ColumnInfo(name = "cover_url") val coverUrl: String, @ColumnInfo(name = "cover_url") val coverUrl: String,
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?, @ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
@ColumnInfo(name = "state") val state: String?, @ColumnInfo(name = "state") val state: String?,

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration24To23 : Migration(24, 23) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS `chapters`")
}
}

View File

@@ -1,3 +1,5 @@
package org.koitharu.kotatsu.core.exceptions package org.koitharu.kotatsu.core.exceptions
class CaughtException(cause: Throwable) : RuntimeException("${cause.javaClass.simpleName}(${cause.message})", cause) class CaughtException(
override val cause: Throwable
) : RuntimeException("${cause.javaClass.simpleName}(${cause.message})", cause)

View File

@@ -3,5 +3,5 @@ package org.koitharu.kotatsu.core.exceptions
import okio.IOException import okio.IOException
class NoDataReceivedException( class NoDataReceivedException(
url: String, val url: String,
) : IOException("No data has been received from $url") ) : IOException("No data has been received from $url")

View File

@@ -0,0 +1,5 @@
package org.koitharu.kotatsu.core.exceptions
import okio.IOException
class WrapperIOException(override val cause: Exception) : IOException(cause)

View File

@@ -1,6 +1,6 @@
package org.koitharu.kotatsu.core.github package org.koitharu.kotatsu.core.github
import org.koitharu.kotatsu.core.util.ext.digits import org.koitharu.kotatsu.parsers.util.digits
import java.util.Locale import java.util.Locale
data class VersionId( data class VersionId(

View File

@@ -5,6 +5,7 @@ import android.graphics.BitmapFactory
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
import android.os.Build import android.os.Build
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@@ -32,19 +33,21 @@ object BitmapDecoderCompat {
} }
@Blocking @Blocking
fun decode(stream: InputStream, type: MediaType?): Bitmap { fun decode(stream: InputStream, type: MediaType?, isMutable: Boolean = false): Bitmap {
val format = type?.subtype val format = type?.subtype
if (format == FORMAT_AVIF) { if (format == FORMAT_AVIF) {
return decodeAvif(stream.toByteBuffer()) return decodeAvif(stream.toByteBuffer())
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return checkBitmapNotNull(BitmapFactory.decodeStream(stream), format) val opts = BitmapFactory.Options()
opts.inMutable = isMutable
return checkBitmapNotNull(BitmapFactory.decodeStream(stream, null, opts), format)
} }
val byteBuffer = stream.toByteBuffer() val byteBuffer = stream.toByteBuffer()
return if (AvifDecoder.isAvifImage(byteBuffer)) { return if (AvifDecoder.isAvifImage(byteBuffer)) {
decodeAvif(byteBuffer) decodeAvif(byteBuffer)
} else { } else {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(byteBuffer)) ImageDecoder.decodeBitmap(ImageDecoder.createSource(byteBuffer), DecoderConfigListener(isMutable))
} }
} }
@@ -74,4 +77,18 @@ object BitmapDecoderCompat {
} }
return bitmap return bitmap
} }
@RequiresApi(Build.VERSION_CODES.P)
private class DecoderConfigListener(
private val isMutable: Boolean,
) : ImageDecoder.OnHeaderDecodedListener {
override fun onHeaderDecoded(
decoder: ImageDecoder,
info: ImageDecoder.ImageInfo,
source: ImageDecoder.Source
) {
decoder.isMutableRequired = isMutable
}
}
} }

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.core.os.ParcelCompat
import kotlinx.parcelize.Parceler import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
@@ -13,6 +12,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
@Parcelize @Parcelize
data class ParcelableManga( data class ParcelableManga(
val manga: Manga, val manga: Manga,
private val withDescription: Boolean = true,
) : Parcelable { ) : Parcelable {
companion object : Parceler<ParcelableManga> { companion object : Parceler<ParcelableManga> {
@@ -24,10 +24,10 @@ data class ParcelableManga(
parcel.writeString(url) parcel.writeString(url)
parcel.writeString(publicUrl) parcel.writeString(publicUrl)
parcel.writeFloat(rating) parcel.writeFloat(rating)
ParcelCompat.writeBoolean(parcel, isNsfw) parcel.writeSerializable(contentRating)
parcel.writeString(coverUrl) parcel.writeString(coverUrl)
parcel.writeString(largeCoverUrl) parcel.writeString(largeCoverUrl)
parcel.writeString(description) parcel.writeString(description.takeIf { withDescription })
parcel.writeParcelable(ParcelableMangaTags(tags), flags) parcel.writeParcelable(ParcelableMangaTags(tags), flags)
parcel.writeSerializable(state) parcel.writeSerializable(state)
parcel.writeString(author) parcel.writeString(author)
@@ -42,8 +42,8 @@ data class ParcelableManga(
url = requireNotNull(parcel.readString()), url = requireNotNull(parcel.readString()),
publicUrl = requireNotNull(parcel.readString()), publicUrl = requireNotNull(parcel.readString()),
rating = parcel.readFloat(), rating = parcel.readFloat(),
isNsfw = ParcelCompat.readBoolean(parcel), contentRating = parcel.readSerializableCompat(),
coverUrl = requireNotNull(parcel.readString()), coverUrl = parcel.readString(),
largeCoverUrl = parcel.readString(), largeCoverUrl = parcel.readString(),
description = parcel.readString(), description = parcel.readString(),
tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags, tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags,
@@ -52,6 +52,7 @@ data class ParcelableManga(
chapters = null, chapters = null,
source = MangaSource(parcel.readString()), source = MangaSource(parcel.readString()),
), ),
withDescription = true,
) )
} }
} }

View File

@@ -35,7 +35,7 @@ class AppProxySelector(
if (type == Proxy.Type.DIRECT) { if (type == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY return Proxy.NO_PROXY
} }
if (address.isNullOrEmpty() || port == 0) { if (address.isNullOrEmpty() || port < 0 || port > 0xFFFF) {
throw ProxyConfigException() throw ProxyConfigException()
} }
cachedProxy?.let { cachedProxy?.let {

View File

@@ -1,19 +1,26 @@
package org.koitharu.kotatsu.core.network package org.koitharu.kotatsu.core.network
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MultipartBody
import okhttp3.Response import okhttp3.Response
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.core.exceptions.WrapperIOException
import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING
class GZipInterceptor : Interceptor { class GZipInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response = try {
val newRequest = chain.request().newBuilder() val request = chain.request()
newRequest.addHeader(CONTENT_ENCODING, "gzip") if (request.body is MultipartBody) {
return try { chain.proceed(request)
} else {
val newRequest = request.newBuilder()
newRequest.addHeader(CONTENT_ENCODING, "gzip")
chain.proceed(newRequest.build()) chain.proceed(newRequest.build())
} catch (e: NullPointerException) {
throw IOException(e)
} }
} catch (e: IOException) {
throw e
} catch (e: Exception) {
throw WrapperIOException(e)
} }
} }

View File

@@ -5,7 +5,7 @@ import androidx.annotation.WorkerThread
import androidx.core.util.Predicate import androidx.core.util.Predicate
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.util.ext.newBuilder import org.koitharu.kotatsu.parsers.util.newBuilder
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine

View File

@@ -17,8 +17,8 @@ import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.isHttpOrHttps
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.util.Collections import java.util.Collections

View File

@@ -6,13 +6,13 @@ import dagger.Reusable
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.UnknownMangaSource import org.koitharu.kotatsu.core.model.UnknownMangaSource
import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.almostEquals import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject

View File

@@ -2,12 +2,10 @@ package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory
import android.util.Base64 import android.util.Base64
import android.webkit.WebView import android.webkit.WebView
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -17,6 +15,7 @@ import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.ResponseBody.Companion.asResponseBody
import okio.Buffer import okio.Buffer
import org.koitharu.kotatsu.core.image.BitmapDecoderCompat
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
@@ -31,7 +30,6 @@ import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.map import org.koitharu.kotatsu.parsers.util.map
import org.koitharu.kotatsu.parsers.util.mimeType
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@@ -80,15 +78,13 @@ class MangaLoaderContextImpl @Inject constructor(
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response { override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
return response.map { body -> return response.map { body ->
val opts = BitmapFactory.Options() BitmapDecoderCompat.decode(body.byteStream(), body.contentType(), isMutable = true).use { bitmap ->
opts.inMutable = true
BitmapFactory.decodeStream(body.byteStream(), null, opts)?.use { bitmap ->
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result -> (redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
Buffer().also { Buffer().also {
result.compressTo(it.outputStream()) result.compressTo(it.outputStream())
}.asResponseBody("image/jpeg".toMediaType()) }.asResponseBody("image/jpeg".toMediaType())
} }
} ?: throw ImageDecodeException(response.request.url.toString(), response.mimeType) }
} }
} }

View File

@@ -7,8 +7,6 @@ import androidx.collection.ArraySet
import androidx.core.net.toUri import androidx.core.net.toUri
import org.jetbrains.annotations.Blocking import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Demographic import org.koitharu.kotatsu.parsers.model.Demographic
@@ -22,6 +20,7 @@ import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.find import org.koitharu.kotatsu.parsers.util.find
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.splitTwoParts import org.koitharu.kotatsu.parsers.util.splitTwoParts
import java.util.EnumSet import java.util.EnumSet
@@ -82,7 +81,7 @@ class ExternalPluginContentSource(
publicUrl = details.publicUrl.ifEmpty { manga.publicUrl }, publicUrl = details.publicUrl.ifEmpty { manga.publicUrl },
rating = maxOf(details.rating, manga.rating), rating = maxOf(details.rating, manga.rating),
isNsfw = details.isNsfw, isNsfw = details.isNsfw,
coverUrl = details.coverUrl.ifEmpty { manga.coverUrl }, coverUrl = details.coverUrl.ifNullOrEmpty { manga.coverUrl },
tags = details.tags + manga.tags, tags = details.tags + manga.tags,
state = details.state ?: manga.state, state = details.state ?: manga.state,
author = details.author.ifNullOrEmpty { manga.author }, author = details.author.ifNullOrEmpty { manga.author },

View File

@@ -30,6 +30,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.find import org.koitharu.kotatsu.parsers.util.find
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import java.io.File import java.io.File
import java.net.Proxy import java.net.Proxy
@@ -412,10 +413,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getString(KEY_PROXY_PORT, null)?.toIntOrNull() ?: 0 get() = prefs.getString(KEY_PROXY_PORT, null)?.toIntOrNull() ?: 0
val proxyLogin: String? val proxyLogin: String?
get() = prefs.getString(KEY_PROXY_LOGIN, null)?.takeUnless { it.isEmpty() } get() = prefs.getString(KEY_PROXY_LOGIN, null)?.nullIfEmpty()
val proxyPassword: String? val proxyPassword: String?
get() = prefs.getString(KEY_PROXY_PASSWORD, null)?.takeUnless { it.isEmpty() } get() = prefs.getString(KEY_PROXY_PASSWORD, null)?.nullIfEmpty()
var localListOrder: SortOrder var localListOrder: SortOrder
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST) get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)

View File

@@ -4,13 +4,14 @@ import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.core.content.edit import androidx.core.content.edit
import org.koitharu.kotatsu.core.util.ext.getEnumValue import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.putEnumValue import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
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.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.settings.utils.validation.DomainValidator import org.koitharu.kotatsu.settings.utils.validation.DomainValidator
class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig { class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig {
@@ -38,7 +39,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue) is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue) is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
is ConfigKey.PreferredImageServer -> prefs.getString(key.key, key.defaultValue)?.takeUnless(String::isEmpty) is ConfigKey.PreferredImageServer -> prefs.getString(key.key, key.defaultValue)?.nullIfEmpty()
} as T } as T
} }

View File

@@ -4,21 +4,8 @@ import androidx.collection.ArrayMap
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.collection.LongSet import androidx.collection.LongSet
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import java.util.Collections
import java.util.EnumSet import java.util.EnumSet
inline fun <T> MutableSet(size: Int, init: (index: Int) -> T): MutableSet<T> {
val set = ArraySet<T>(size)
repeat(size) { index -> set.add(init(index)) }
return set
}
inline fun <T> Set(size: Int, init: (index: Int) -> T): Set<T> = when (size) {
0 -> emptySet()
1 -> Collections.singleton(init(0))
else -> MutableSet(size, init)
}
fun <T> Collection<T>.asArrayList(): ArrayList<T> = if (this is ArrayList<*>) { fun <T> Collection<T>.asArrayList(): ArrayList<T> = if (this is ArrayList<*>) {
this as ArrayList<T> this as ArrayList<T>
} else { } else {
@@ -76,15 +63,6 @@ fun <T> Iterable<T>.sortedWithSafe(comparator: Comparator<in T>): List<T> = try
} }
} }
fun Collection<*>?.sizeOrZero() = this?.size ?: 0
@Suppress("UNCHECKED_CAST")
inline fun <T, reified R> Collection<T>.mapToArray(transform: (T) -> R): Array<R> {
val result = arrayOfNulls<R>(size)
forEachIndexed { index, t -> result[index] = transform(t) }
return result as Array<R>
}
fun LongSet.toLongArray(): LongArray { fun LongSet.toLongArray(): LongArray {
val result = LongArray(size) val result = LongArray(size)
var i = 0 var i = 0

View File

@@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.storage.StorageManager import android.os.storage.StorageManager
import android.provider.DocumentsContract import android.provider.DocumentsContract
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.removeSuffix import org.koitharu.kotatsu.parsers.util.removeSuffix
import java.io.File import java.io.File
import java.lang.reflect.Array as ArrayReflect import java.lang.reflect.Array as ArrayReflect
@@ -80,7 +81,7 @@ private fun getVolumePathForAndroid11AndAbove(volumeId: String, context: Context
private fun getVolumeIdFromTreeUri(treeUri: Uri): String? { private fun getVolumeIdFromTreeUri(treeUri: Uri): String? {
val docId = DocumentsContract.getTreeDocumentId(treeUri) val docId = DocumentsContract.getTreeDocumentId(treeUri)
val split = docId.split(":".toRegex()) val split = docId.split(":".toRegex())
return split.firstOrNull()?.takeUnless { it.isEmpty() } return split.firstOrNull()?.nullIfEmpty()
} }
private fun getDocumentPathFromTreeUri(treeUri: Uri): String? { private fun getDocumentPathFromTreeUri(treeUri: Uri): String? {

View File

@@ -1,7 +1,5 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
@@ -27,12 +25,6 @@ fun Response.parseJsonOrNull(): JSONObject? {
} }
} }
val HttpUrl.isHttpOrHttps: Boolean
get() {
val s = scheme.lowercase()
return s == "https" || s == "http"
}
fun Response.ensureSuccess() = apply { fun Response.ensureSuccess() = apply {
if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) { if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) {
closeQuietly() closeQuietly()
@@ -40,26 +32,6 @@ fun Response.ensureSuccess() = apply {
} }
} }
fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
c.name(name)
c.value(value)
if (persistent) {
c.expiresAt(expiresAt)
}
if (hostOnly) {
c.hostOnlyDomain(domain)
} else {
c.domain(domain)
}
c.path(path)
if (secure) {
c.secure()
}
if (httpOnly) {
c.httpOnly()
}
}
fun String.sanitizeHeaderValue(): String { fun String.sanitizeHeaderValue(): String {
return if (all(Char::isValidForHeaderValue)) { return if (all(Char::isValidForHeaderValue)) {
this // fast path this // fast path

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.Context import android.content.Context
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.Set
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.Locale import java.util.Locale

View File

@@ -1,7 +1,2 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
inline fun Long.ifZero(defaultValue: () -> Long): Long = if (this == 0L) defaultValue() else this
fun longOf(a: Int, b: Int): Long {
return a.toLong() shl 32 or (b.toLong() and 0xffffffffL)
}

View File

@@ -2,25 +2,11 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.Context import android.content.Context
import android.database.DatabaseUtils import android.database.DatabaseUtils
import androidx.annotation.FloatRange import androidx.collection.arraySetOf
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.ellipsize import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import java.util.UUID import java.util.UUID
inline fun <C : CharSequence?> C?.ifNullOrEmpty(defaultValue: () -> C): C {
return if (this.isNullOrEmpty()) defaultValue() else this
}
fun String.longHashCode(): Long {
var h = 1125899906842597L
val len: Int = this.length
for (i in 0 until len) {
h = 31 * h + this[i].code
}
return h
}
fun String.toUUIDOrNull(): UUID? = try { fun String.toUUIDOrNull(): UUID? = try {
UUID.fromString(this) UUID.fromString(this)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@@ -28,19 +14,35 @@ fun String.toUUIDOrNull(): UUID? = try {
null null
} }
fun String.digits() = filter { it.isDigit() } fun String.transliterate(skipMissing: Boolean): String {
val cyr = charArrayOf(
/** 'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п',
* @param threshold 0 = exact match 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я', 'ё', 'ў',
*/ )
fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float): Boolean { val lat = arrayOf(
if (threshold == 0f) { "a", "b", "v", "g", "d", "e", "zh", "z", "i", "y", "k", "l", "m", "n", "o", "p",
return equals(other, ignoreCase = true) "r", "s", "t", "u", "f", "h", "ts", "ch", "sh", "sch", "", "i", "", "e", "ju", "ja", "jo", "w",
)
return buildString(length + 5) {
for (c in this@transliterate) {
val p = cyr.binarySearch(c.lowercaseChar())
if (p in lat.indices) {
if (c.isUpperCase()) {
append(lat[p].uppercase())
} else {
append(lat[p])
}
} else if (!skipMissing) {
append(c)
}
}
} }
val diff = lowercase().levenshteinDistance(other.lowercase()) / ((length + other.length) / 2f)
return diff < threshold
} }
fun String.toFileNameSafe(): String = this.transliterate(false)
.replace(Regex("[^a-z0-9_\\-]", arraySetOf(RegexOption.IGNORE_CASE)), " ")
.replace(Regex("\\s+"), "_")
fun CharSequence.sanitize(): CharSequence { fun CharSequence.sanitize(): CharSequence {
return filterNot { c -> c.isReplacement() } return filterNot { c -> c.isReplacement() }
} }
@@ -68,10 +70,11 @@ fun <T> Collection<T>.joinToStringWithLimit(context: Context, limit: Int, transf
} }
} }
@Deprecated("", @Deprecated(
"",
ReplaceWith( ReplaceWith(
"sqlEscapeString(this)", "sqlEscapeString(this)",
"android.database.DatabaseUtils.sqlEscapeString" "android.database.DatabaseUtils.sqlEscapeString",
) ),
) )
fun String.sqlEscape(): String = DatabaseUtils.sqlEscapeString(this) fun String.sqlEscape(): String = DatabaseUtils.sqlEscapeString(this)

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
import org.koitharu.kotatsu.core.exceptions.SyncApiException import org.koitharu.kotatsu.core.exceptions.SyncApiException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.core.exceptions.WrapperIOException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.io.NullOutputStream import org.koitharu.kotatsu.core.io.NullOutputStream
@@ -37,6 +38,7 @@ import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
import java.net.ConnectException import java.net.ConnectException
@@ -54,6 +56,8 @@ fun Throwable.getDisplayMessage(resources: Resources): String = getDisplayMessag
?: resources.getString(R.string.error_occurred) ?: resources.getString(R.string.error_occurred)
private fun Throwable.getDisplayMessageOrNull(resources: Resources): String? = when (this) { private fun Throwable.getDisplayMessageOrNull(resources: Resources): String? = when (this) {
is CaughtException -> cause.getDisplayMessageOrNull(resources)
is WrapperIOException -> cause.getDisplayMessageOrNull(resources)
is ScrobblerAuthRequiredException -> resources.getString( is ScrobblerAuthRequiredException -> resources.getString(
R.string.scrobbler_auth_required, R.string.scrobbler_auth_required,
resources.getString(scrobbler.titleResId), resources.getString(scrobbler.titleResId),
@@ -141,7 +145,9 @@ fun Throwable.getCauseUrl(): String? = when (this) {
is ParseException -> url is ParseException -> url
is NotFoundException -> url is NotFoundException -> url
is TooManyRequestExceptions -> url is TooManyRequestExceptions -> url
is CaughtException -> cause?.getCauseUrl() is CaughtException -> cause.getCauseUrl()
is WrapperIOException -> cause.getCauseUrl()
is NoDataReceivedException -> url
is CloudFlareBlockedException -> url is CloudFlareBlockedException -> url
is CloudFlareProtectedException -> url is CloudFlareProtectedException -> url
is HttpStatusException -> url is HttpStatusException -> url
@@ -174,7 +180,10 @@ fun Throwable.isReportable(): Boolean {
return true return true
} }
if (this is CaughtException) { if (this is CaughtException) {
return cause?.isReportable() == true return cause.isReportable()
}
if (this is WrapperIOException) {
return cause.isReportable()
} }
if (ExceptionResolver.canResolve(this)) { if (ExceptionResolver.canResolve(this)) {
return false return false

View File

@@ -77,7 +77,6 @@ import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable import org.koitharu.kotatsu.core.util.ext.drawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.isTextTruncated import org.koitharu.kotatsu.core.util.ext.isTextTruncated
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
@@ -113,6 +112,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.ellipsize import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
@@ -274,7 +274,7 @@ class DetailsActivity :
startActivity( startActivity(
ImageActivity.newIntent( ImageActivity.newIntent(
v.context, v.context,
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } ?: return,
manga.source, manga.source,
), ),
scaleUpActivityOptionsOf(v), scaleUpActivityOptionsOf(v),

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextColorAttr import org.koitharu.kotatsu.core.util.ext.setTextColorAttr
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
fun pageThumbnailAD( fun pageThumbnailAD(
@@ -36,7 +37,7 @@ fun pageThumbnailAD(
AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView) AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView)
bind { bind {
val data: Any = item.page.preview?.takeUnless { it.isEmpty() } ?: item.page.toMangaPage() val data: Any = item.page.preview?.nullIfEmpty() ?: item.page.toMangaPage()
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run { binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
defaultPlaceholders(context) defaultPlaceholders(context)
size(thumbSize) size(thumbSize)

View File

@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView
import org.koitharu.kotatsu.core.util.ext.findActivity import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit import org.koitharu.kotatsu.core.util.ext.joinToStringWithLimit
import org.koitharu.kotatsu.core.util.ext.mapToArray
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.parentView import org.koitharu.kotatsu.core.util.ext.parentView
@@ -39,6 +38,7 @@ import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.settings.storage.DirectoryModel import org.koitharu.kotatsu.settings.storage.DirectoryModel
import javax.inject.Inject import javax.inject.Inject

View File

@@ -21,7 +21,6 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.download.ui.worker.DownloadTask import org.koitharu.kotatsu.download.ui.worker.DownloadTask
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
@@ -30,6 +29,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.sizeOrZero
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import org.koitharu.kotatsu.settings.storage.DirectoryModel import org.koitharu.kotatsu.settings.storage.DirectoryModel
import javax.inject.Inject import javax.inject.Inject

View File

@@ -60,7 +60,6 @@ import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getWorkInputData import org.koitharu.kotatsu.core.util.ext.getWorkInputData
import org.koitharu.kotatsu.core.util.ext.getWorkSpec import org.koitharu.kotatsu.core.util.ext.getWorkSpec
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.withTicker import org.koitharu.kotatsu.core.util.ext.withTicker
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
@@ -79,6 +78,7 @@ import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
import org.koitharu.kotatsu.parsers.model.Manga 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.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -199,7 +199,7 @@ class DownloadWorker @AssistedInject constructor(
format = task.format ?: settings.preferredDownloadFormat, format = task.format ?: settings.preferredDownloadFormat,
) )
val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl } val coverUrl = mangaDetails.largeCoverUrl.ifNullOrEmpty { mangaDetails.coverUrl }
if (coverUrl.isNotEmpty()) { if (!coverUrl.isNullOrEmpty()) {
downloadFile(coverUrl, destination, repo.source).let { file -> downloadFile(coverUrl, destination, repo.source).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
file.deleteAwait() file.deleteAwait()

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.explore.domain
import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.almostEquals
import org.koitharu.kotatsu.core.util.ext.asArrayList import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
@@ -11,6 +10,7 @@ import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist
import javax.inject.Inject import javax.inject.Inject
@@ -77,7 +77,7 @@ class ExploreRepository @Inject constructor(
val list = repository.getList( val list = repository.getList(
offset = 0, offset = 0,
order = order, order = order,
filter = MangaListFilter(tags = setOfNotNull(tag)) filter = MangaListFilter(tags = setOfNotNull(tag)),
).asArrayList() ).asArrayList()
if (settings.isSuggestionsExcludeNsfw) { if (settings.isSuggestionsExcludeNsfw) {
list.removeAll { it.isNsfw } list.removeAll { it.isNsfw }

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.explore.ui.model package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.core.model.MangaSourceInfo import org.koitharu.kotatsu.core.model.MangaSourceInfo
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.longHashCode
data class MangaSourceItem( data class MangaSourceItem(
val source: MangaSourceInfo, val source: MangaSourceInfo,

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.favourites.domain.model
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
data class Cover( data class Cover(
val url: String, val url: String?,
val source: String, val source: String,
) { ) {
val mangaSource by lazy { MangaSource(source) } val mangaSource by lazy { MangaSource(source) }

View File

@@ -65,7 +65,7 @@ class FavoriteSheet : BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(), OnLis
fun show(fm: FragmentManager, manga: Collection<Manga>) = FavoriteSheet().withArgs(1) { fun show(fm: FragmentManager, manga: Collection<Manga>) = FavoriteSheet().withArgs(1) {
putParcelableArrayList( putParcelableArrayList(
KEY_MANGA_LIST, KEY_MANGA_LIST,
manga.mapTo(ArrayList(manga.size), ::ParcelableManga), manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withDescription = false) },
) )
}.showDistinct(fm, TAG) }.showDistinct(fm, TAG)
} }

View File

@@ -52,7 +52,8 @@ class FavoriteSheetViewModel @Inject constructor(
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }, settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
) { categories, _, tracker -> ) { categories, _, tracker ->
mapList(categories, tracker) mapList(categories, tracker)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(header)) }.withErrorHandling()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(header))
fun setChecked(categoryId: Long, isChecked: Boolean) { fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {

View File

@@ -36,6 +36,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.YEAR_MIN import org.koitharu.kotatsu.parsers.model.YEAR_MIN
import org.koitharu.kotatsu.parsers.util.ifZero import org.koitharu.kotatsu.parsers.util.ifZero
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
@@ -267,7 +268,7 @@ class FilterCoordinator @Inject constructor(
} }
fun setQuery(value: String?) { fun setQuery(value: String?) {
val newQuery = value?.trim()?.takeUnless { it.isEmpty() } val newQuery = value?.trim()?.nullIfEmpty()
currentListFilter.update { oldValue -> currentListFilter.update { oldValue ->
if (capabilities.isSearchWithFiltersSupported || newQuery == null) { if (capabilities.isSearchWithFiltersSupported || newQuery == null) {
oldValue.copy(query = newQuery) oldValue.copy(query = newQuery)

View File

@@ -10,6 +10,7 @@ import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.badge.ExperimentalBadgeUtils
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
@CheckResult @CheckResult
fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? { fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? {
@@ -34,7 +35,7 @@ private fun View.bindBadgeImpl(
if (counter > 0) { if (counter > 0) {
badgeDrawable.number = counter badgeDrawable.number = counter
} else { } else {
badgeDrawable.text = text?.takeUnless { it.isEmpty() } badgeDrawable.text = text?.nullIfEmpty()
} }
badgeDrawable.isVisible = true badgeDrawable.isVisible = true
badgeDrawable.align(this) badgeDrawable.align(this)

View File

@@ -7,7 +7,7 @@ data class MangaCompactListModel(
override val id: Long, override val id: Long,
override val title: String, override val title: String,
val subtitle: String, val subtitle: String,
override val coverUrl: String, override val coverUrl: String?,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: ReadingProgress?, override val progress: ReadingProgress?,

View File

@@ -8,7 +8,7 @@ data class MangaDetailedListModel(
override val id: Long, override val id: Long,
override val title: String, override val title: String,
val subtitle: String?, val subtitle: String?,
override val coverUrl: String, override val coverUrl: String?,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: ReadingProgress?, override val progress: ReadingProgress?,

View File

@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
data class MangaGridModel( data class MangaGridModel(
override val id: Long, override val id: Long,
override val title: String, override val title: String,
override val coverUrl: String, override val coverUrl: String?,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: ReadingProgress?, override val progress: ReadingProgress?,

View File

@@ -11,7 +11,7 @@ sealed class MangaListModel : ListModel {
abstract val id: Long abstract val id: Long
abstract val manga: Manga abstract val manga: Manga
abstract val title: String abstract val title: String
abstract val coverUrl: String abstract val coverUrl: String?
abstract val counter: Int abstract val counter: Int
abstract val isFavorite: Boolean abstract val isFavorite: Boolean
abstract val progress: ReadingProgress? abstract val progress: ReadingProgress?

View File

@@ -28,7 +28,6 @@ import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable import org.koitharu.kotatsu.core.util.ext.drawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
@@ -40,6 +39,7 @@ import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject import javax.inject.Inject
@@ -100,7 +100,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
R.id.imageView_cover -> startActivity( R.id.imageView_cover -> startActivity(
ImageActivity.newIntent( ImageActivity.newIntent(
v.context, v.context,
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } ?: return,
manga.source, manga.source,
), ),
scaleUpActivityOptionsOf(v), scaleUpActivityOptionsOf(v),

View File

@@ -152,7 +152,8 @@ class LocalMangaRepository @Inject constructor(
"Manga is not stored on local storage" "Manga is not stored on local storage"
}.manga }.manga
LocalMangaUtil(subject).deleteChapters(ids) LocalMangaUtil(subject).deleteChapters(ids)
localStorageChanges.emit(LocalManga(subject)) val updated = getDetails(subject)
localStorageChanges.emit(LocalManga(updated))
} }
suspend fun getRemoteManga(localManga: Manga): Manga? { suspend fun getRemoteManga(localManga: Manga): Manga? {

View File

@@ -16,12 +16,12 @@ import okio.use
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.compressToPNG import org.koitharu.kotatsu.core.util.ext.compressToPNG
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.subdir import org.koitharu.kotatsu.core.util.ext.subdir
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.takeIfWriteable import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.io.File import java.io.File

View File

@@ -22,8 +22,8 @@ import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
import org.koitharu.kotatsu.core.util.ext.isFileUri import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isRegularFile import org.koitharu.kotatsu.core.util.ext.isRegularFile
import org.koitharu.kotatsu.core.util.ext.isZipUri import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toFileNameSafe
import org.koitharu.kotatsu.core.util.ext.toListSorted import org.koitharu.kotatsu.core.util.ext.toListSorted
import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.hasZipExtension import org.koitharu.kotatsu.local.data.hasZipExtension
@@ -33,10 +33,8 @@ import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga 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.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.toCamelCase
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import java.io.File import java.io.File
/** /**
@@ -61,26 +59,31 @@ class LocalMangaParser(private val uri: Uri) {
val mangaInfo = index?.getMangaInfo() val mangaInfo = index?.getMangaInfo()
if (mangaInfo != null) { if (mangaInfo != null) {
val coverEntry: Path? = index.getCoverEntry()?.let { rootPath / it } ?: fileSystem.findFirstImage(rootPath) val coverEntry: Path? = index.getCoverEntry()?.let { rootPath / it } ?: fileSystem.findFirstImage(rootPath)
mangaInfo.copyInternal( mangaInfo.copy(
source = LocalMangaSource, source = LocalMangaSource,
url = rootFile.toUri().toString(), url = rootFile.toUri().toString(),
coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() }.orEmpty(), coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() },
largeCoverUrl = null, largeCoverUrl = null,
chapters = if (withDetails) { chapters = if (withDetails) {
mangaInfo.chapters?.map { c -> mangaInfo.chapters?.mapNotNull { c ->
c.copyInternal( val path = index.getChapterFileName(c.id)?.toPath()
url = index.getChapterFileName(c.id)?.toPath()?.let { if (path != null && !fileSystem.exists(rootPath / path)) {
uri.child(it, resolve = false).toString() null
} ?: uri.toString(), } else {
source = LocalMangaSource, c.copy(
) url = path?.let {
uri.child(it, resolve = false).toString()
} ?: uri.toString(),
source = LocalMangaSource,
)
}
} }
} else { } else {
null null
}, },
) )
} else { } else {
val title = rootFile.nameWithoutExtension.replace("_", " ").toCamelCase() val title = rootFile.name.fileNameToTitle()
val coverEntry = fileSystem.findFirstImage(rootPath) val coverEntry = fileSystem.findFirstImage(rootPath)
val mimeTypeMap = MimeTypeMap.getSingleton() val mimeTypeMap = MimeTypeMap.getSingleton()
Manga( Manga(
@@ -89,9 +92,7 @@ class LocalMangaParser(private val uri: Uri) {
url = rootFile.toUri().toString(), url = rootFile.toUri().toString(),
publicUrl = rootFile.toUri().toString(), publicUrl = rootFile.toUri().toString(),
source = LocalMangaSource, source = LocalMangaSource,
coverUrl = coverEntry?.let { coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() },
uri.child(it, resolve = true).toString()
}.orEmpty(),
chapters = if (withDetails) { chapters = if (withDetails) {
val chapters = fileSystem.listRecursively(rootPath) val chapters = fileSystem.listRecursively(rootPath)
.mapNotNullTo(HashSet()) { path -> .mapNotNullTo(HashSet()) { path ->
@@ -111,7 +112,7 @@ class LocalMangaParser(private val uri: Uri) {
}.toString().removePrefix(Path.DIRECTORY_SEPARATOR) }.toString().removePrefix(Path.DIRECTORY_SEPARATOR)
MangaChapter( MangaChapter(
id = "$i$s".longHashCode(), id = "$i$s".longHashCode(),
name = s.ifEmpty { title }, name = s.fileNameToTitle().ifEmpty { title },
number = 0f, number = 0f,
volume = 0, volume = 0,
source = LocalMangaSource, source = LocalMangaSource,
@@ -270,43 +271,8 @@ class LocalMangaParser(private val uri: Uri) {
Path.DIRECTORY_SEPARATOR + this Path.DIRECTORY_SEPARATOR + this
}.toPath() }.toPath()
private fun Manga.copyInternal( private fun String.fileNameToTitle() = substringBeforeLast('.')
url: String = this.url, .replace('_', ' ')
coverUrl: String = this.coverUrl, .replaceFirstChar { it.uppercase() }
largeCoverUrl: String? = this.largeCoverUrl,
chapters: List<MangaChapter>? = this.chapters,
source: MangaSource = this.source,
): Manga = Manga(
id = id,
title = title,
altTitle = altTitle,
url = url,
publicUrl = publicUrl,
rating = rating,
isNsfw = isNsfw,
coverUrl = coverUrl,
tags = tags,
state = state,
author = author,
largeCoverUrl = largeCoverUrl,
description = description,
chapters = chapters,
source = source,
)
private fun MangaChapter.copyInternal(
url: String = this.url,
source: MangaSource = this.source,
) = MangaChapter(
id = id,
name = name,
number = number,
volume = volume,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = source,
)
} }
} }

View File

@@ -10,12 +10,12 @@ import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toFileNameSafe
import org.koitharu.kotatsu.core.zip.ZipOutput import org.koitharu.kotatsu.core.zip.ZipOutput
import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.local.data.input.LocalMangaParser
import org.koitharu.kotatsu.parsers.model.Manga 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.util.toFileNameSafe
import java.io.File import java.io.File
class LocalMangaDirOutput( class LocalMangaDirOutput(
@@ -96,7 +96,9 @@ class LocalMangaDirOutput(
} }
suspend fun deleteChapters(ids: Set<Long>) = mutex.withLock { suspend fun deleteChapters(ids: Set<Long>) = mutex.withLock {
val chapters = checkNotNull((index.getMangaInfo() ?: LocalMangaParser(rootFile).getManga(withDetails = true).manga).chapters) { val chapters = checkNotNull(
(index.getMangaInfo() ?: LocalMangaParser(rootFile).getManga(withDetails = true).manga).chapters,
) {
"No chapters found" "No chapters found"
}.withIndex() }.withIndex()
val victimsIds = ids.toMutableSet() val victimsIds = ids.toMutableSet()

View File

@@ -7,11 +7,11 @@ import kotlinx.coroutines.withContext
import okio.Closeable import okio.Closeable
import org.koitharu.kotatsu.core.prefs.DownloadFormat import org.koitharu.kotatsu.core.prefs.DownloadFormat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toFileNameSafe
import org.koitharu.kotatsu.local.data.input.LocalMangaParser import org.koitharu.kotatsu.local.data.input.LocalMangaParser
import org.koitharu.kotatsu.parsers.model.Manga 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.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import java.io.File import java.io.File
sealed class LocalMangaOutput( sealed class LocalMangaOutput(

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.local.domain package org.koitharu.kotatsu.local.domain
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.fold import kotlinx.coroutines.flow.fold
@@ -13,7 +12,6 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -26,7 +24,6 @@ class DeleteReadChaptersUseCase @Inject constructor(
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
) { ) {
suspend operator fun invoke(manga: Manga): Int { suspend operator fun invoke(manga: Manga): Int {
@@ -37,7 +34,6 @@ class DeleteReadChaptersUseCase @Inject constructor(
} }
val task = getDeletionTask(localManga) ?: return 0 val task = getDeletionTask(localManga) ?: return 0
localMangaRepository.deleteChapters(task.manga.manga, task.chaptersIds) localMangaRepository.deleteChapters(task.manga.manga, task.chaptersIds)
emitUpdate(localManga)
return task.chaptersIds.size return task.chaptersIds.size
} }
@@ -62,7 +58,6 @@ class DeleteReadChaptersUseCase @Inject constructor(
}.buffer().map { }.buffer().map {
runCatchingCancellable { runCatchingCancellable {
localMangaRepository.deleteChapters(it.manga.manga, it.chaptersIds) localMangaRepository.deleteChapters(it.manga.manga, it.chaptersIds)
emitUpdate(it.manga)
it.chaptersIds.size it.chaptersIds.size
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()
@@ -88,11 +83,6 @@ class DeleteReadChaptersUseCase @Inject constructor(
} }
} }
private suspend fun emitUpdate(subject: LocalManga) {
val updated = localMangaRepository.getDetails(subject.manga)
localStorageChanges.emit(subject.copy(manga = updated))
}
private suspend fun getAllChapters(manga: LocalManga): List<MangaChapter> = runCatchingCancellable { private suspend fun getAllChapters(manga: LocalManga): List<MangaChapter> = runCatchingCancellable {
val remoteManga = checkNotNull(localMangaRepository.getRemoteManga(manga.manga)) val remoteManga = checkNotNull(localMangaRepository.getRemoteManga(manga.manga))
checkNotNull(mangaRepositoryFactory.create(remoteManga.source).getDetails(remoteManga).chapters) checkNotNull(mangaRepositoryFactory.create(remoteManga.source).getDetails(remoteManga).chapters)

View File

@@ -152,7 +152,8 @@ class ImportService : CoroutineIntentService() {
private const val CHANNEL_ID = "importing" private const val CHANNEL_ID = "importing"
private const val FOREGROUND_NOTIFICATION_ID = 37 private const val FOREGROUND_NOTIFICATION_ID = 37
fun start(context: Context, uris: Iterable<Uri>): Boolean = try { fun start(context: Context, uris: Collection<Uri>): Boolean = try {
require(uris.isNotEmpty())
for (uri in uris) { for (uri in uris) {
val intent = Intent(context, ImportService::class.java) val intent = Intent(context, ImportService::class.java)
intent.putExtra(DATA_URI, uri.toString()) intent.putExtra(DATA_URI, uri.toString())

View File

@@ -10,11 +10,11 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.bookmarkKey import org.koitharu.kotatsu.core.util.ext.bookmarkKey
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.mangaKey import org.koitharu.kotatsu.core.util.ext.mangaKey
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.findById import org.koitharu.kotatsu.parsers.util.findById
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Collections import java.util.Collections
import javax.inject.Inject import javax.inject.Inject

View File

@@ -185,11 +185,8 @@ class PageLoader @Inject constructor(
prefetchLock.withLock { prefetchLock.withLock {
while (prefetchQueue.isNotEmpty()) { while (prefetchQueue.isNotEmpty()) {
val page = prefetchQueue.pollFirst() ?: return@launch val page = prefetchQueue.pollFirst() ?: return@launch
if (cache.get(page.url) == null) { synchronized(tasks) {
synchronized(tasks) { tasks[page.id] = loadPageAsyncImpl(page, skipCache = false, isPrefetch = true)
tasks[page.id] = loadPageAsyncImpl(page, skipCache = false, isPrefetch = true)
}
return@launch
} }
} }
} }
@@ -202,12 +199,14 @@ class PageLoader @Inject constructor(
): ProgressDeferred<Uri, Float> { ): ProgressDeferred<Uri, Float> {
val progress = MutableStateFlow(PROGRESS_UNDEFINED) val progress = MutableStateFlow(PROGRESS_UNDEFINED)
val deferred = loaderScope.async { val deferred = loaderScope.async {
if (!skipCache) {
cache.get(page.url)?.let { return@async it.toUri() }
}
counter.incrementAndGet() counter.incrementAndGet()
try { try {
loadPageImpl(page, progress, isPrefetch) loadPageImpl(
page = page,
progress = progress,
isPrefetch = isPrefetch,
skipCache = skipCache,
)
} finally { } finally {
if (counter.decrementAndGet() == 0) { if (counter.decrementAndGet() == 0) {
onIdle() onIdle()
@@ -231,9 +230,13 @@ class PageLoader @Inject constructor(
page: MangaPage, page: MangaPage,
progress: MutableStateFlow<Float>, progress: MutableStateFlow<Float>,
isPrefetch: Boolean, isPrefetch: Boolean,
skipCache: Boolean,
): Uri = semaphore.withPermit { ): Uri = semaphore.withPermit {
val pageUrl = getPageUrl(page) val pageUrl = getPageUrl(page)
check(pageUrl.isNotBlank()) { "Cannot obtain full image url for $page" } check(pageUrl.isNotBlank()) { "Cannot obtain full image url for $page" }
if (!skipCache) {
cache.get(pageUrl)?.let { return it.toUri() }
}
val uri = Uri.parse(pageUrl) val uri = Uri.parse(pageUrl)
return when { return when {
uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) { uri.isZipUri() -> if (uri.scheme == URI_SCHEME_ZIP) {

View File

@@ -31,12 +31,12 @@ import okio.source
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.isFileUri import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isZipUri import org.koitharu.kotatsu.core.util.ext.isZipUri
import org.koitharu.kotatsu.core.util.ext.toFileNameSafe
import org.koitharu.kotatsu.core.util.ext.toFileOrNull import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.parsers.model.Manga 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.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat

View File

@@ -209,6 +209,9 @@ class ReaderInfoBarView @JvmOverloads constructor(
} }
private fun Drawable.drawWithOutline(canvas: Canvas) { private fun Drawable.drawWithOutline(canvas: Canvas) {
if (bounds.isEmpty) {
return
}
var requiredScale = (bounds.width() + paint.strokeWidth * 2f) / bounds.width().toFloat() var requiredScale = (bounds.width() + paint.strokeWidth * 2f) / bounds.width().toFloat()
setTint(colorOutline) setTint(colorOutline)
canvas.withScale(requiredScale, requiredScale, bounds.exactCenterX(), bounds.exactCenterY()) { canvas.withScale(requiredScale, requiredScale, bounds.exactCenterX(), bounds.exactCenterY()) {

View File

@@ -40,9 +40,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsInteractor import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
@@ -57,6 +55,8 @@ import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.sizeOrZero
import org.koitharu.kotatsu.reader.domain.ChaptersLoader import org.koitharu.kotatsu.reader.domain.ChaptersLoader
import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase import org.koitharu.kotatsu.reader.domain.DetectReaderModeUseCase
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader

View File

@@ -38,6 +38,7 @@ import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -137,7 +138,7 @@ class ColorFilterConfigActivity :
} }
private fun loadPreview(page: MangaPage) { private fun loadPreview(page: MangaPage) {
val data: Any = page.preview?.takeUnless { it.isEmpty() } ?: page val data: Any = page.preview?.nullIfEmpty() ?: page
ImageRequest.Builder(this@ColorFilterConfigActivity) ImageRequest.Builder(this@ColorFilterConfigActivity)
.data(data) .data(data)
.scale(Scale.FILL) .scale(Scale.FILL)

View File

@@ -8,9 +8,9 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.mapToArray
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import kotlin.coroutines.resume import kotlin.coroutines.resume

View File

@@ -26,7 +26,6 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.getCauseUrl import org.koitharu.kotatsu.core.util.ext.getCauseUrl
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.domain.ExploreRepository import org.koitharu.kotatsu.explore.domain.ExploreRepository
@@ -40,6 +39,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorFooter
import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.sizeOrZero
import javax.inject.Inject import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L private const val FILTER_MIN_INTERVAL = 250L

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.scrobbling.common.data
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import org.jsoup.internal.StringUtil.StringJoiner import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerUser
@@ -31,7 +32,7 @@ class ScrobblerStorage(context: Context, service: ScrobblerService) {
ScrobblerUser( ScrobblerUser(
id = lines[0].toLong(), id = lines[0].toLong(),
nickname = lines[1], nickname = lines[1],
avatar = lines[2].takeUnless(String::isEmpty), avatar = lines[2].nullIfEmpty(),
service = ScrobblerService.valueOf(lines[3]), service = ScrobblerService.valueOf(lines[3]),
) )
} }

View File

@@ -7,6 +7,7 @@ import okhttp3.internal.closeQuietly
import okio.IOException import okio.IOException
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.parsers.util.mimeType import org.koitharu.kotatsu.parsers.util.mimeType
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.parseHtml import org.koitharu.kotatsu.parsers.util.parseHtml
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage import org.koitharu.kotatsu.scrobbling.common.data.ScrobblerStorage
@@ -34,7 +35,7 @@ class KitsuInterceptor(private val storage: ScrobblerStorage) : Interceptor {
} }
if (response.mimeType?.toMediaTypeOrNull()?.subtype == SUBTYPE_HTML) { if (response.mimeType?.toMediaTypeOrNull()?.subtype == SUBTYPE_HTML) {
val message = runCatchingCancellable { val message = runCatchingCancellable {
response.parseHtml().title().takeUnless { it.isEmpty() } response.parseHtml().title().nullIfEmpty()
}.onFailure { }.onFailure {
response.closeQuietly() response.closeQuietly()
}.getOrNull() ?: "Invalid response (${response.code})" }.getOrNull() ?: "Invalid response (${response.code})"

View File

@@ -21,11 +21,11 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
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.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.sizeOrZero
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 import javax.inject.Inject

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.settings
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.preference.Preference import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -10,12 +11,16 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.settings.search.SettingsSearchMenuProvider
import org.koitharu.kotatsu.settings.search.SettingsSearchViewModel
@AndroidEntryPoint @AndroidEntryPoint
class RootSettingsFragment : BasePreferenceFragment(0) { class RootSettingsFragment : BasePreferenceFragment(0) {
private val viewModel: RootSettingsViewModel by viewModels() private val viewModel: RootSettingsViewModel by viewModels()
private val activityViewModel: SettingsSearchViewModel by activityViewModels()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_root) addPreferencesFromResource(R.xml.pref_root)
@@ -41,6 +46,8 @@ class RootSettingsFragment : BasePreferenceFragment(0) {
} }
} }
} }
addMenuProvider(SettingsSearchMenuProvider(activityViewModel))
addMenuProvider(SettingsMenuProvider(view.context))
} }
override fun setTitle(title: CharSequence?) { override fun setTitle(title: CharSequence?) {

View File

@@ -34,7 +34,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.about.AboutSettingsFragment import org.koitharu.kotatsu.settings.about.AboutSettingsFragment
import org.koitharu.kotatsu.settings.search.SettingsItem import org.koitharu.kotatsu.settings.search.SettingsItem
import org.koitharu.kotatsu.settings.search.SettingsSearchFragment import org.koitharu.kotatsu.settings.search.SettingsSearchFragment
import org.koitharu.kotatsu.settings.search.SettingsSearchMenuProvider
import org.koitharu.kotatsu.settings.search.SettingsSearchViewModel import org.koitharu.kotatsu.settings.search.SettingsSearchViewModel
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
@@ -76,8 +75,6 @@ class SettingsActivity :
} }
viewModel.isSearchActive.observe(this, ::toggleSearchMode) viewModel.isSearchActive.observe(this, ::toggleSearchMode)
viewModel.onNavigateToPreference.observeEvent(this, ::navigateToPreference) viewModel.onNavigateToPreference.observeEvent(this, ::navigateToPreference)
addMenuProvider(SettingsSearchMenuProvider(viewModel))
addMenuProvider(SettingsMenuProvider(this))
} }
override fun onPreferenceStartFragment( override fun onPreferenceStartFragment(
@@ -174,8 +171,9 @@ class SettingsActivity :
} }
private fun navigateToPreference(item: SettingsItem) { private fun navigateToPreference(item: SettingsItem) {
val args = Bundle(1) val args = Bundle(1).apply {
args.putString(ARG_PREF_KEY, item.key) putString(ARG_PREF_KEY, item.key)
}
openFragment(item.fragmentClass, args, true) openFragment(item.fragmentClass, args, true)
} }

View File

@@ -44,6 +44,6 @@ class SyncSettingsFragment : BasePreferenceFragment(R.string.sync_settings), Fra
private fun bindHostSummary() { private fun bindHostSummary() {
val preference = findPreference<Preference>(SyncSettings.KEY_SYNC_URL) ?: return val preference = findPreference<Preference>(SyncSettings.KEY_SYNC_URL) ?: return
preference.summary = syncSettings.syncURL preference.summary = syncSettings.syncUrl
} }
} }

View File

@@ -10,6 +10,7 @@ import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.core.github.AppVersion
@@ -29,10 +30,8 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
addPreferencesFromResource(R.xml.pref_about) addPreferencesFromResource(R.xml.pref_about)
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run { findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
title = getString(R.string.app_version, BuildConfig.VERSION_NAME) title = getString(R.string.app_version, BuildConfig.VERSION_NAME)
isEnabled = viewModel.isUpdateSupported
} }
findPreference<SwitchPreferenceCompat>(AppSettings.KEY_UPDATES_UNSTABLE)?.run { findPreference<SwitchPreferenceCompat>(AppSettings.KEY_UPDATES_UNSTABLE)?.run {
isVisible = viewModel.isUpdateSupported
isEnabled = VersionId(BuildConfig.VERSION_NAME).isStable isEnabled = VersionId(BuildConfig.VERSION_NAME).isStable
if (!isEnabled) isChecked = true if (!isEnabled) isChecked = true
} }
@@ -40,9 +39,12 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.isLoading.observe(viewLifecycleOwner) { combine(viewModel.isUpdateSupported, viewModel.isLoading, ::Pair)
findPreference<Preference>(AppSettings.KEY_APP_UPDATE)?.isEnabled = !it .observe(viewLifecycleOwner) { (isUpdateSupported, isLoading) ->
} findPreference<Preference>(AppSettings.KEY_UPDATES_UNSTABLE)?.isVisible = isUpdateSupported
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.isEnabled = isUpdateSupported && !isLoading
}
viewModel.onUpdateAvailable.observeEvent(viewLifecycleOwner, ::onUpdateAvailable) viewModel.onUpdateAvailable.observeEvent(viewLifecycleOwner, ::onUpdateAvailable)
} }

View File

@@ -1,6 +1,10 @@
package org.koitharu.kotatsu.settings.about package org.koitharu.kotatsu.settings.about
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import org.koitharu.kotatsu.core.github.AppUpdateRepository import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -13,7 +17,10 @@ class AboutSettingsViewModel @Inject constructor(
private val appUpdateRepository: AppUpdateRepository, private val appUpdateRepository: AppUpdateRepository,
) : BaseViewModel() { ) : BaseViewModel() {
val isUpdateSupported = appUpdateRepository.isUpdateSupported() val isUpdateSupported = flow {
emit(appUpdateRepository.isUpdateSupported())
}.stateIn(viewModelScope, SharingStarted.Eagerly, false)
val onUpdateAvailable = MutableEventFlow<AppVersion?>() val onUpdateAvailable = MutableEventFlow<AppVersion?>()
fun checkForUpdates() { fun checkForUpdates() {

View File

@@ -7,6 +7,8 @@ import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
@@ -17,7 +19,9 @@ import org.koitharu.kotatsu.databinding.FragmentSearchSuggestionBinding
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
@AndroidEntryPoint @AndroidEntryPoint
class SettingsSearchFragment : BaseFragment<FragmentSearchSuggestionBinding>(), OnListItemClickListener<SettingsItem> { class SettingsSearchFragment : BaseFragment<FragmentSearchSuggestionBinding>(),
OnListItemClickListener<SettingsItem>,
ListListener<SettingsItem> {
private val viewModel: SettingsSearchViewModel by activityViewModels() private val viewModel: SettingsSearchViewModel by activityViewModels()
@@ -29,6 +33,7 @@ class SettingsSearchFragment : BaseFragment<FragmentSearchSuggestionBinding>(),
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
val adapter = BaseListAdapter<SettingsItem>() val adapter = BaseListAdapter<SettingsItem>()
.addDelegate(ListItemType.NAV_ITEM, settingsItemAD(this)) .addDelegate(ListItemType.NAV_ITEM, settingsItemAD(this))
adapter.addListListener(this)
binding.root.adapter = adapter binding.root.adapter = adapter
binding.root.setHasFixedSize(true) binding.root.setHasFixedSize(true)
viewModel.content.observe(viewLifecycleOwner, adapter) viewModel.content.observe(viewLifecycleOwner, adapter)
@@ -45,4 +50,13 @@ class SettingsSearchFragment : BaseFragment<FragmentSearchSuggestionBinding>(),
} }
override fun onItemClick(item: SettingsItem, view: View) = viewModel.navigateToPreference(item) override fun onItemClick(item: SettingsItem, view: View) = viewModel.navigateToPreference(item)
override fun onCurrentListChanged(
previousList: List<SettingsItem?>,
currentList: List<SettingsItem?>
) {
if (currentList.size != previousList.size) {
(viewBinding?.root?.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(0, 0)
}
}
} }

View File

@@ -13,9 +13,12 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.settings.AppearanceSettingsFragment import org.koitharu.kotatsu.settings.AppearanceSettingsFragment
import org.koitharu.kotatsu.settings.DownloadsSettingsFragment import org.koitharu.kotatsu.settings.DownloadsSettingsFragment
import org.koitharu.kotatsu.settings.NetworkSettingsFragment import org.koitharu.kotatsu.settings.NetworkSettingsFragment
import org.koitharu.kotatsu.settings.ProxySettingsFragment
import org.koitharu.kotatsu.settings.ReaderSettingsFragment import org.koitharu.kotatsu.settings.ReaderSettingsFragment
import org.koitharu.kotatsu.settings.ServicesSettingsFragment import org.koitharu.kotatsu.settings.ServicesSettingsFragment
import org.koitharu.kotatsu.settings.SuggestionsSettingsFragment
import org.koitharu.kotatsu.settings.about.AboutSettingsFragment import org.koitharu.kotatsu.settings.about.AboutSettingsFragment
import org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment
@@ -39,6 +42,30 @@ class SettingsSearchHelper @Inject constructor(
preferenceManager.inflateTo(result, R.xml.pref_tracker, emptyList(), TrackerSettingsFragment::class.java) preferenceManager.inflateTo(result, R.xml.pref_tracker, emptyList(), TrackerSettingsFragment::class.java)
preferenceManager.inflateTo(result, R.xml.pref_services, emptyList(), ServicesSettingsFragment::class.java) preferenceManager.inflateTo(result, R.xml.pref_services, emptyList(), ServicesSettingsFragment::class.java)
preferenceManager.inflateTo(result, R.xml.pref_about, emptyList(), AboutSettingsFragment::class.java) preferenceManager.inflateTo(result, R.xml.pref_about, emptyList(), AboutSettingsFragment::class.java)
preferenceManager.inflateTo(
result,
R.xml.pref_backup_periodic,
listOf(context.getString(R.string.data_and_privacy)),
PeriodicalBackupSettingsFragment::class.java,
)
preferenceManager.inflateTo(
result,
R.xml.pref_proxy,
listOf(context.getString(R.string.proxy)),
ProxySettingsFragment::class.java,
)
preferenceManager.inflateTo(
result,
R.xml.pref_suggestions,
listOf(context.getString(R.string.suggestions)),
SuggestionsSettingsFragment::class.java,
)
preferenceManager.inflateTo(
result,
R.xml.pref_sources,
listOf(context.getString(R.string.remote_sources)),
SourcesSettingsFragment::class.java,
)
return result return result
} }

View File

@@ -22,18 +22,20 @@ class SettingsSearchMenuProvider(
override fun onPrepareMenu(menu: Menu) { override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu) super.onPrepareMenu(menu)
val currentQuery = viewModel.currentQuery if (viewModel.isSearchActive.value) {
if (currentQuery.isNotEmpty()) {
val menuItem = menu.findItem(R.id.action_search) val menuItem = menu.findItem(R.id.action_search)
menuItem.expandActionView() menuItem.expandActionView()
val searchView = menuItem.actionView as SearchView val searchView = menuItem.actionView as SearchView
searchView.setQuery(currentQuery, false) searchView.setQuery(viewModel.currentQuery, false)
} }
} }
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
override fun onMenuItemActionExpand(item: MenuItem): Boolean = true override fun onMenuItemActionExpand(item: MenuItem): Boolean {
viewModel.startSearch()
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
viewModel.discardSearch() viewModel.discardSearch()

View File

@@ -18,30 +18,43 @@ class SettingsSearchViewModel @Inject constructor(
private val searchHelper: SettingsSearchHelper, private val searchHelper: SettingsSearchHelper,
) : BaseViewModel() { ) : BaseViewModel() {
private val query = MutableStateFlow("") private val query = MutableStateFlow<String?>(null)
private val allSettings by lazy { private val allSettings by lazy {
searchHelper.inflatePreferences() searchHelper.inflatePreferences()
} }
val content = query.map { q -> val content = query.map { q ->
allSettings.filter { it.title.contains(q, ignoreCase = true) } if (q == null) {
emptyList()
} else {
allSettings.filter { it.title.contains(q, ignoreCase = true) }
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList()) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
val isSearchActive = query.map { val isSearchActive = query.map {
it.isNotEmpty() it != null
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, query.value != null)
val onNavigateToPreference = MutableEventFlow<SettingsItem>() val onNavigateToPreference = MutableEventFlow<SettingsItem>()
val currentQuery: String val currentQuery: String
get() = query.value get() = query.value.orEmpty()
fun onQueryChanged(value: String) { fun onQueryChanged(value: String) {
query.value = value if (query.value != null) {
query.value = value
}
} }
fun discardSearch() = onQueryChanged("") fun discardSearch() {
query.value = null
}
fun startSearch() {
query.value = query.value.orEmpty()
}
fun navigateToPreference(item: SettingsItem) { fun navigateToPreference(item: SettingsItem) {
discardSearch()
onNavigateToPreference.call(item) onNavigateToPreference.call(item)
} }
} }

View File

@@ -10,9 +10,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.EmptyMangaRepository import org.koitharu.kotatsu.core.parser.EmptyMangaRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.ParserMangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.mapToArray
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.settings.utils.AutoCompleteTextViewPreference import org.koitharu.kotatsu.settings.utils.AutoCompleteTextViewPreference
import org.koitharu.kotatsu.settings.utils.EditTextBindListener import org.koitharu.kotatsu.settings.utils.EditTextBindListener
import org.koitharu.kotatsu.settings.utils.EditTextDefaultSummaryProvider import org.koitharu.kotatsu.settings.utils.EditTextDefaultSummaryProvider
@@ -119,6 +119,6 @@ private fun PreferenceFragmentCompat.addPreferencesFromEmptyRepository() {
preferenceScreen.addPreference(preference) preferenceScreen.addPreference(preference)
} }
private fun Array<out String>.toStringArray(): Array<String> { private fun Array<out String?>.toStringArray(): Array<String> {
return Array(size) { i -> this[i] as? String ?: "" } return Array(size) { i -> this[i].orEmpty() }
} }

View File

@@ -16,4 +16,4 @@ class EditTextDefaultSummaryProvider(
text text
} }
} }
} }

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.suggestions.domain package org.koitharu.kotatsu.suggestions.domain
import org.koitharu.kotatsu.core.util.ext.almostEquals
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.almostEquals
class TagsBlacklist( class TagsBlacklist(
private val tags: Set<String>, private val tags: Set<String>,

View File

@@ -51,7 +51,6 @@ import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.almostEquals
import org.koitharu.kotatsu.core.util.ext.asArrayList import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag
@@ -60,7 +59,6 @@ import org.koitharu.kotatsu.core.util.ext.flatten
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sanitize import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.core.util.ext.takeMostFrequent import org.koitharu.kotatsu.core.util.ext.takeMostFrequent
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.core.util.ext.trySetForeground
@@ -73,7 +71,9 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilter
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.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.sizeOrZero
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler

View File

@@ -32,7 +32,7 @@ class SyncAuthenticator(
private fun tryRefreshToken() = runCatching { private fun tryRefreshToken() = runCatching {
runBlocking { runBlocking {
authApi.authenticate( authApi.authenticate(
syncSettings.syncURL, syncSettings.syncUrl,
account.name, account.name,
accountManager.getPassword(account), accountManager.getPassword(account),
) )

View File

@@ -6,7 +6,7 @@ import android.content.Context
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import javax.inject.Inject import javax.inject.Inject
class SyncSettings( class SyncSettings(
@@ -27,13 +27,9 @@ class SyncSettings(
@get:WorkerThread @get:WorkerThread
@set:WorkerThread @set:WorkerThread
var syncURL: String var syncUrl: String
get() = account?.let { get() = account?.let {
val result = accountManager.getUserData(it, KEY_SYNC_URL) accountManager.getUserData(it, KEY_SYNC_URL)?.withHttpSchema()
if (!result.startsWith("http://") && !result.startsWith("https://")) {
return "http://$result"
}
return result
}.ifNullOrEmpty { defaultSyncUrl } }.ifNullOrEmpty { defaultSyncUrl }
set(value) { set(value) {
account?.let { account?.let {
@@ -43,6 +39,12 @@ class SyncSettings(
companion object { companion object {
private fun String.withHttpSchema(): String = if (!startsWith("http://") && !startsWith("https://")) {
"http://$this"
} else {
this
}
const val KEY_SYNC_URL = "host" const val KEY_SYNC_URL = "host"
} }
} }

View File

@@ -59,7 +59,7 @@ class SyncHelper @AssistedInject constructor(
.addInterceptor(SyncInterceptor(context, account)) .addInterceptor(SyncInterceptor(context, account))
.build() .build()
private val baseUrl: String by lazy { private val baseUrl: String by lazy {
settings.syncURL settings.syncUrl
} }
private val defaultGcPeriod: Long // gc period if sync enabled private val defaultGcPeriod: Long // gc period if sync enabled
get() = TimeUnit.DAYS.toMillis(4) get() = TimeUnit.DAYS.toMillis(4)

View File

@@ -13,9 +13,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding import org.koitharu.kotatsu.databinding.PreferenceDialogAutocompletetextviewBinding
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.settings.utils.validation.UrlValidator import org.koitharu.kotatsu.settings.utils.validation.UrlValidator
import org.koitharu.kotatsu.sync.data.SyncSettings import org.koitharu.kotatsu.sync.data.SyncSettings
import javax.inject.Inject import javax.inject.Inject
@@ -52,7 +52,7 @@ class SyncHostDialogFragment : AlertDialogFragment<PreferenceDialogAutocompletet
binding.message.setText(R.string.sync_host_description) binding.message.setText(R.string.sync_host_description)
val entries = binding.root.resources.getStringArray(R.array.sync_url_list) val entries = binding.root.resources.getStringArray(R.array.sync_url_list)
val editText = binding.edit val editText = binding.edit
editText.setText(arguments?.getString(KEY_SYNC_URL).ifNullOrEmpty { syncSettings.syncURL }) editText.setText(arguments?.getString(KEY_SYNC_URL).ifNullOrEmpty { syncSettings.syncUrl })
editText.threshold = 0 editText.threshold = 0
editText.setAdapter(ArrayAdapter(binding.root.context, android.R.layout.simple_spinner_dropdown_item, entries)) editText.setAdapter(ArrayAdapter(binding.root.context, android.R.layout.simple_spinner_dropdown_item, entries))
binding.dropdown.setOnClickListener { binding.dropdown.setOnClickListener {
@@ -69,7 +69,7 @@ class SyncHostDialogFragment : AlertDialogFragment<PreferenceDialogAutocompletet
if (!result.startsWith("https://") && !result.startsWith("http://")) { if (!result.startsWith("https://") && !result.startsWith("http://")) {
scheme = "http://" scheme = "http://"
} }
syncSettings.syncURL = "$scheme$result" syncSettings.syncUrl = "$scheme$result"
parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(KEY_SYNC_URL to "$scheme$result")) parentFragmentManager.setFragmentResult(REQUEST_KEY, bundleOf(KEY_SYNC_URL to "$scheme$result"))
} }
} }

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.tracker.domain package org.koitharu.kotatsu.tracker.domain
import android.util.Log
import coil3.request.CachePolicy import coil3.request.CachePolicy
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.CachingMangaRepository import org.koitharu.kotatsu.core.parser.CachingMangaRepository
@@ -11,6 +13,7 @@ import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.findById
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
@@ -45,8 +48,9 @@ class CheckNewChaptersUseCase @Inject constructor(
runCatchingCancellable { runCatchingCancellable {
repository.updateTracks() repository.updateTracks()
val details = getFullManga(manga) val details = getFullManga(manga)
val chapters = details.chapters ?: return@withLock
val track = repository.getTrackOrNull(manga) ?: return@withLock val track = repository.getTrackOrNull(manga) ?: return@withLock
val branch = checkNotNull(details.chapters?.findById(currentChapterId)).branch
val chapters = details.getChapters(branch)
val chapterIndex = chapters.indexOfFirst { x -> x.id == currentChapterId } val chapterIndex = chapters.indexOfFirst { x -> x.id == currentChapterId }
val lastNewChapterIndex = chapters.size - track.newChapters val lastNewChapterIndex = chapters.size - track.newChapters
val lastChapter = chapters.lastOrNull() val lastChapter = chapters.lastOrNull()
@@ -70,7 +74,7 @@ class CheckNewChaptersUseCase @Inject constructor(
private suspend fun invokeImpl(track: MangaTracking): MangaUpdates = runCatchingCancellable { private suspend fun invokeImpl(track: MangaTracking): MangaUpdates = runCatchingCancellable {
val details = getFullManga(track.manga) val details = getFullManga(track.manga)
compare(track, details, getBranch(details)) compare(track, details, getBranch(details, track.lastChapterId))
}.getOrElse { error -> }.getOrElse { error ->
MangaUpdates.Failure( MangaUpdates.Failure(
manga = track.manga, manga = track.manga,
@@ -80,9 +84,17 @@ class CheckNewChaptersUseCase @Inject constructor(
repository.saveUpdates(updates) repository.saveUpdates(updates)
} }
private suspend fun getBranch(manga: Manga): String? { private suspend fun getBranch(manga: Manga, trackChapterId: Long): String? {
val history = historyRepository.getOne(manga) historyRepository.getOne(manga)?.let {
return manga.getPreferredBranch(history) manga.chapters?.findById(it.chapterId)
}?.let {
return it.branch
}
manga.chapters?.findById(trackChapterId)?.let {
return it.branch
}
// fallback
return manga.getPreferredBranch(null)
} }
private suspend fun getFullManga(manga: Manga): Manga = when { private suspend fun getFullManga(manga: Manga): Manga = when {
@@ -111,25 +123,29 @@ class CheckNewChaptersUseCase @Inject constructor(
private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates.Success { private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates.Success {
if (track.isEmpty()) { if (track.isEmpty()) {
// first check or manga was empty on last check // first check or manga was empty on last check
return MangaUpdates.Success(manga, emptyList(), isValid = false) return MangaUpdates.Success(manga, branch, emptyList(), isValid = false)
} }
val chapters = requireNotNull(manga.getChapters(branch)) val chapters = requireNotNull(manga.getChapters(branch))
if (BuildConfig.DEBUG && chapters.findById(track.lastChapterId) == null) {
Log.e("Tracker", "Chapter ${track.lastChapterId} not found")
}
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId } val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
return when { return when {
newChapters.isEmpty() -> { newChapters.isEmpty() -> {
MangaUpdates.Success( MangaUpdates.Success(
manga = manga, manga = manga,
branch = branch,
newChapters = emptyList(), newChapters = emptyList(),
isValid = chapters.lastOrNull()?.id == track.lastChapterId, isValid = chapters.lastOrNull()?.id == track.lastChapterId,
) )
} }
newChapters.size == chapters.size -> { newChapters.size == chapters.size -> {
MangaUpdates.Success(manga, emptyList(), isValid = false) MangaUpdates.Success(manga, branch, emptyList(), isValid = false)
} }
else -> { else -> {
MangaUpdates.Success(manga, newChapters, isValid = true) MangaUpdates.Success(manga, branch, newChapters, isValid = true)
} }
} }
} }

View File

@@ -11,12 +11,12 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.ifZero
import org.koitharu.kotatsu.tracker.data.TrackEntity import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.toTrackingLogItem import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
@@ -216,7 +216,6 @@ class TrackingRepository @Inject constructor(
} }
private fun TrackEntity.mergeWith(updates: MangaUpdates): TrackEntity { private fun TrackEntity.mergeWith(updates: MangaUpdates): TrackEntity {
val chapters = updates.manga.chapters.orEmpty()
return when (updates) { return when (updates) {
is MangaUpdates.Failure -> TrackEntity( is MangaUpdates.Failure -> TrackEntity(
mangaId = mangaId, mangaId = mangaId,
@@ -230,7 +229,7 @@ class TrackingRepository @Inject constructor(
is MangaUpdates.Success -> TrackEntity( is MangaUpdates.Success -> TrackEntity(
mangaId = mangaId, mangaId = mangaId,
lastChapterId = chapters.lastOrNull()?.id ?: NO_ID, lastChapterId = updates.manga.getChapters(updates.branch).lastOrNull()?.id ?: NO_ID,
newChapters = if (updates.isValid) newChapters + updates.newChapters.size else 0, newChapters = if (updates.isValid) newChapters + updates.newChapters.size else 0,
lastCheckTime = System.currentTimeMillis(), lastCheckTime = System.currentTimeMillis(),
lastChapterDate = updates.lastChapterDate().ifZero { lastChapterDate }, lastChapterDate = updates.lastChapterDate().ifZero { lastChapterDate },

View File

@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.tracker.domain.model package org.koitharu.kotatsu.tracker.domain.model
import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions import org.koitharu.kotatsu.parsers.exception.TooManyRequestExceptions
import org.koitharu.kotatsu.parsers.model.Manga 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.util.ifZero
sealed interface MangaUpdates { sealed interface MangaUpdates {
@@ -11,6 +11,7 @@ sealed interface MangaUpdates {
data class Success( data class Success(
override val manga: Manga, override val manga: Manga,
val branch: String?,
val newChapters: List<MangaChapter>, val newChapters: List<MangaChapter>,
val isValid: Boolean, val isValid: Boolean,
) : MangaUpdates { ) : MangaUpdates {

View File

@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
data class FeedItem( data class FeedItem(
val id: Long, val id: Long,
val imageUrl: String, val imageUrl: String?,
val title: String, val title: String,
val manga: Manga, val manga: Manga,
val count: Int, val count: Int,

View File

@@ -30,7 +30,7 @@ class WidgetUpdater @Inject constructor(
private fun updateWidgets(cls: Class<*>) { private fun updateWidgets(cls: Class<*>) {
val intent = Intent(context, cls) val intent = Intent(context, cls)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(context) val ids = (AppWidgetManager.getInstance(context) ?: return)
.getAppWidgetIds(ComponentName(context, cls)) .getAppWidgetIds(ComponentName(context, cls))
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
context.sendBroadcast(intent) context.sendBroadcast(intent)

View File

@@ -59,6 +59,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="?colorSecondaryContainer" android:background="?colorSecondaryContainer"
android:clipToOutline="true"
android:foreground="?selectableItemBackground" android:foreground="?selectableItemBackground"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="H,13:18" app:layout_constraintDimensionRatio="H,13:18"

View File

@@ -52,6 +52,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="?colorSecondaryContainer" android:background="?colorSecondaryContainer"
android:clipToOutline="true"
android:foreground="?selectableItemBackground" android:foreground="?selectableItemBackground"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="H,13:18" app:layout_constraintDimensionRatio="H,13:18"

View File

@@ -20,6 +20,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="?colorSecondaryContainer" android:background="?colorSecondaryContainer"
android:clipToOutline="true"
android:foreground="?selectableItemBackground" android:foreground="?selectableItemBackground"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="H,13:18" app:layout_constraintDimensionRatio="H,13:18"

View File

@@ -25,6 +25,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:clipToOutline="true"
android:foreground="?selectableItemBackground" android:foreground="?selectableItemBackground"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="H,13:18" app:layout_constraintDimensionRatio="H,13:18"

View File

@@ -6,4 +6,5 @@
<bool name="com_samsung_android_icon_container_has_icon_container">true</bool> <bool name="com_samsung_android_icon_container_has_icon_container">true</bool>
<bool name="is_color_themes_available">false</bool> <bool name="is_color_themes_available">false</bool>
<bool name="is_sync_enabled">true</bool> <bool name="is_sync_enabled">true</bool>
<bool name="is_predictive_back_enabled">true</bool>
</resources> </resources>

View File

@@ -46,7 +46,7 @@
android:summary="@string/restore_summary" android:summary="@string/restore_summary"
android:title="@string/restore_backup" /> android:title="@string/restore_backup" />
<Preference <PreferenceScreen
android:fragment="org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment" android:fragment="org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment"
android:key="backup_periodic" android:key="backup_periodic"
android:persistent="false" android:persistent="false"

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Disable predictive back due to crashes -->
<bool name="is_predictive_back_enabled">false</bool>
</resources>

View File

@@ -28,10 +28,10 @@ leakcanary = "3.0-alpha-8"
lifecycle = "2.8.7" lifecycle = "2.8.7"
markwon = "4.6.2" markwon = "4.6.2"
material = "1.12.0" material = "1.12.0"
moshi = "1.15.1" moshi = "1.15.2"
okhttp = "4.12.0" okhttp = "4.12.0"
okio = "3.9.1" okio = "3.9.1"
parsers = "fece09b781" parsers = "794a737b6d"
preference = "1.2.1" preference = "1.2.1"
recyclerview = "1.3.2" recyclerview = "1.3.2"
room = "2.6.1" room = "2.6.1"