Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a26587250 | ||
|
|
bb68869fe1 | ||
|
|
e60ca7115a | ||
|
|
e03a200c32 | ||
|
|
8713faa487 | ||
|
|
15e99c03a9 | ||
|
|
b425f3e779 | ||
|
|
c6a51d4d08 | ||
|
|
503bff292c | ||
|
|
0aa78c0d7e | ||
|
|
8e1d02f356 | ||
|
|
1e90d5541b | ||
|
|
04c7ca7291 | ||
|
|
8d52cab6d8 | ||
|
|
efa13df106 | ||
|
|
8bc29ac331 | ||
|
|
7991f9ca97 | ||
|
|
eb1eee1681 | ||
|
|
b3f748c000 | ||
|
|
58a9f7b25a | ||
|
|
fc1d704f6f | ||
|
|
c2c3b0f757 | ||
|
|
8d519dd80f | ||
|
|
3b5a9cd2b4 | ||
|
|
95f4d39893 | ||
|
|
f3f269c7fa | ||
|
|
40f262b0ef | ||
|
|
0f68be9663 | ||
|
|
0b8afe9c40 | ||
|
|
754ccc4197 | ||
|
|
ef691b1aed |
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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?,
|
||||||
|
|||||||
@@ -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`")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package org.koitharu.kotatsu.core.exceptions
|
||||||
|
|
||||||
|
import okio.IOException
|
||||||
|
|
||||||
|
class WrapperIOException(override val cause: Exception) : IOException(cause)
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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? {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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?,
|
||||||
|
|||||||
@@ -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?,
|
||||||
|
|||||||
@@ -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?,
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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? {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ class EditTextDefaultSummaryProvider(
|
|||||||
text
|
text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
5
app/src/release/res/values/bools.xml
Normal file
5
app/src/release/res/values/bools.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user