Compare commits

..

30 Commits

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

114
.github/workflows/auto_release.yml vendored Normal file
View File

@@ -0,0 +1,114 @@
name: Build automatic release
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-24.04
outputs:
should_build: ${{ steps.check-updates.outputs.has_changes }}
steps:
- name: Check for updates 🌏
id: check-updates
run: |
last_run=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.created_at')
kotatsu_updated=$(curl -s "https://api.github.com/repos/KotatsuApp/Kotatsu/commits?since=$last_run" | jq '. | length')
parsers_updated=$(curl -s "https://api.github.com/repos/KotatsuApp/kotatsu-parsers/commits?since=$last_run" | jq '. | length')
if [ "$kotatsu_updated" -gt "0" ] || [ "$parsers_updated" -gt "0" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
build:
needs: check
if: needs.check.outputs.should_build == 'true'
runs-on: ubuntu-24.04
outputs:
new_tag: ${{ steps.tagger.outputs.new_tag }}
steps:
- uses: actions/checkout@v3
with:
ref: autobuild
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Setup Android SDK 💻
uses: android-actions/setup-android@v3
- name: Grant permissions 💻
run: chmod a+x gradlew
- name: Generate build number 📆
id: tagger
run: |
echo "new_tag=$(./gradlew -q versionInfo -DbuildNumberIncrement=true)" >> $GITHUB_OUTPUT
echo "formatted_date=$(date +'%Y/%m/%d')" >> $GITHUB_OUTPUT
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1
with:
fileName: 'keystore/kotatsu.jks'
encodedString: ${{ secrets.ANDROID_SIGNING_KEY }}
- name: Building new APK 💻
run: >-
./gradlew assembleRelease
-DparsersVersionOverride=$(curl -s https://api.github.com/repos/kotatsuapp/kotatsu-parsers/commits/master -H "Accept: application/vnd.github.sha" | cut -c -10)
- name: Prepare to Upload 🌏
run: |
mv ${{steps.sign_app.outputs.signedFile}} app/build/outputs/apk/release/release.apk
echo "SIGNED_APK=app/build/outputs/apk/release/release.apk" >> $GITHUB_ENV
- name: Get latest changes 📑
id: changelog
run: |
CHANGELOG=$(cat CHANGELOG.txt)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create new GH Release + Uploading 🌏
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.tagger.outputs.new_tag }}
name: "Build ${{ steps.tagger.outputs.new_tag }}"
body: |
Automated build generated on ${{ steps.tagger.outputs.formatted_date }}
${{ steps.changelog.outputs.content }}
files: ${{ env.SIGNED_APK }}
prerelease: false
update:
needs: build
if: needs.check.outputs.should_build == 'true'
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with:
ref: autobuild
fetch-depth: 0
- name: Commit 🌏
run: |
git config --local user.email "autorelease@users.noreply.github.com"
git config --local user.name "autorelease"
if [[ -n $(git status -s) ]]; then
git add README.md
git commit -m "Automatic release v${{ needs.build.outputs.new_tag }}"
git push origin autobuild
else
echo "No changes to push!"
fi

View File

@@ -9,17 +9,24 @@ plugins {
id 'dagger.hilt.android.plugin'
}
def Properties versionProps = getVersionProps()
android {
compileSdk = 35
buildToolsVersion = '35.0.0'
namespace = 'org.koitharu.kotatsu'
defaultConfig {
def code = versionProps['code'].toInteger()
def base = versionProps['base'].trim()
def build = versionProps['build'].toInteger()
def variant = versionProps['variant'].trim()
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 35
versionCode = 694
versionName = '7.7.2'
versionCode = code * 1000 + build
versionName = base + (build == 0 ? '' : '.' + build) + (variant == '' ? '' : '-') + variant
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -30,6 +37,22 @@ android {
generateLocaleConfig true
}
}
signingConfigs {
release {
def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
def allFilesFromDir = new File(tmpFilePath).listFiles()
if (allFilesFromDir != null) {
def keystoreFile = allFilesFromDir.first()
keystoreFile.renameTo("keystore/kotatsu.jks")
}
storeFile = file("keystore/kotatsu.jks")
storePassword System.getenv("SIGNING_STORE_PASSWORD")
keyAlias System.getenv("SIGNING_KEY_ALIAS")
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
}
}
buildTypes {
debug {
applicationIdSuffix = '.debug'
@@ -38,6 +61,7 @@ android {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
nightly {
initWith release
@@ -194,3 +218,22 @@ dependencies {
androidTestImplementation libs.hilt.android.testing
kaptAndroidTest libs.hilt.android.compiler
}
tasks.register('versionInfo') {
def base = versionProps['base'].trim()
def build = versionProps['build'].toInteger()
def variant = versionProps['variant'].trim()
println base + (build == 0 ? '' : '.' + build) + (variant == '' ? '' : '-') + variant
}
def getVersionProps() {
def versionPropsFile = file('version.properties')
def Properties versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
if (System.getProperty('buildNumberIncrement') == 'true') {
def code = versionProps['build'].toInteger() + 1
versionProps['build'] = code.toString()
versionProps.store(versionPropsFile.newWriter(), null)
}
return versionProps
}

View File

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

View File

@@ -8,13 +8,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
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.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject

View File

@@ -1,11 +1,11 @@
package org.koitharu.kotatsu.core.db.entity
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.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
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.toTitleCase
@@ -49,7 +49,7 @@ fun Manga.toEntity() = MangaEntity(
publicUrl = publicUrl,
source = source.name,
largeCoverUrl = largeCoverUrl,
coverUrl = coverUrl,
coverUrl = coverUrl.orEmpty(),
altTitle = altTitle,
rating = rating,
isNsfw = isNsfw,

View File

@@ -14,7 +14,7 @@ data class MangaEntity(
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "public_url") val publicUrl: String,
@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 = "large_cover_url") val largeCoverUrl: String?,
@ColumnInfo(name = "state") val state: String?,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import androidx.annotation.WorkerThread
import androidx.core.util.Predicate
import okhttp3.Cookie
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.suspendCoroutine

View File

@@ -17,8 +17,8 @@ import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
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.isHttpOrHttps
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.net.HttpURLConnection
import java.util.Collections

View File

@@ -6,13 +6,13 @@ import dagger.Reusable
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.UnknownMangaSource
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.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
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.runCatchingCancellable
import javax.inject.Inject

View File

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

View File

@@ -7,8 +7,6 @@ import androidx.collection.ArraySet
import androidx.core.net.toUri
import org.jetbrains.annotations.Blocking
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.ContentType
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.SortOrder
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.splitTwoParts
import java.util.EnumSet
@@ -82,7 +81,7 @@ class ExternalPluginContentSource(
publicUrl = details.publicUrl.ifEmpty { manga.publicUrl },
rating = maxOf(details.rating, manga.rating),
isNsfw = details.isNsfw,
coverUrl = details.coverUrl.ifEmpty { manga.coverUrl },
coverUrl = details.coverUrl.ifNullOrEmpty { manga.coverUrl },
tags = details.tags + manga.tags,
state = details.state ?: manga.state,
author = details.author.ifNullOrEmpty { manga.author },

View File

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

View File

@@ -4,13 +4,14 @@ import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.core.content.edit
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.sanitizeHeaderValue
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
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
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.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
}

View File

@@ -4,21 +4,8 @@ import androidx.collection.ArrayMap
import androidx.collection.ArraySet
import androidx.collection.LongSet
import org.koitharu.kotatsu.BuildConfig
import java.util.Collections
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<*>) {
this as ArrayList<T>
} 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 {
val result = LongArray(size)
var i = 0

View File

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

View File

@@ -1,7 +1,5 @@
package org.koitharu.kotatsu.core.util.ext
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
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 {
if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) {
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 {
return if (all(Char::isValidForHeaderValue)) {
this // fast path

View File

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

View File

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

View File

@@ -2,25 +2,11 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.Context
import android.database.DatabaseUtils
import androidx.annotation.FloatRange
import androidx.collection.arraySetOf
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
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 {
UUID.fromString(this)
} catch (e: IllegalArgumentException) {
@@ -28,19 +14,35 @@ fun String.toUUIDOrNull(): UUID? = try {
null
}
fun String.digits() = filter { it.isDigit() }
/**
* @param threshold 0 = exact match
*/
fun String.almostEquals(other: String, @FloatRange(from = 0.0) threshold: Float): Boolean {
if (threshold == 0f) {
return equals(other, ignoreCase = true)
fun String.transliterate(skipMissing: Boolean): String {
val cyr = charArrayOf(
'а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'й', 'к', 'л', 'м', 'н', 'о', 'п',
'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ъ', 'ы', 'ь', 'э', 'ю', 'я', 'ё', 'ў',
)
val lat = arrayOf(
"a", "b", "v", "g", "d", "e", "zh", "z", "i", "y", "k", "l", "m", "n", "o", "p",
"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 {
return filterNot { c -> c.isReplacement() }
}
@@ -68,10 +70,11 @@ fun <T> Collection<T>.joinToStringWithLimit(context: Context, limit: Int, transf
}
}
@Deprecated("",
@Deprecated(
"",
ReplaceWith(
"sqlEscapeString(this)",
"android.database.DatabaseUtils.sqlEscapeString"
)
"android.database.DatabaseUtils.sqlEscapeString",
),
)
fun String.sqlEscape(): String = DatabaseUtils.sqlEscapeString(this)

View File

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

View File

@@ -77,7 +77,6 @@ import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
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.joinToStringWithLimit
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.MangaTag
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.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
@@ -274,7 +274,7 @@ class DetailsActivity :
startActivity(
ImageActivity.newIntent(
v.context,
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } ?: return,
manga.source,
),
scaleUpActivityOptionsOf(v),

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextColorAttr
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import com.google.android.material.R as materialR
fun pageThumbnailAD(
@@ -36,7 +37,7 @@ fun pageThumbnailAD(
AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView)
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 {
defaultPlaceholders(context)
size(thumbSize)

View File

@@ -27,7 +27,6 @@ import org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
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.observeEvent
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.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import javax.inject.Inject

View File

@@ -21,7 +21,6 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
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.DownloadWorker
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.util.mapToSet
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.settings.storage.DirectoryModel
import javax.inject.Inject

View File

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

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.explore.domain
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaRepository
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.printStackTraceDebug
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.MangaListFilter
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.suggestions.domain.TagsBlacklist
import javax.inject.Inject
@@ -77,7 +77,7 @@ class ExploreRepository @Inject constructor(
val list = repository.getList(
offset = 0,
order = order,
filter = MangaListFilter(tags = setOfNotNull(tag))
filter = MangaListFilter(tags = setOfNotNull(tag)),
).asArrayList()
if (settings.isSuggestionsExcludeNsfw) {
list.removeAll { it.isNsfw }

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.explore.ui.model
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.parsers.util.longHashCode
data class MangaSourceItem(
val source: MangaSourceInfo,

View File

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

View File

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

View File

@@ -52,7 +52,8 @@ class FavoriteSheetViewModel @Inject constructor(
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
) { 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) {
launchJob(Dispatchers.Default) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,6 @@ import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawable
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.observe
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.MangaListFilter
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.search.ui.MangaListActivity
import javax.inject.Inject
@@ -100,7 +100,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
R.id.imageView_cover -> startActivity(
ImageActivity.newIntent(
v.context,
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl } ?: return,
manga.source,
),
scaleUpActivityOptionsOf(v),

View File

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

View File

@@ -16,12 +16,12 @@ import okio.use
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
import org.koitharu.kotatsu.core.util.FileSize
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.subdir
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.takeIfWriteable
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.suspendlazy.suspendLazy
import java.io.File

View File

@@ -22,8 +22,8 @@ import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
import org.koitharu.kotatsu.core.util.ext.isFileUri
import org.koitharu.kotatsu.core.util.ext.isRegularFile
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.toFileNameSafe
import org.koitharu.kotatsu.core.util.ext.toListSorted
import org.koitharu.kotatsu.local.data.MangaIndex
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.MangaChapter
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.toCamelCase
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import java.io.File
/**
@@ -61,26 +59,31 @@ class LocalMangaParser(private val uri: Uri) {
val mangaInfo = index?.getMangaInfo()
if (mangaInfo != null) {
val coverEntry: Path? = index.getCoverEntry()?.let { rootPath / it } ?: fileSystem.findFirstImage(rootPath)
mangaInfo.copyInternal(
mangaInfo.copy(
source = LocalMangaSource,
url = rootFile.toUri().toString(),
coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() }.orEmpty(),
largeCoverUrl = null,
chapters = if (withDetails) {
mangaInfo.chapters?.map { c ->
c.copyInternal(
url = index.getChapterFileName(c.id)?.toPath()?.let {
uri.child(it, resolve = false).toString()
} ?: uri.toString(),
source = LocalMangaSource,
)
mangaInfo.chapters?.mapNotNull { c ->
val path = index.getChapterFileName(c.id)?.toPath()
if (path != null && !fileSystem.exists(rootPath / path)) {
null
} else {
c.copy(
url = path?.let {
uri.child(it, resolve = false).toString()
} ?: uri.toString(),
source = LocalMangaSource,
)
}
}
} else {
null
},
)
} else {
val title = rootFile.nameWithoutExtension.replace("_", " ").toCamelCase()
val title = rootFile.name.fileNameToTitle()
val coverEntry = fileSystem.findFirstImage(rootPath)
val mimeTypeMap = MimeTypeMap.getSingleton()
Manga(
@@ -111,7 +114,7 @@ class LocalMangaParser(private val uri: Uri) {
}.toString().removePrefix(Path.DIRECTORY_SEPARATOR)
MangaChapter(
id = "$i$s".longHashCode(),
name = s.ifEmpty { title },
name = s.fileNameToTitle().ifEmpty { title },
number = 0f,
volume = 0,
source = LocalMangaSource,
@@ -270,43 +273,8 @@ class LocalMangaParser(private val uri: Uri) {
Path.DIRECTORY_SEPARATOR + this
}.toPath()
private fun Manga.copyInternal(
url: String = this.url,
coverUrl: String = this.coverUrl,
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,
)
private fun String.fileNameToTitle() = substringBeforeLast('.')
.replace('_', ' ')
.replaceFirstChar { it.uppercase() }
}
}

View File

@@ -10,12 +10,12 @@ import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.util.ext.deleteAwait
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.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import java.io.File
class LocalMangaDirOutput(
@@ -96,7 +96,9 @@ class LocalMangaDirOutput(
}
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"
}.withIndex()
val victimsIds = ids.toMutableSet()

View File

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

View File

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

View File

@@ -152,7 +152,8 @@ class ImportService : CoroutineIntentService() {
private const val CHANNEL_ID = "importing"
private const val 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) {
val intent = Intent(context, ImportService::class.java)
intent.putExtra(DATA_URI, uri.toString())

View File

@@ -10,11 +10,11 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
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.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.findById
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Collections
import javax.inject.Inject

View File

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

View File

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

View File

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

View File

@@ -40,9 +40,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
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.sizeOrZero
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsInteractor
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.parsers.model.Manga
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.DetectReaderModeUseCase
import org.koitharu.kotatsu.reader.domain.PageLoader

View File

@@ -38,6 +38,7 @@ import org.koitharu.kotatsu.databinding.ActivityColorFilterBinding
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -137,7 +138,7 @@ class ColorFilterConfigActivity :
}
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)
.data(data)
.scale(Scale.FILL)

View File

@@ -8,9 +8,9 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
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.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import kotlin.coroutines.resume

View File

@@ -26,7 +26,6 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.getCauseUrl
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.explore.data.MangaSourcesRepository
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.toErrorState
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.sizeOrZero
import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.scrobbling.common.data
import android.content.Context
import androidx.core.content.edit
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.ScrobblerUser
@@ -31,7 +32,7 @@ class ScrobblerStorage(context: Context, service: ScrobblerService) {
ScrobblerUser(
id = lines[0].toLong(),
nickname = lines[1],
avatar = lines[2].takeUnless(String::isEmpty),
avatar = lines[2].nullIfEmpty(),
service = ScrobblerService.valueOf(lines[3]),
)
}

View File

@@ -7,6 +7,7 @@ import okhttp3.internal.closeQuietly
import okio.IOException
import org.koitharu.kotatsu.core.network.CommonHeaders
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.runCatchingCancellable
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) {
val message = runCatchingCancellable {
response.parseHtml().title().takeUnless { it.isEmpty() }
response.parseHtml().title().nullIfEmpty()
}.onFailure {
response.closeQuietly()
}.getOrNull() ?: "Invalid response (${response.code})"

View File

@@ -21,11 +21,11 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
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.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
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.ui.suggestion.model.SearchSuggestionItem
import javax.inject.Inject

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.settings
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
@@ -10,12 +11,16 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
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.settings.search.SettingsSearchMenuProvider
import org.koitharu.kotatsu.settings.search.SettingsSearchViewModel
@AndroidEntryPoint
class RootSettingsFragment : BasePreferenceFragment(0) {
private val viewModel: RootSettingsViewModel by viewModels()
private val activityViewModel: SettingsSearchViewModel by activityViewModels()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
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?) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,8 @@ import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
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
@AndroidEntryPoint
class SettingsSearchFragment : BaseFragment<FragmentSearchSuggestionBinding>(), OnListItemClickListener<SettingsItem> {
class SettingsSearchFragment : BaseFragment<FragmentSearchSuggestionBinding>(),
OnListItemClickListener<SettingsItem>,
ListListener<SettingsItem> {
private val viewModel: SettingsSearchViewModel by activityViewModels()
@@ -29,6 +33,7 @@ class SettingsSearchFragment : BaseFragment<FragmentSearchSuggestionBinding>(),
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = BaseListAdapter<SettingsItem>()
.addDelegate(ListItemType.NAV_ITEM, settingsItemAD(this))
adapter.addListListener(this)
binding.root.adapter = adapter
binding.root.setHasFixedSize(true)
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 onCurrentListChanged(
previousList: List<SettingsItem?>,
currentList: List<SettingsItem?>
) {
if (currentList.size != previousList.size) {
(viewBinding?.root?.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(0, 0)
}
}
}

View File

@@ -13,9 +13,12 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.settings.AppearanceSettingsFragment
import org.koitharu.kotatsu.settings.DownloadsSettingsFragment
import org.koitharu.kotatsu.settings.NetworkSettingsFragment
import org.koitharu.kotatsu.settings.ProxySettingsFragment
import org.koitharu.kotatsu.settings.ReaderSettingsFragment
import org.koitharu.kotatsu.settings.ServicesSettingsFragment
import org.koitharu.kotatsu.settings.SuggestionsSettingsFragment
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.tracker.TrackerSettingsFragment
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_services, emptyList(), ServicesSettingsFragment::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
}

View File

@@ -22,18 +22,20 @@ class SettingsSearchMenuProvider(
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
val currentQuery = viewModel.currentQuery
if (currentQuery.isNotEmpty()) {
if (viewModel.isSearchActive.value) {
val menuItem = menu.findItem(R.id.action_search)
menuItem.expandActionView()
val searchView = menuItem.actionView as SearchView
searchView.setQuery(currentQuery, false)
searchView.setQuery(viewModel.currentQuery, 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 {
viewModel.discardSearch()

View File

@@ -18,30 +18,43 @@ class SettingsSearchViewModel @Inject constructor(
private val searchHelper: SettingsSearchHelper,
) : BaseViewModel() {
private val query = MutableStateFlow("")
private val query = MutableStateFlow<String?>(null)
private val allSettings by lazy {
searchHelper.inflatePreferences()
}
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())
val isSearchActive = query.map {
it.isNotEmpty()
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
it != null
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, query.value != null)
val onNavigateToPreference = MutableEventFlow<SettingsItem>()
val currentQuery: String
get() = query.value
get() = query.value.orEmpty()
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) {
discardSearch()
onNavigateToPreference.call(item)
}
}

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
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.MangaTag
import org.koitharu.kotatsu.parsers.util.almostEquals
class TagsBlacklist(
private val tags: Set<String>,

View File

@@ -51,7 +51,6 @@ import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaRepository
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.awaitUniqueWorkInfoByName
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.printStackTraceDebug
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.toBitmapOrNull
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.MangaTag
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.sizeOrZero
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.tracker.domain
import android.util.Log
import coil3.request.CachePolicy
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.model.isLocal
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.local.data.LocalMangaRepository
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.tracker.domain.model.MangaTracking
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
@@ -45,8 +48,9 @@ class CheckNewChaptersUseCase @Inject constructor(
runCatchingCancellable {
repository.updateTracks()
val details = getFullManga(manga)
val chapters = details.chapters ?: 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 lastNewChapterIndex = chapters.size - track.newChapters
val lastChapter = chapters.lastOrNull()
@@ -70,7 +74,7 @@ class CheckNewChaptersUseCase @Inject constructor(
private suspend fun invokeImpl(track: MangaTracking): MangaUpdates = runCatchingCancellable {
val details = getFullManga(track.manga)
compare(track, details, getBranch(details))
compare(track, details, getBranch(details, track.lastChapterId))
}.getOrElse { error ->
MangaUpdates.Failure(
manga = track.manga,
@@ -80,9 +84,17 @@ class CheckNewChaptersUseCase @Inject constructor(
repository.saveUpdates(updates)
}
private suspend fun getBranch(manga: Manga): String? {
val history = historyRepository.getOne(manga)
return manga.getPreferredBranch(history)
private suspend fun getBranch(manga: Manga, trackChapterId: Long): String? {
historyRepository.getOne(manga)?.let {
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 {
@@ -111,25 +123,29 @@ class CheckNewChaptersUseCase @Inject constructor(
private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates.Success {
if (track.isEmpty()) {
// 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))
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 }
return when {
newChapters.isEmpty() -> {
MangaUpdates.Success(
manga = manga,
branch = branch,
newChapters = emptyList(),
isValid = chapters.lastOrNull()?.id == track.lastChapterId,
)
}
newChapters.size == chapters.size -> {
MangaUpdates.Success(manga, emptyList(), isValid = false)
MangaUpdates.Success(manga, branch, emptyList(), isValid = false)
}
else -> {
MangaUpdates.Success(manga, newChapters, isValid = true)
MangaUpdates.Success(manga, branch, newChapters, isValid = true)
}
}
}

View File

@@ -11,12 +11,12 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
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.toInstantOrNull
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
import org.koitharu.kotatsu.list.domain.ListFilterOption
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.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.toTrackingLogItem
@@ -216,7 +216,6 @@ class TrackingRepository @Inject constructor(
}
private fun TrackEntity.mergeWith(updates: MangaUpdates): TrackEntity {
val chapters = updates.manga.chapters.orEmpty()
return when (updates) {
is MangaUpdates.Failure -> TrackEntity(
mangaId = mangaId,
@@ -230,7 +229,7 @@ class TrackingRepository @Inject constructor(
is MangaUpdates.Success -> TrackEntity(
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,
lastCheckTime = System.currentTimeMillis(),
lastChapterDate = updates.lastChapterDate().ifZero { lastChapterDate },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

5
app/version.properties Normal file
View File

@@ -0,0 +1,5 @@
#Wed Jan 22 19:14:17 EET 2025
code=1
build=8
variant=
base=7.7

View File

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