Compare commits

...

32 Commits
v7.6 ... v7.6.2

Author SHA1 Message Date
Koitharu
b46c00f2d0 Fix parsers version 2024-10-05 16:28:29 +03:00
Anon
9358617a3a Translated using Weblate (Serbian)
Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Serbian)

Currently translated at 96.4% (703 of 729 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-10-05 16:03:55 +03:00
Draken
ba9f31835f Translated using Weblate (Vietnamese)
Currently translated at 100.0% (732 of 732 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-10-05 16:03:55 +03:00
abc0922001
357308bfbb Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 90.5% (660 of 729 strings)

Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-10-05 16:03:55 +03:00
gekka
cab56209c1 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (732 of 732 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-10-05 16:03:55 +03:00
Oğuz Ersen
e9cd32c870 Translated using Weblate (Turkish)
Currently translated at 100.0% (732 of 732 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-10-05 16:03:55 +03:00
gallegonovato
357517ceac Translated using Weblate (Spanish)
Currently translated at 100.0% (732 of 732 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-10-05 16:03:55 +03:00
Infy's Tagalog Translations
a57fcce72b Translated using Weblate (Filipino)
Currently translated at 98.3% (717 of 729 strings)

Translated using Weblate (Filipino)

Currently translated at 98.3% (716 of 728 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-10-05 16:03:55 +03:00
Priit Jõerüüt
2e2a818c05 Translated using Weblate (Estonian)
Currently translated at 71.0% (520 of 732 strings)

Translated using Weblate (Estonian)

Currently translated at 67.9% (495 of 728 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/et/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/et/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-10-05 16:03:55 +03:00
Matt
b6f618101f Translated using Weblate (Portuguese)
Currently translated at 98.7% (719 of 728 strings)

Co-authored-by: Matt <contact.mattdev@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-10-05 16:03:55 +03:00
Koitharu
0ce368751a Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2024-10-05 16:03:07 +03:00
Koitharu
1d28538893 Fixes batch 2024-10-05 16:02:49 +03:00
Koitharu
4ad2f3f608 Fixes batch 2024-10-04 14:27:01 +03:00
Koitharu
5301cc7f97 Ability to start download paused 2024-10-04 11:20:49 +03:00
Koitharu
1290db4a7c Fixes batch 2024-10-04 10:23:49 +03:00
Koitharu
1f1309d934 Increase source add button size 2024-10-03 14:32:34 +03:00
Koitharu
350f1521a6 Fix warnings and cleanup 2024-10-03 13:08:09 +03:00
mnv
cebce20bed Fix MangaSource import and improve user agent handling
MangaSource class was imported twice from different packages.
2024-10-03 09:16:30 +03:00
Koitharu
e5b6947586 Fix StrictMode errors 2024-10-02 16:39:21 +03:00
Koitharu
ac96c49b60 Strict mode notificaiton for debug build 2024-10-02 15:13:54 +03:00
Koitharu
a4345a40bf Update dependencies 2024-10-02 11:34:51 +03:00
Koitharu
f518acb8ee Skip error for local manga list (close #1113, close #1115) 2024-09-29 19:46:48 +03:00
大王叫我来巡山
b39a51d497 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (728 of 728 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-09-29 19:43:34 +03:00
Felipe Nascimento
8819d8b1ee Translated using Weblate (Portuguese)
Currently translated at 98.6% (718 of 728 strings)

Co-authored-by: Felipe Nascimento <f.kgb@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-09-29 19:43:34 +03:00
Draken
05a502b89a Translated using Weblate (Vietnamese)
Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (724 of 724 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-09-29 19:43:34 +03:00
gekka
c320e3c26a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.8% (723 of 724 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-09-29 19:43:34 +03:00
Matt
938849c31e Translated using Weblate (Japanese)
Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Matt <contact.mattdev@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ja/
Translation: Kotatsu/plurals
2024-09-29 19:43:34 +03:00
Oğuz Ersen
95c243daa1 Translated using Weblate (Turkish)
Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (724 of 724 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-09-29 19:43:34 +03:00
gallegonovato
6ce6a02b56 Translated using Weblate (Spanish)
Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (724 of 724 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-09-29 19:43:34 +03:00
Koitharu
e92e9fb393 Update SSIV 2024-09-29 19:43:09 +03:00
Koitharu
f4186a2787 Remove loggers and reorganize settings 2024-09-27 14:40:31 +03:00
Koitharu
8b93b699d3 Update readme 2024-09-26 16:02:52 +03:00
121 changed files with 824 additions and 1054 deletions

View File

@@ -1,8 +1,8 @@
# Kotatsu
Kotatsu is a free and open source manga reader for Android.
Kotatsu is a free and open-source manga reader for Android with built-in online content sources.
![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) ![Kotlin](https://img.shields.io/github/languages/top/KotatsuApp/Kotatsu) ![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF)](https://t.me/kotatsuapp) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5)
[![Sources count](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FKotatsuApp%2Fkotatsu-parsers%2Frefs%2Fheads%2Fmaster%2F.github%2Fsummary.yaml&query=total&label=manga%20sources&color=%23E9321C)](https://github.com/KotatsuApp/kotatsu-parsers) ![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF)](https://t.me/kotatsuapp) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) [![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu)](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
### Download
@@ -12,16 +12,15 @@ Kotatsu is a free and open source manga reader for Android.
### Main Features
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers)
* Search manga by name and genres
* Search manga by name, genres, and more filters
* Reading history and bookmarks
* Favourites organized by user-defined categories
* Favorites organized by user-defined categories
* Downloading manga and reading it offline. Third-party CBZ archives also supported
* Tablet-optimized Material You UI
* Standard and Webtoon-optimized reader
* Standard and Webtoon-optimized customizable reader
* Notifications about new chapters with updates feed
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
* Password/fingerprint protect access to the app
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices
* Password/fingerprint-protected access to the app
### Screenshots

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 35
versionCode = 673
versionName = '7.6'
versionCode = 675
versionName = '7.6.2'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -64,7 +64,7 @@ android {
}
lint {
abortOnError true
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged', 'SetJavaScriptEnabled'
disable 'MissingTranslation', 'PrivateResource', 'SetJavaScriptEnabled'
}
testOptions {
unitTests.includeAndroidResources true
@@ -82,8 +82,7 @@ afterEvaluate {
}
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:3cdd391410') {
implementation('com.github.KotatsuApp:kotatsu-parsers:6f7e1fcfb2') {
exclude group: 'org.json', module: 'json'
}
@@ -94,7 +93,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.2'
implementation 'androidx.fragment:fragment-ktx:1.8.3'
implementation 'androidx.fragment:fragment-ktx:1.8.4'
implementation 'androidx.transition:transition-ktx:1.5.1'
implementation 'androidx.collection:collection-ktx:1.4.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6'
@@ -137,7 +136,7 @@ dependencies {
implementation 'io.coil-kt:coil-base:2.7.0'
implementation 'io.coil-kt:coil-svg:2.7.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:4ec7176962'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:e04098de68'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu
import android.content.Context
import android.os.Build
import android.os.StrictMode
import androidx.fragment.app.strictmode.FragmentStrictMode
import org.koitharu.kotatsu.core.BaseApp
@@ -18,30 +19,55 @@ class KotatsuApp : BaseApp() {
}
private fun enableStrictMode() {
val notifier = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
StrictModeNotifier(this)
} else {
null
}
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build(),
StrictMode.ThreadPolicy.Builder().apply {
detectNetwork()
detectDiskWrites()
detectCustomSlowCalls()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectUnbufferedIo()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) detectResourceMismatches()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) detectExplicitGc()
penaltyLog()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
penaltyListener(notifier.executor, notifier)
}
}.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.setClassInstanceLimit(PageLoader::class.java, 1)
.setClassInstanceLimit(ReaderViewModel::class.java, 1)
.penaltyLog()
.build(),
StrictMode.VmPolicy.Builder().apply {
detectActivityLeaks()
detectLeakedSqlLiteObjects()
detectLeakedClosableObjects()
detectLeakedRegistrationObjects()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectContentUriWithoutPermission()
detectFileUriExposure()
setClassInstanceLimit(LocalMangaRepository::class.java, 1)
setClassInstanceLimit(PagesCache::class.java, 1)
setClassInstanceLimit(MangaLoaderContext::class.java, 1)
setClassInstanceLimit(PageLoader::class.java, 1)
setClassInstanceLimit(ReaderViewModel::class.java, 1)
penaltyLog()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
penaltyListener(notifier.executor, notifier)
}
}.build()
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
.detectWrongFragmentContainer()
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()
.build()
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder().apply {
detectWrongFragmentContainer()
detectFragmentTagUsage()
detectRetainInstanceUsage()
detectSetUserVisibleHint()
detectWrongNestedHierarchy()
detectFragmentReuse()
penaltyLog()
if (notifier != null) {
penaltyListener(notifier)
}
}.build()
}
}

View File

@@ -0,0 +1,64 @@
package org.koitharu.kotatsu
import android.app.Notification
import android.app.Notification.BigTextStyle
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.os.StrictMode
import android.os.strictmode.Violation
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.fragment.app.strictmode.FragmentStrictMode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import org.koitharu.kotatsu.core.ErrorReporterReceiver
import kotlin.math.absoluteValue
import androidx.fragment.app.strictmode.Violation as FragmentViolation
@RequiresApi(Build.VERSION_CODES.P)
class StrictModeNotifier(
private val context: Context,
) : StrictMode.OnVmViolationListener, StrictMode.OnThreadViolationListener, FragmentStrictMode.OnViolationListener {
val executor = Dispatchers.Default.asExecutor()
private val notificationManager by lazy {
val nm = checkNotNull(context.getSystemService<NotificationManager>())
val channel = NotificationChannel(
CHANNEL_ID,
context.getString(R.string.strict_mode),
NotificationManager.IMPORTANCE_LOW,
)
nm.createNotificationChannel(channel)
nm
}
override fun onVmViolation(v: Violation) = showNotification(v)
override fun onThreadViolation(v: Violation) = showNotification(v)
override fun onViolation(violation: FragmentViolation) = showNotification(violation)
private fun showNotification(violation: Throwable) = Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_error)
.setContentTitle(context.getString(R.string.strict_mode))
.setContentText(violation.message)
.setStyle(
BigTextStyle()
.setBigContentTitle(context.getString(R.string.strict_mode))
.setSummaryText(violation.message)
.bigText(violation.stackTraceToString()),
).setShowWhen(true)
.setContentIntent(ErrorReporterReceiver.getPendingIntent(context, violation))
.setAutoCancel(true)
.setGroup(CHANNEL_ID)
.build()
.let { notificationManager.notify(CHANNEL_ID, violation.hashCode().absoluteValue, it) }
private companion object {
const val CHANNEL_ID = "strict_mode"
}
}

View File

@@ -1,3 +1,4 @@
<resources>
<string name="app_name" translatable="false">Kotatsu Dev</string>
</resources>
<string name="strict_mode">Strict mode</string>
</resources>

View File

@@ -165,13 +165,14 @@ class AutoFixService : CoroutineIntentService() {
} else {
error.getDisplayMessage(applicationContext.resources)
},
)
.setSmallIcon(android.R.drawable.stat_notify_error)
.addAction(
).setSmallIcon(android.R.drawable.stat_notify_error)
ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent ->
notification.addAction(
R.drawable.ic_alert_outline,
applicationContext.getString(R.string.report),
ErrorReporterReceiver.getPendingIntent(applicationContext, error),
reportIntent,
)
}
}
return notification.build()
}

View File

@@ -17,9 +17,6 @@ data class Bookmark(
val percent: Float,
) : ListModel {
val directImageUrl: String?
get() = if (isImageUrlDirect()) imageUrl else null
val imageLoadData: Any
get() = if (isImageUrlDirect()) imageUrl else toMangaPage()

View File

@@ -12,7 +12,6 @@ import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.internal.userAgent
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders
@@ -45,7 +44,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
viewBinding.webView.configureForParser(userAgent)
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this)

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.core
import android.app.Application
import android.content.ContentResolver
import android.content.Context
import android.provider.SearchRecentSuggestions
import android.text.Html

View File

@@ -5,9 +5,11 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.BadParcelableException
import androidx.core.app.PendingIntentCompat
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.report
class ErrorReporterReceiver : BroadcastReceiver() {
@@ -22,12 +24,15 @@ class ErrorReporterReceiver : BroadcastReceiver() {
private const val EXTRA_ERROR = "err"
private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR"
fun getPendingIntent(context: Context, e: Throwable): PendingIntent {
fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try {
val intent = Intent(context, ErrorReporterReceiver::class.java)
intent.setAction(ACTION_REPORT)
intent.setData(Uri.parse("err://${e.hashCode()}"))
intent.putExtra(EXTRA_ERROR, e)
return checkNotNull(PendingIntentCompat.getBroadcast(context, 0, intent, 0, false))
PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)
} catch (e: BadParcelableException) {
e.printStackTraceDebug()
null
}
}
}

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.exceptions
import okhttp3.Headers
import okio.IOException
import org.koitharu.kotatsu.parsers.model.MangaSource

View File

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

View File

@@ -6,7 +6,6 @@ import androidx.activity.result.ActivityResultCaller
import androidx.annotation.StringRes
import androidx.collection.MutableScatterMap
import androidx.fragment.app.FragmentManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -19,7 +18,8 @@ import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.util.ext.restartApplication
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
@@ -124,15 +124,16 @@ class ExceptionResolver @AssistedInject constructor(
Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
return
}
MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.ignore_ssl_errors)
.setMessage(R.string.ignore_ssl_errors_summary)
.setPositiveButton(R.string.apply) { _, _ ->
buildAlertDialog(ctx) {
setTitle(R.string.ignore_ssl_errors)
setMessage(R.string.ignore_ssl_errors_summary)
setPositiveButton(R.string.apply) { _, _ ->
settings.isSSLBypassEnabled = true
Toast.makeText(ctx, R.string.settings_apply_restart_required, Toast.LENGTH_SHORT).show()
ctx.findActivity()?.finishAffinity()
}.setNegativeButton(android.R.string.cancel, null)
.show()
Toast.makeText(ctx, R.string.settings_apply_restart_required, Toast.LENGTH_LONG).show()
ctx.restartApplication()
}
setNegativeButton(android.R.string.cancel, null)
}.show()
}
private inline fun Host.withContext(block: Context.() -> Unit) {

View File

@@ -1,20 +1,31 @@
package org.koitharu.kotatsu.core.fs
import android.os.Build
import org.koitharu.kotatsu.core.util.iterator.CloseableIterator
import androidx.annotation.RequiresApi
import org.koitharu.kotatsu.core.util.CloseableSequence
import org.koitharu.kotatsu.core.util.iterator.MappingIterator
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
class FileSequence(private val dir: File) : Sequence<File> {
sealed interface FileSequence : CloseableSequence<File> {
override fun iterator(): Iterator<File> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val stream = Files.newDirectoryStream(dir.toPath())
CloseableIterator(MappingIterator(stream.iterator(), Path::toFile), stream)
} else {
dir.listFiles().orEmpty().iterator()
}
@RequiresApi(Build.VERSION_CODES.O)
class StreamImpl(dir: File) : FileSequence {
private val stream = Files.newDirectoryStream(dir.toPath())
override fun iterator(): Iterator<File> = MappingIterator(stream.iterator(), Path::toFile)
override fun close() = stream.close()
}
class ListImpl(dir: File) : FileSequence {
private val list = dir.listFiles().orEmpty()
override fun iterator(): Iterator<File> = list.iterator()
override fun close() = Unit
}
}

View File

@@ -1,148 +0,0 @@
package org.koitharu.kotatsu.core.logs
import android.content.Context
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.core.util.ext.subdir
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File
import java.io.FileOutputStream
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
import java.util.concurrent.ConcurrentLinkedQueue
private const val DIR = "logs"
private const val FLUSH_DELAY = 2_000L
private const val MAX_SIZE_BYTES = 1024 * 1024 // 1 MB
class FileLogger(
context: Context,
private val settings: AppSettings,
name: String,
) {
val file by lazy {
val dir = context.getExternalFilesDir(DIR) ?: context.filesDir.subdir(DIR)
File(dir, "$name.log")
}
val isEnabled: Boolean
get() = settings.isLoggingEnabled
private val dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withLocale(Locale.ROOT)
private val buffer = ConcurrentLinkedQueue<String>()
private val mutex = Mutex()
private var flushJob: Job? = null
fun log(message: String, e: Throwable? = null) {
if (!isEnabled) {
return
}
val text = buildString {
append(dateTimeFormatter.format(LocalDateTime.now()))
append(": ")
if (e != null) {
append("E!")
}
append(message)
if (e != null) {
append(' ')
append(e.stackTraceToString())
appendLine()
}
}
buffer.add(text)
postFlush()
}
inline fun log(messageProducer: () -> String) {
if (isEnabled) {
log(messageProducer())
}
}
suspend fun flush() {
if (!isEnabled) {
return
}
flushJob?.cancelAndJoin()
flushImpl()
}
@WorkerThread
fun flushBlocking() {
if (!isEnabled) {
return
}
runBlockingSafe { flushJob?.cancelAndJoin() }
runBlockingSafe { flushImpl() }
}
private fun postFlush() {
if (flushJob?.isActive == true) {
return
}
flushJob = processLifecycleScope.launch(Dispatchers.Default) {
delay(FLUSH_DELAY)
runCatchingCancellable {
flushImpl()
}.onFailure {
it.printStackTraceDebug()
}
}
}
private suspend fun flushImpl() = withContext(NonCancellable) {
mutex.withLock {
if (buffer.isEmpty()) {
return@withContext
}
runInterruptible(Dispatchers.IO) {
if (file.length() > MAX_SIZE_BYTES) {
rotate()
}
FileOutputStream(file, true).use {
while (true) {
val message = buffer.poll() ?: break
it.write(message.toByteArray())
it.write('\n'.code)
}
it.flush()
}
}
}
}
@WorkerThread
private fun rotate() {
val length = file.length()
val bakFile = File(file.parentFile, file.name + ".bak")
file.renameTo(bakFile)
bakFile.inputStream().use { input ->
input.skip(length - MAX_SIZE_BYTES / 2)
file.outputStream().use { output ->
input.copyTo(output)
output.flush()
}
}
bakFile.delete()
}
private inline fun runBlockingSafe(crossinline block: suspend () -> Unit) = try {
runBlocking(NonCancellable) { block() }
} catch (_: InterruptedException) {
}
}

View File

@@ -1,11 +0,0 @@
package org.koitharu.kotatsu.core.logs
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class TrackerLogger
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class SyncLogger

View File

@@ -1,40 +0,0 @@
package org.koitharu.kotatsu.core.logs
import android.content.Context
import androidx.collection.arraySetOf
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ElementsIntoSet
import org.koitharu.kotatsu.core.prefs.AppSettings
@Module
@InstallIn(SingletonComponent::class)
object LoggersModule {
@Provides
@TrackerLogger
fun provideTrackerLogger(
@ApplicationContext context: Context,
settings: AppSettings,
) = FileLogger(context, settings, "tracker")
@Provides
@SyncLogger
fun provideSyncLogger(
@ApplicationContext context: Context,
settings: AppSettings,
) = FileLogger(context, settings, "sync")
@Provides
@ElementsIntoSet
fun provideAllLoggers(
@TrackerLogger trackerLogger: FileLogger,
@SyncLogger syncLogger: FileLogger,
): Set<@JvmSuppressWildcards FileLogger> = arraySetOf(
trackerLogger,
syncLogger,
)
}

View File

@@ -74,7 +74,7 @@ interface NetworkModule {
if (settings.isSSLBypassEnabled) {
disableCertificateVerification()
} else {
installExtraCertsificates(contextProvider.get())
installExtraCertificates(contextProvider.get())
}
cache(cache)
addInterceptor(GZipInterceptor())

View File

@@ -35,7 +35,7 @@ fun OkHttpClient.Builder.disableCertificateVerification() = also { builder ->
}
}
fun OkHttpClient.Builder.installExtraCertsificates(context: Context) = also { builder ->
fun OkHttpClient.Builder.installExtraCertificates(context: Context) = also { builder ->
val certificatesBuilder = HandshakeCertificates.Builder()
.addPlatformTrustedCertificates()
val assets = context.assets.list("").orEmpty()

View File

@@ -7,6 +7,7 @@ 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
@@ -30,6 +31,7 @@ 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
@@ -86,7 +88,7 @@ class MangaLoaderContextImpl @Inject constructor(
result.compressTo(it.outputStream())
}.asResponseBody("image/jpeg".toMediaType())
}
} ?: error("Cannot decode bitmap")
} ?: throw ImageDecodeException(response.request.url.toString(), response.mimeType)
}
}

View File

@@ -8,7 +8,6 @@ import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Favicons
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -17,13 +16,10 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource
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.SuspendLazy
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Locale
class ParserMangaRepository(
private val parser: MangaParser,

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.parsers.util.SuspendLazy
import java.util.EnumSet
class ExternalMangaRepository(
private val contentResolver: ContentResolver,
contentResolver: ContentResolver,
override val source: ExternalMangaSource,
cache: MemoryContentCache,
) : CachingMangaRepository(cache) {

View File

@@ -239,9 +239,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
} ?: EnumSet.allOf(SearchSuggestionType::class.java)
val isLoggingEnabled: Boolean
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
var isBiometricProtectionEnabled: Boolean
get() = prefs.getBoolean(KEY_PROTECT_APP_BIOMETRIC, true)
set(value) = prefs.edit { putBoolean(KEY_PROTECT_APP_BIOMETRIC, value) }
@@ -665,7 +662,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging"
const val KEY_SOURCES_GRID = "sources_grid"
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
const val KEY_TIPS_CLOSED = "tips_closed"
@@ -709,9 +705,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_APP_VERSION = "app_version"
const val KEY_IGNORE_DOZE = "ignore_dose"
const val KEY_TRACKER_DEBUG = "tracker_debug"
const val KEY_LOGS_SHARE = "logs_share"
const val KEY_APP_UPDATE = "app_update"
const val KEY_APP_TRANSLATION = "about_app_translation"
const val KEY_LINK_WEBLATE = "about_app_translation"
const val KEY_LINK_TELEGRAM = "about_telegram"
const val KEY_LINK_GITHUB = "about_github"
const val KEY_LINK_MANUAL = "about_help"
const val PROXY_TEST = "proxy_test"
// old keys are for migration only

View File

@@ -80,11 +80,11 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
(activity as? SettingsActivity)?.setSectionTitle(title)
}
protected fun startActivitySafe(intent: Intent) {
try {
startActivity(intent)
} catch (_: ActivityNotFoundException) {
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
protected fun startActivitySafe(intent: Intent): Boolean = try {
startActivity(intent)
true
} catch (_: ActivityNotFoundException) {
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
false
}
}

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.util.Collections
import org.koitharu.kotatsu.parsers.util.move
import java.util.LinkedList
open class ReorderableListAdapter<T : ListModel> : ListDelegationAdapter<List<T>>(), FlowCollector<List<T>?> {
@@ -28,13 +28,17 @@ open class ReorderableListAdapter<T : ListModel> : ListDelegationAdapter<List<T>
listListeners.forEach { it.onCurrentListChanged(oldList, newList) }
}
@Deprecated("Use emit() to dispatch list updates", level = DeprecationLevel.ERROR)
override fun setItems(items: List<T>?) {
super.setItems(items)
}
@Deprecated(
message = "Use emit() to dispatch list updates",
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("emit(items)"),
)
override fun setItems(items: List<T>?) = super.setItems(items)
fun reorderItems(oldPos: Int, newPos: Int) {
Collections.swap(items ?: return, oldPos, newPos)
val reordered = items?.toMutableList() ?: return
reordered.move(oldPos, newPos)
super.setItems(reordered)
notifyItemMoved(oldPos, newPos)
}

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.core.ui.dialog
import android.content.Context
import androidx.annotation.UiContext
import org.koitharu.kotatsu.R
object CommonAlertDialogs {
fun showDownloadConfirmation(
@UiContext context: Context,
onConfirmed: (startPaused: Boolean) -> Unit,
) = buildAlertDialog(context, isCentered = true) {
var startPaused = false
setTitle(R.string.save_manga)
setIcon(R.drawable.ic_download)
setMessage(R.string.save_manga_confirm)
setCheckbox(R.string.start_download, true) { _, isChecked ->
startPaused = !isChecked
}
setPositiveButton(R.string.save) { _, _ ->
onConfirmed(startPaused)
}
setNegativeButton(android.R.string.cancel, null)
}.show()
}

View File

@@ -0,0 +1,3 @@
package org.koitharu.kotatsu.core.util
interface CloseableSequence<T> : Sequence<T>, AutoCloseable

View File

@@ -2,12 +2,10 @@ package org.koitharu.kotatsu.core.util
import android.content.Context
import android.net.Uri
import android.widget.Toast
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.model.appUrl
import org.koitharu.kotatsu.parsers.model.Manga
import java.io.File
@@ -84,25 +82,4 @@ class ShareHelper(private val context: Context) {
.setChooserTitle(R.string.share)
.startChooser()
}
fun shareLogs(loggers: Collection<FileLogger>) {
val intentBuilder = ShareCompat.IntentBuilder(context)
.setType(TYPE_TEXT)
var hasLogs = false
for (logger in loggers) {
val logFile = logger.file
if (!logFile.exists()) {
continue
}
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", logFile)
intentBuilder.addStream(uri)
hasLogs = true
}
if (hasLogs) {
intentBuilder.setChooserTitle(R.string.share_logs)
intentBuilder.startChooser()
} else {
Toast.makeText(context, R.string.nothing_here, Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -7,10 +7,12 @@ import android.app.ActivityManager
import android.app.ActivityManager.MemoryInfo
import android.app.ActivityOptions
import android.app.LocaleConfig
import android.content.ComponentName
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Context.POWER_SERVICE
import android.content.ContextWrapper
import android.content.Intent
import android.content.OperationApplicationException
import android.content.SharedPreferences
import android.content.SyncResult
@@ -33,6 +35,7 @@ import androidx.annotation.IntegerRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDialog
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@@ -61,6 +64,7 @@ import okio.use
import org.json.JSONException
import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
@@ -274,3 +278,10 @@ fun WebView.configureForParser(userAgentOverride: String?) = with(settings) {
userAgentString = userAgentOverride
}
}
fun Context.restartApplication() {
val activity = findActivity()
val intent = Intent.makeRestartActivityTask(ComponentName(this, MainActivity::class.java))
startActivity(intent)
activity?.finishAndRemoveTask()
}

View File

@@ -92,7 +92,7 @@ fun LongSet.toLongArray(): LongArray {
return result
}
fun LongSet.toSet(): Set<Long> = toCollection(ArraySet<Long>(size))
fun LongSet.toSet(): Set<Long> = toCollection(ArraySet(size))
fun <R : MutableCollection<Long>> LongSet.toCollection(out: R): R = out.also { result ->
forEach(result::add)

View File

@@ -15,7 +15,6 @@ import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.fs.FileSequence
import java.io.File
import java.io.FileFilter
import java.io.InputStream
import java.nio.file.attribute.BasicFileAttributes
import java.util.zip.ZipEntry
@@ -87,9 +86,13 @@ suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) {
walkCompat(includeDirectories = false).sumOf { it.length() }
}
fun File.children() = FileSequence(this)
inline fun <R> File.withChildren(block: (children: Sequence<File>) -> R): R = FileSequence(this).use(block)
fun Sequence<File>.filterWith(filter: FileFilter): Sequence<File> = filter { f -> filter.accept(f) }
fun FileSequence(dir: File): FileSequence = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
FileSequence.StreamImpl(dir)
} else {
FileSequence.ListImpl(dir)
}
val File.creationTime
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

View File

@@ -17,7 +17,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.transformWhile
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger

View File

@@ -29,7 +29,7 @@ fun Context.getThemeColor(
@Px
fun Context.getThemeDimensionPixelSize(
@AttrRes resId: Int,
@ColorInt fallback: Int = 0,
@Px fallback: Int = 0,
) = obtainStyledAttributes(intArrayOf(resId)).use {
it.getDimensionPixelSize(0, fallback)
}
@@ -37,7 +37,7 @@ fun Context.getThemeDimensionPixelSize(
@Px
fun Context.getThemeDimensionPixelOffset(
@AttrRes resId: Int,
@ColorInt fallback: Int = 0,
@Px fallback: Int = 0,
) = obtainStyledAttributes(intArrayOf(resId)).use {
it.getDimensionPixelOffset(0, fallback)
}

View File

@@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import android.content.res.Resources
import androidx.annotation.DrawableRes
import coil.network.HttpException
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import okio.FileNotFoundException
import okio.IOException
import okio.ProtocolException
@@ -80,6 +81,11 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is UnknownHostException,
is SocketTimeoutException -> resources.getString(R.string.network_error)
is ImageDecodeException -> resources.getString(
R.string.error_image_format,
format.ifNullOrEmpty { resources.getString(R.string.unknown) },
)
is NoDataReceivedException -> resources.getString(R.string.error_no_data_received)
is IncompatiblePluginException -> resources.getString(R.string.plugin_incompatible)
is WrongPasswordException -> resources.getString(R.string.wrong_password)
@@ -107,6 +113,18 @@ fun Throwable.getDisplayIcon() = when (this) {
else -> R.drawable.ic_error_large
}
fun Throwable.getCauseUrl(): String? = when (this) {
is ParseException -> url
is NotFoundException -> url
is TooManyRequestExceptions -> url
is CaughtException -> cause?.getCauseUrl()
is CloudFlareBlockedException -> url
is CloudFlareProtectedException -> url
is HttpStatusException -> url
is HttpException -> response.request.url.toString()
else -> null
}
private fun getHttpDisplayMessage(statusCode: Int, resources: Resources): String? = when (statusCode) {
404 -> resources.getString(R.string.not_found_404)
in 500..599 -> resources.getString(R.string.server_error, statusCode)

View File

@@ -1,36 +0,0 @@
package org.koitharu.kotatsu.core.util.iterator
import okhttp3.internal.closeQuietly
import okio.Closeable
class CloseableIterator<T>(
private val upstream: Iterator<T>,
private val closeable: Closeable,
) : Iterator<T>, Closeable {
private var isClosed = false
override fun hasNext(): Boolean {
val result = upstream.hasNext()
if (!result) {
close()
}
return result
}
override fun next(): T {
try {
return upstream.next()
} catch (e: NoSuchElementException) {
close()
throw e
}
}
override fun close() {
if (!isClosed) {
closeable.closeQuietly()
isClosed = true
}
}
}

View File

@@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.zip
import androidx.annotation.WorkerThread
import androidx.collection.ArraySet
import okio.Closeable
import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.withChildren
import java.io.File
import java.io.FileInputStream
import java.util.concurrent.atomic.AtomicBoolean
import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@@ -17,7 +18,7 @@ class ZipOutput(
) : Closeable {
private val entryNames = ArraySet<String>()
private var isClosed = false
private val isClosed = AtomicBoolean(false)
private val output = ZipOutputStream(file.outputStream()).apply {
setLevel(compressionLevel)
}
@@ -72,9 +73,8 @@ class ZipOutput(
}
override fun close() {
if (!isClosed) {
if (isClosed.compareAndSet(false, true)) {
output.close()
isClosed = true
}
}
@@ -91,8 +91,10 @@ class ZipOutput(
}
putNextEntry(entry)
closeEntry()
fileToZip.children().forEach { childFile ->
appendFile(childFile, "$name/${childFile.name}")
fileToZip.withChildren { children ->
children.forEach { childFile ->
appendFile(childFile, "$name/${childFile.name}")
}
}
} else {
FileInputStream(fileToZip).use { fis ->

View File

@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
class ChaptersAdapter(
private val onItemClickListener: OnListItemClickListener<ChapterListItem>,
onItemClickListener: OnListItemClickListener<ChapterListItem>,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
private var hasVolumes = false

View File

@@ -168,6 +168,7 @@ abstract class ChaptersPagesViewModel(
downloadScheduler.schedule(
manga = requireManga(),
chaptersIds = chaptersIds,
isPaused = false,
isSilent = false,
)
onDownloadStarted.call(Unit)

View File

@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.mimeType
import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.reader.domain.PageLoader
import java.util.zip.ZipFile
import javax.inject.Inject
@@ -98,9 +99,7 @@ class MangaPageFetcher(
if (!response.isSuccessful) {
throw HttpException(response)
}
val body = checkNotNull(response.body) {
"Null response"
}
val body = response.requireBody()
val mimeType = response.mimeType
val file = body.use {
pagesCache.put(pageUrl, it.source())

View File

@@ -124,6 +124,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
binding.buttonSkip.isVisible = false
binding.buttonSkipAll.isVisible = false
binding.buttonPause.isVisible = false
}
@@ -147,6 +148,7 @@ fun downloadItemAD(
binding.buttonResume.isVisible = item.isPaused
binding.buttonResume.setText(if (item.error == null) R.string.resume else R.string.retry)
binding.buttonSkip.isVisible = item.isPaused && item.error != null
binding.buttonSkipAll.isVisible = item.isPaused && item.error != null
binding.buttonPause.isVisible = item.canPause
}
@@ -169,6 +171,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
binding.buttonSkip.isVisible = false
binding.buttonSkipAll.isVisible = false
binding.buttonPause.isVisible = false
}
@@ -182,6 +185,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
binding.buttonSkip.isVisible = false
binding.buttonSkipAll.isVisible = false
binding.buttonPause.isVisible = false
}
@@ -195,6 +199,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
binding.buttonSkip.isVisible = false
binding.buttonSkipAll.isVisible = false
binding.buttonPause.isVisible = false
}
}

View File

@@ -213,13 +213,15 @@ class DownloadNotificationFactory @AssistedInject constructor(
builder.setWhen(System.currentTimeMillis())
builder.setStyle(NotificationCompat.BigTextStyle().bigText(state.errorMessage))
if (state.error.isReportable()) {
builder.addAction(
NotificationCompat.Action(
0,
context.getString(R.string.report),
ErrorReporterReceiver.getPendingIntent(context, state.error),
),
)
ErrorReporterReceiver.getPendingIntent(context, state.error)?.let { reportIntent ->
builder.addAction(
NotificationCompat.Action(
0,
context.getString(R.string.report),
reportIntent,
),
)
}
}
}

View File

@@ -71,7 +71,6 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.TempFileFilter
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.domain.MangaLock
@@ -81,6 +80,7 @@ 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.mapToSet
import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.domain.PageLoader
import java.io.File
@@ -128,7 +128,11 @@ class DownloadWorker @AssistedInject constructor(
val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() }
val downloadedIds = getDoneChapters(manga)
return try {
withContext(PausingHandle()) {
val pausingHandle = PausingHandle()
if (inputData.getBoolean(START_PAUSED, false)) {
pausingHandle.pause()
}
withContext(pausingHandle) {
downloadMangaImpl(manga, chaptersIds, downloadedIds)
}
Result.success(currentState.toWorkData())
@@ -359,7 +363,7 @@ class DownloadWorker @AssistedInject constructor(
.use { response ->
val file = File(destination, UUID.randomUUID().toString() + ".tmp")
try {
checkNotNull(response.body).use { body ->
response.requireBody().use { body ->
file.sink(append = false).buffer().use {
it.writeAllCancellable(body.source())
}
@@ -431,10 +435,16 @@ class DownloadWorker @AssistedInject constructor(
private val settings: AppSettings,
) {
suspend fun schedule(manga: Manga, chaptersIds: Collection<Long>?, isSilent: Boolean) {
suspend fun schedule(
manga: Manga,
chaptersIds: Collection<Long>?,
isPaused: Boolean,
isSilent: Boolean,
) {
dataRepository.storeManga(manga)
val data = Data.Builder()
.putLong(MANGA_ID, manga.id)
.putBoolean(START_PAUSED, isPaused)
.putBoolean(IS_SILENT, isSilent)
if (!chaptersIds.isNullOrEmpty()) {
data.putLongArray(CHAPTERS_IDS, chaptersIds.toLongArray())
@@ -442,11 +452,15 @@ class DownloadWorker @AssistedInject constructor(
scheduleImpl(listOf(data.build()))
}
suspend fun schedule(manga: Collection<Manga>) {
suspend fun schedule(
manga: Collection<Manga>,
isPaused: Boolean,
) {
val data = manga.map {
dataRepository.storeManga(it)
Data.Builder()
.putLong(MANGA_ID, it.id)
.putBoolean(START_PAUSED, isPaused)
.build()
}
scheduleImpl(data)
@@ -556,6 +570,7 @@ class DownloadWorker @AssistedInject constructor(
const val MANGA_ID = "manga_id"
const val CHAPTERS_IDS = "chapters"
const val IS_SILENT = "silent"
const val START_PAUSED = "paused"
const val TAG = "download"
}
}

View File

@@ -10,22 +10,27 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
import org.koitharu.kotatsu.list.ui.model.ListModel
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.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaTag
@HiltViewModel(assistedFactory = TagsCatalogViewModel.Factory::class)
class TagsCatalogViewModel @AssistedInject constructor(
@Assisted private val filter: FilterCoordinator,
@Assisted private val isExcluded: Boolean,
private val mangaDataRepository: MangaDataRepository,
) : BaseViewModel() {
val searchQuery = MutableStateFlow("")
@@ -33,23 +38,13 @@ class TagsCatalogViewModel @AssistedInject constructor(
private val filterProperty: StateFlow<FilterProperty<MangaTag>>
get() = if (isExcluded) filter.tagsExcluded else filter.tags
@Suppress("RemoveExplicitTypeArguments")
private val tags: StateFlow<List<ListModel>> = combine(
filter.getAllTags(),
flow<Collection<MangaTag>> { emit(emptyList()); emit(mangaDataRepository.findTags(filter.mangaSource)) },
filterProperty.map { it.selectedItems },
) { all, selected ->
all.fold(
onSuccess = {
it.map { tag ->
TagCatalogItem(
tag = tag,
isChecked = tag in selected,
)
}
},
onFailure = {
listOf(it.toErrorState(false))
},
)
) { available, cached, selected ->
buildList(available, cached, selected)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
val content = combine(tags, searchQuery) { raw, query ->
@@ -66,6 +61,50 @@ class TagsCatalogViewModel @AssistedInject constructor(
}
}
private fun buildList(
available: Result<List<MangaTag>>,
cached: Collection<MangaTag>,
selected: Set<MangaTag>,
): List<ListModel> {
val capacity = (available.getOrNull()?.size ?: 1) + cached.size
val result = ArrayList<ListModel>(capacity)
val added = HashSet<String>(capacity)
available.getOrNull()?.forEach { tag ->
if (added.add(tag.title)) {
result.add(
TagCatalogItem(
tag = tag,
isChecked = tag in selected,
),
)
}
}
cached.forEach { tag ->
if (added.add(tag.title)) {
result.add(
TagCatalogItem(
tag = tag,
isChecked = tag in selected,
),
)
}
}
if (result.isNotEmpty()) {
val locale = (filter.mangaSource as? MangaParserSource)?.locale
result.sortWith(compareBy(TagTitleComparator(locale)) { (it as TagCatalogItem).tag })
}
available.exceptionOrNull()?.let { error ->
result.add(
if (result.isEmpty()) {
error.toErrorState(canRetry = false)
} else {
error.toErrorFooter()
},
)
}
return result
}
@AssistedFactory
interface Factory {
fun create(filter: FilterCoordinator, isExcludeTag: Boolean): TagsCatalogViewModel

View File

@@ -27,6 +27,7 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.ui.list.FitHeightGridLayoutManager
import org.koitharu.kotatsu.core.ui.list.FitHeightLinearLayoutManager
@@ -238,6 +239,7 @@ abstract class MangaListFragment :
}
override fun onFilterOptionClick(option: ListFilterOption) {
selectionController?.clear()
(viewModel as? QuickFilterListener)?.toggleFilterOption(option)
}
@@ -322,8 +324,11 @@ abstract class MangaListFragment :
}
R.id.action_save -> {
viewModel.download(selectedItems)
mode?.finish()
val itemsSnapshot = selectedItems
CommonAlertDialogs.showDownloadConfirmation(context ?: return false) { startPaused ->
mode?.finish()
viewModel.download(itemsSnapshot, isPaused = startPaused)
}
true
}

View File

@@ -22,7 +22,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
abstract class MangaListViewModel(
private val settings: AppSettings,
@@ -47,9 +46,9 @@ abstract class MangaListViewModel(
abstract fun onRetry()
fun download(items: Set<Manga>) {
fun download(items: Set<Manga>, isPaused: Boolean) {
launchJob(Dispatchers.Default) {
downloadScheduler.schedule(items)
downloadScheduler.schedule(items, isPaused)
onDownloadStarted.call(Unit)
}
}

View File

@@ -1,7 +1,5 @@
package org.koitharu.kotatsu.list.ui.model
import androidx.annotation.DrawableRes
data class ErrorFooter(
val exception: Throwable,
) : ListModel {

View File

@@ -15,10 +15,9 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.filterWith
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.withChildren
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
@@ -77,7 +76,9 @@ class LocalMangaRepository @Inject constructor(
}
override suspend fun getFilterOptions() = MangaListFilterOptions(
availableTags = localMangaIndex.getAvailableTags().mapToSet { MangaTag(title = it, key = it, source = source) },
availableTags = localMangaIndex.getAvailableTags(
skipNsfw = settings.isNsfwContentDisabled,
).mapToSet { MangaTag(title = it, key = it, source = source) },
availableContentRating = if (!settings.isNsfwContentDisabled) {
EnumSet.of(ContentRating.SAFE, ContentRating.ADULT)
} else {
@@ -216,10 +217,15 @@ class LocalMangaRepository @Inject constructor(
}
val dirs = storageManager.getWriteableDirs()
runInterruptible(Dispatchers.IO) {
dirs.flatMap { dir ->
dir.children().filterWith(TempFileFilter())
}.forEach { file ->
file.deleteRecursively()
val filter = TempFileFilter()
dirs.forEach { dir ->
dir.withChildren { children ->
children.forEach { child ->
if (filter.accept(child)) {
child.deleteRecursively()
}
}
}
}
}
return true
@@ -230,9 +236,12 @@ class LocalMangaRepository @Inject constructor(
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
for (file in files) {
launch(dispatcher) {
val m = LocalMangaInput.ofOrNull(file)?.getManga()
if (m != null) {
send(m)
runCatchingCancellable {
LocalMangaInput.ofOrNull(file)?.getManga()
}.onFailure { e ->
e.printStackTraceDebug()
}.onSuccess { m ->
if (m != null) send(m)
}
}
}
@@ -243,7 +252,7 @@ class LocalMangaRepository @Inject constructor(
private suspend fun getAllFiles() = storageManager.getReadableDirs()
.asSequence()
.flatMap { dir ->
dir.children().filterNot { it.isHidden }
dir.withChildren { children -> children.filterNot { it.isHidden }.toList() }
}
private fun Collection<LocalManga>.unwrap(): List<Manga> = map { it.manga }

View File

@@ -83,8 +83,13 @@ class LocalMangaIndex @Inject constructor(
db.getLocalMangaIndexDao().delete(mangaId)
}
suspend fun getAvailableTags(): List<String> {
return db.getLocalMangaIndexDao().findTags()
suspend fun getAvailableTags(skipNsfw: Boolean): List<String> {
val dao = db.getLocalMangaIndexDao()
return if (skipNsfw) {
dao.findTags(isNsfw = false)
} else {
dao.findTags()
}
}
private suspend fun upsert(manga: LocalManga) {

View File

@@ -13,6 +13,9 @@ interface LocalMangaIndexDao {
@Query("SELECT title FROM local_index LEFT JOIN manga_tags ON manga_tags.manga_id = local_index.manga_id LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id WHERE title IS NOT NULL GROUP BY title")
suspend fun findTags(): List<String>
@Query("SELECT title FROM local_index LEFT JOIN manga_tags ON manga_tags.manga_id = local_index.manga_id LEFT JOIN tags ON tags.tag_id = manga_tags.tag_id WHERE (SELECT nsfw FROM manga WHERE manga.manga_id = local_index.manga_id) = :isNsfw AND title IS NOT NULL GROUP BY title")
suspend fun findTags(isNsfw: Boolean): List<String>
@Upsert
suspend fun upsert(entity: LocalMangaIndexEntity)

View File

@@ -6,11 +6,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.util.AlphanumComparator
import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.creationTime
import org.koitharu.kotatsu.core.util.ext.longHashCode
import org.koitharu.kotatsu.core.util.ext.toListSorted
import org.koitharu.kotatsu.core.util.ext.walkCompat
import org.koitharu.kotatsu.core.util.ext.withChildren
import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.hasCbzExtension
import org.koitharu.kotatsu.local.data.hasImageExtension
@@ -101,13 +101,14 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.IO) {
val file = chapter.url.toUri().toFile()
if (file.isDirectory) {
file.children()
.filter { it.isFile && hasImageExtension(it) }
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map {
val pageUri = it.toUri().toString()
MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource)
}
file.withChildren { children ->
children
.filter { it.isFile && hasImageExtension(it) }
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
}.map {
val pageUri = it.toUri().toString()
MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource)
}
} else {
ZipFile(file).use { zip ->
zip.entries()
@@ -153,6 +154,6 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
}
private fun File.isChapterDirectory(): Boolean {
return isDirectory && children().any { hasImageExtension(it) }
return isDirectory && withChildren { children -> children.any { hasImageExtension(it) } }
}
}

View File

@@ -129,7 +129,7 @@ class LocalMangaDirOutput(
index.getChapterFileName(chapter.value.id)?.let {
return it
}
val baseName = "${chapter.index}_${chapter.value.name.toFileNameSafe()}".take(18)
val baseName = "${chapter.index}_${chapter.value.name.toFileNameSafe()}".take(32)
var i = 0
while (true) {
val name = (if (i == 0) baseName else baseName + "_$i") + ".cbz"

View File

@@ -4,7 +4,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.internal.format
import okio.Closeable
import org.koitharu.kotatsu.core.prefs.DownloadFormat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug

View File

@@ -139,11 +139,13 @@ class ImportService : CoroutineIntentService() {
notification.setContentTitle(applicationContext.getString(R.string.error_occurred))
.setContentText(error.getDisplayMessage(applicationContext.resources))
.setSmallIcon(android.R.drawable.stat_notify_error)
.addAction(
ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent ->
notification.addAction(
R.drawable.ic_alert_outline,
applicationContext.getString(R.string.report),
ErrorReporterReceiver.getPendingIntent(applicationContext, error),
reportIntent,
)
}
}
return notification.build()
}

View File

@@ -10,9 +10,9 @@ import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.findById
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.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.exception.ParseException
@@ -70,10 +70,10 @@ class CoverRestoreInterceptor @Inject constructor(
}
private suspend fun restoreMangaImpl(manga: Manga): Boolean {
if (dataRepository.findMangaById(manga.id) == null) {
if (dataRepository.findMangaById(manga.id) == null || manga.isLocal) {
return false
}
val repo = repositoryFactory.create(manga.source) as? ParserMangaRepository ?: return false
val repo = repositoryFactory.create(manga.source)
val fixed = repo.find(manga) ?: return false
return if (fixed != manga) {
dataRepository.storeManga(fixed)
@@ -100,7 +100,10 @@ class CoverRestoreInterceptor @Inject constructor(
}
private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean {
val repo = repositoryFactory.create(bookmark.manga.source) as? ParserMangaRepository ?: return false
if (bookmark.manga.isLocal) {
return false
}
val repo = repositoryFactory.create(bookmark.manga.source)
val chapter = repo.getDetails(bookmark.manga).chapters?.findById(bookmark.chapterId) ?: return false
val page = repo.getPages(chapter)[bookmark.page]
val imageUrl = page.preview.ifNullOrEmpty { page.url }

View File

@@ -55,7 +55,7 @@ import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.favourites.ui.container.FavouritesContainerFragment
import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.ui.LocalIndexUpdateService
import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
@@ -352,7 +352,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
MangaPrefetchService.prefetchLast(this@MainActivity)
requestNotificationsPermission()
}
startService(Intent(this@MainActivity, LocalMangaIndex::class.java))
startService(Intent(this@MainActivity, LocalIndexUpdateService::class.java))
}
}

View File

@@ -8,7 +8,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.parsers.util.isNumeric
import org.koitharu.kotatsu.parsers.util.md5
import javax.inject.Inject

View File

@@ -56,6 +56,7 @@ import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.requireBody
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import java.util.LinkedList
@@ -233,8 +234,7 @@ class PageLoader @Inject constructor(
else -> {
val request = createPageRequest(pageUrl, page.source)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
val body = checkNotNull(response.body) { "Null response body" }
body.withProgress(progress).use {
response.requireBody().withProgress(progress).use {
cache.put(pageUrl, it.source())
}
}.toUri()

View File

@@ -19,7 +19,7 @@ import java.util.EnumMap
class ReaderManager(
private val fragmentManager: FragmentManager,
private val container: FragmentContainerView,
private val settings: AppSettings,
settings: AppSettings,
) {
private val modeMap = EnumMap<ReaderMode, Class<out BaseReaderFragment<*>>>(ReaderMode::class.java)

View File

@@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted

View File

@@ -7,7 +7,6 @@ import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.prefs.ReaderAnimation
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.reader.ui.ReaderState

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.getCauseUrl
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.withArgs
@@ -72,24 +73,23 @@ class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
if (filterCoordinator.isFilterApplied) {
filterCoordinator.reset()
} else {
openInBrowser()
openInBrowser(null)
}
}
override fun onSecondaryErrorActionClick(error: Throwable) {
openInBrowser()
openInBrowser(error.getCauseUrl())
}
private fun openInBrowser() {
val browserUrl = viewModel.browserUrl
if (browserUrl.isNullOrEmpty()) {
private fun openInBrowser(url: String?) {
if (url.isNullOrEmpty()) {
Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
.show()
} else {
startActivity(
BrowserActivity.newIntent(
requireContext(),
browserUrl,
url,
viewModel.source,
viewModel.source.getTitle(requireContext()),
),

View File

@@ -21,10 +21,10 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
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
@@ -39,7 +39,6 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter
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.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject
@@ -68,9 +67,6 @@ open class RemoteListViewModel @Inject constructor(
private var loadingJob: Job? = null
private var randomJob: Job? = null
val browserUrl: String?
get() = (repository as? ParserMangaRepository)?.domain?.let { "https://$it" }
override val content = combine(
mangaList.map { it?.skipNsfwIfNeeded() },
observeListModeWithTriggers(),
@@ -82,7 +78,7 @@ open class RemoteListViewModel @Inject constructor(
list.isNullOrEmpty() && error != null -> add(
error.toErrorState(
canRetry = true,
secondaryAction = if (error !is NotFoundException && browserUrl != null) R.string.open_in_browser else 0,
secondaryAction = if (error.getCauseUrl().isNullOrEmpty()) 0 else R.string.open_in_browser,
),
)

View File

@@ -20,7 +20,6 @@ import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.requireValue

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.widgets.TipView
@@ -184,8 +185,11 @@ class SearchActivity :
}
R.id.action_save -> {
viewModel.download(collectSelectedItems())
mode?.finish()
val itemsSnapshot = collectSelectedItems()
CommonAlertDialogs.showDownloadConfirmation(this) { startPaused ->
mode?.finish()
viewModel.download(itemsSnapshot, isPaused = startPaused)
}
true
}

View File

@@ -109,9 +109,9 @@ class SearchViewModel @Inject constructor(
retryCounter.value += 1
}
fun download(items: Set<Manga>) {
fun download(items: Set<Manga>, isPaused: Boolean) {
launchJob(Dispatchers.Default) {
downloadScheduler.schedule(items)
downloadScheduler.schedule(items, isPaused)
onDownloadStarted.call(Unit)
}
}

View File

@@ -17,7 +17,6 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.DownloadFormat
import org.koitharu.kotatsu.core.prefs.ReaderAnimation
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.resolveFile

View File

@@ -10,6 +10,7 @@ import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.preference.Preference
@@ -70,10 +71,12 @@ class SettingsActivity :
caller: PreferenceFragmentCompat,
pref: Preference,
): Boolean {
val fm = supportFragmentManager
val fragment = fm.fragmentFactory.instantiate(classLoader, pref.fragment ?: return false)
fragment.arguments = pref.extras
openFragment(fragment, isFromRoot = caller is RootSettingsFragment)
val fragmentName = pref.fragment ?: return false
openFragment(
fragmentClass = FragmentFactory.loadFragmentClass(classLoader, fragmentName),
args = pref.peekExtras(),
isFromRoot = caller is RootSettingsFragment,
)
return true
}
@@ -93,11 +96,11 @@ class SettingsActivity :
} ?: setTitle(title ?: getString(R.string.settings))
}
fun openFragment(fragment: Fragment, isFromRoot: Boolean) {
fun openFragment(fragmentClass: Class<out Fragment>, args: Bundle?, isFromRoot: Boolean) {
val hasFragment = supportFragmentManager.findFragmentById(R.id.container) != null
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.container, fragment)
replace(R.id.container, fragmentClass, args)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
if (!isMasterDetails || (hasFragment && !isFromRoot)) {
addToBackStack(null)

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.settings.about
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.core.net.toUri
import androidx.fragment.app.viewModels
import androidx.preference.Preference
@@ -14,23 +15,16 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.github.isStable
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity
import javax.inject.Inject
@AndroidEntryPoint
class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
private val viewModel by viewModels<AboutSettingsViewModel>()
@Inject
lateinit var loggers: Set<@JvmSuppressWildcards FileLogger>
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_about)
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
@@ -41,12 +35,6 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
isEnabled = VersionId(BuildConfig.VERSION_NAME).isStable
if (!isEnabled) isChecked = true
}
if (!settings.isTrackerEnabled) {
findPreference<Preference>(AppSettings.KEY_TRACKER_DEBUG)?.run {
isEnabled = false
setSummary(R.string.check_for_new_chapters_disabled)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -64,21 +52,27 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
true
}
AppSettings.KEY_APP_TRANSLATION -> {
openLink(getString(R.string.url_weblate), preference.title)
AppSettings.KEY_LINK_WEBLATE -> {
openLink(R.string.url_weblate, preference.title)
true
}
AppSettings.KEY_LOGS_SHARE -> {
ShareHelper(preference.context).shareLogs(loggers)
AppSettings.KEY_LINK_GITHUB -> {
openLink(R.string.url_github, preference.title)
true
}
AppSettings.KEY_TRACKER_DEBUG -> {
startActivity(Intent(preference.context, TrackerDebugActivity::class.java))
AppSettings.KEY_LINK_MANUAL -> {
openLink(R.string.url_user_manual, preference.title)
true
}
AppSettings.KEY_LINK_TELEGRAM -> {
if (!openLink(R.string.url_telegram, null)) {
openLink(R.string.url_telegram_web, preference.title)
}
true
}
else -> super.onPreferenceTreeClick(preference)
}
@@ -87,15 +81,15 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
private fun onUpdateAvailable(version: AppVersion?) {
if (version == null) {
Snackbar.make(listView, R.string.no_update_available, Snackbar.LENGTH_SHORT).show()
return
} else {
startActivity(Intent(requireContext(), AppUpdateActivity::class.java))
}
startActivity(Intent(requireContext(), AppUpdateActivity::class.java))
}
private fun openLink(url: String, title: CharSequence?) {
private fun openLink(@StringRes url: Int, title: CharSequence?): Boolean {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = url.toUri()
startActivitySafe(
intent.data = getString(url).toUri()
return startActivitySafe(
if (title != null) {
Intent.createChooser(intent, title)
} else {

View File

@@ -15,7 +15,6 @@ import org.koitharu.kotatsu.core.backup.BackupZipOutput
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File
import java.io.FileDescriptor
import java.io.FileInputStream

View File

@@ -1,8 +1,6 @@
package org.koitharu.kotatsu.settings.backup
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -13,8 +11,6 @@ import org.koitharu.kotatsu.core.backup.BackupEntry
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.BackupZipInput
import org.koitharu.kotatsu.core.backup.CompositeResult
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.sources.catalog
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -16,12 +17,14 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelOffset
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import com.google.android.material.R as materialR
fun sourceCatalogItemSourceAD(
coil: ImageLoader,
@@ -39,6 +42,13 @@ fun sourceCatalogItemSourceAD(
binding.root.setOnClickListener { v ->
listener.onItemClick(item, v)
}
val basePadding = context.getThemeDimensionPixelOffset(
materialR.attr.listPreferredItemPaddingEnd,
binding.root.paddingStart,
)
binding.root.updatePaddingRelative(
end = (basePadding - context.resources.getDimensionPixelOffset(R.dimen.margin_small)).coerceAtLeast(0),
)
bind {
binding.textViewTitle.text = item.source.getTitle(context)

View File

@@ -13,7 +13,7 @@ data class SourceCatalogPage(
return other is SourceCatalogPage && other.type == type
}
override fun getChangePayload(previousState: ListModel): Any? {
override fun getChangePayload(previousState: ListModel): Any {
return ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED
}
}

View File

@@ -18,10 +18,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.model.unwrap
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject
@@ -63,8 +66,8 @@ class SourcesListProducer @Inject constructor(
}
private suspend fun buildList(): List<SourceConfigItem> {
val enabledSources = repository.getEnabledSources()
val pinned = repository.getPinnedSources()
val enabledSources = repository.getEnabledSources().filter { it.unwrap() is MangaParserSource }
val pinned = repository.getPinnedSources().mapToSet { it.name }
val isNsfwDisabled = settings.isNsfwContentDisabled
val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL
val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER)
@@ -79,7 +82,7 @@ class SourcesListProducer @Inject constructor(
isEnabled = it in enabledSet,
isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(),
isPinned = it in pinned,
isPinned = it.name in pinned,
)
}.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult)
@@ -100,7 +103,7 @@ class SourcesListProducer @Inject constructor(
isEnabled = true,
isDraggable = isReorderAvailable,
isAvailable = false,
isPinned = it in pinned,
isPinned = it.name in pinned,
)
}
}

View File

@@ -106,8 +106,11 @@ class SourcesManageFragment :
}
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) {
val fragment = SourceSettingsFragment.newInstance(item.source)
(activity as? SettingsActivity)?.openFragment(fragment, false)
(activity as? SettingsActivity)?.openFragment(
fragmentClass = SourceSettingsFragment::class.java,
args = Bundle(1).apply { putString(SourceSettingsFragment.EXTRA_SOURCE, item.source.name) },
isFromRoot = false,
)
}
override fun onItemLiftClick(item: SourceConfigItem.SourceItem) {

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet
import org.koitharu.kotatsu.settings.utils.DozeHelper
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
import org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity
import org.koitharu.kotatsu.tracker.work.TrackerNotificationHelper
import javax.inject.Inject
@@ -116,6 +117,11 @@ class TrackerSettingsFragment :
true
}
AppSettings.KEY_TRACKER_DEBUG -> {
startActivity(Intent(preference.context, TrackerDebugActivity::class.java))
true
}
else -> super.onPreferenceTreeClick(preference)
}
}

View File

@@ -1,62 +0,0 @@
package org.koitharu.kotatsu.settings.utils
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.TooltipCompat
import androidx.core.net.toUri
import androidx.core.view.forEach
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.PreferenceAboutLinksBinding
class AboutLinksPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : Preference(context, attrs), View.OnClickListener {
init {
layoutResource = R.layout.preference_about_links
isSelectable = false
isPersistent = false
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val binding = PreferenceAboutLinksBinding.bind(holder.itemView)
binding.root.forEach { button ->
TooltipCompat.setTooltipText(button, button.contentDescription)
button.setOnClickListener(this)
}
}
override fun onClick(v: View) {
val urlResId = when (v.id) {
R.id.btn_discord -> R.string.url_discord
R.id.btn_telegram -> R.string.url_telegram
R.id.btn_github -> R.string.url_github
else -> return
}
openLink(v, v.context.getString(urlResId), v.contentDescription)
}
private fun openLink(v: View, url: String, title: CharSequence?) {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
try {
context.startActivity(
if (title != null) {
Intent.createChooser(intent, title)
} else {
intent
},
)
} catch (_: ActivityNotFoundException) {
Snackbar.make(v, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
}
}

View File

@@ -7,7 +7,7 @@ class PercentSummaryProvider : Preference.SummaryProvider<SliderPreference> {
private var percentPattern: String? = null
override fun provideSummary(preference: SliderPreference): CharSequence? {
override fun provideSummary(preference: SliderPreference): CharSequence {
val pattern = percentPattern ?: preference.context.getString(R.string.percent_string_pattern).also {
percentPattern = it
}

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.stats.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
@Entity(

View File

@@ -1,10 +1,7 @@
package org.koitharu.kotatsu.stats.data
import androidx.collection.LongIntMap
import androidx.collection.MutableLongIntMap
import androidx.room.withTransaction
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -13,7 +10,6 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.stats.domain.StatsPeriod
import org.koitharu.kotatsu.stats.domain.StatsRecord
import java.util.NavigableMap

View File

@@ -1,17 +1,9 @@
package org.koitharu.kotatsu.stats.domain
import android.content.Context
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
data class StatsRecord(
val manga: Manga?,

View File

@@ -21,7 +21,7 @@ import javax.inject.Inject
@HiltViewModel
class StatsViewModel @Inject constructor(
private val repository: StatsRepository,
private val favouritesRepository: FavouritesRepository,
favouritesRepository: FavouritesRepository,
) : BaseViewModel() {
val period = MutableStateFlow(StatsPeriod.WEEK)

View File

@@ -1,10 +1,8 @@
package org.koitharu.kotatsu.stats.ui.sheet
import androidx.collection.IntList
import androidx.collection.LongIntMap
import androidx.collection.MutableIntList
import androidx.collection.emptyIntList
import androidx.collection.emptyLongIntMap
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -15,7 +13,6 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.stats.data.StatsRepository
import org.koitharu.kotatsu.stats.domain.StatsRecord
import java.time.Instant
import java.util.concurrent.TimeUnit
import javax.inject.Inject

View File

@@ -13,6 +13,7 @@ import androidx.core.graphics.ColorUtils
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.replaceWith
import kotlin.math.atan2
import kotlin.math.sqrt
class PieChartView @JvmOverloads constructor(
@@ -130,7 +131,7 @@ class PieChartView @JvmOverloads constructor(
if (distance < chartBounds.height() / 4f || distance > chartBounds.centerX()) {
return -1
}
var touchAngle = Math.toDegrees(Math.atan2(dy, dx)).toFloat()
var touchAngle = Math.toDegrees(atan2(dy, dx)).toFloat()
if (touchAngle < 0) {
touchAngle += 360
}

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.util.ext.toRequestBody
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.parsers.util.parseRaw
import org.koitharu.kotatsu.parsers.util.removeSurrounding
import javax.inject.Inject
@@ -30,7 +31,7 @@ class SyncAuthApi @Inject constructor(
return response.parseJson().getString("token")
} else {
val code = response.code
val message = response.use { checkNotNull(it.body).string() }.removeSurrounding('"')
val message = response.parseRaw().removeSurrounding('"')
throw SyncApiException(message, code)
}
}

View File

@@ -4,7 +4,6 @@ import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import androidx.annotation.WorkerThread
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
@@ -30,11 +29,11 @@ class SyncSettings(
@set:WorkerThread
var syncURL: String
get() = account?.let {
val sync_url = accountManager.getUserData(it, KEY_SYNC_URL)
if ( !sync_url.startsWith("http://") && !sync_url.startsWith("https://") ) {
return "http://$sync_url"
val result = accountManager.getUserData(it, KEY_SYNC_URL)
if (!result.startsWith("http://") && !result.startsWith("https://")) {
return "http://$result"
}
return sync_url
return result
}.ifNullOrEmpty { defaultSyncUrl }
set(value) {
account?.let {

View File

@@ -10,6 +10,7 @@ import android.content.SyncResult
import android.content.SyncStats
import android.database.Cursor
import android.net.Uri
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.core.content.contentValuesOf
import dagger.assisted.Assisted
@@ -18,9 +19,9 @@ import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
@@ -28,10 +29,9 @@ import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.db.TABLE_MANGA
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
import org.koitharu.kotatsu.core.db.TABLE_TAGS
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.logs.SyncLogger
import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.util.ext.parseJsonOrNull
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toContentValues
import org.koitharu.kotatsu.core.util.ext.toJson
import org.koitharu.kotatsu.core.util.ext.toRequestBody
@@ -50,7 +50,6 @@ class SyncHelper @AssistedInject constructor(
@Assisted private val account: Account,
@Assisted private val provider: ContentProviderClient,
private val settings: SyncSettings,
@SyncLogger private val logger: FileLogger,
) {
private val authorityHistory = context.getString(R.string.sync_authority_history)
@@ -75,7 +74,7 @@ class SyncHelper @AssistedInject constructor(
.url("$baseUrl/resource/$TABLE_FAVOURITES")
.post(data.toRequestBody())
.build()
val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
val response = httpClient.newCall(request).execute().parseJsonOrNull()
if (response != null) {
val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES))
stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
@@ -97,7 +96,7 @@ class SyncHelper @AssistedInject constructor(
.url("$baseUrl/resource/$TABLE_HISTORY")
.post(data.toRequestBody())
.build()
val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
val response = httpClient.newCall(request).execute().parseJsonOrNull()
if (response != null) {
val result = upsertHistory(
json = response.getJSONArray(TABLE_HISTORY),
@@ -110,15 +109,12 @@ class SyncHelper @AssistedInject constructor(
}
fun onError(e: Throwable) {
if (logger.isEnabled) {
logger.log("Sync error", e)
}
e.printStackTraceDebug()
}
fun onSyncComplete(result: SyncResult) {
if (logger.isEnabled) {
logger.log("Sync finished: ${result.toDebugString()}")
logger.flushBlocking()
if (BuildConfig.DEBUG) {
Log.i("Sync", "Sync finished: ${result.toDebugString()}")
}
}
@@ -298,12 +294,6 @@ class SyncHelper @AssistedInject constructor(
private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray
private fun Response.log() = apply {
if (logger.isEnabled) {
logger.log("$code ${request.url}")
}
}
@AssistedFactory
interface Factory {

View File

@@ -43,17 +43,17 @@ fun trackDebugAD(
}
binding.textViewTitle.text = item.manga.title
binding.textViewSummary.text = buildSpannedString {
item.lastCheckTime?.let {
append(
append(
item.lastCheckTime?.let {
DateUtils.getRelativeDateTimeString(
context,
it.toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
),
)
}
)
} ?: getString(R.string.never),
)
if (item.lastResult == TrackEntity.RESULT_FAILED) {
append(" - ")
bold {

View File

@@ -16,7 +16,7 @@ import javax.inject.Inject
@HiltViewModel
class TrackerDebugViewModel @Inject constructor(
private val db: MangaDatabase
db: MangaDatabase
) : BaseViewModel() {
val content = db.getTracksDao().observeAll()

View File

@@ -45,13 +45,12 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.logs.TrackerLogger
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.TrackerDownloadStrategy
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.onEachIndexed
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.trySetForeground
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.local.data.LocalMangaRepository
@@ -80,7 +79,6 @@ class TrackWorker @AssistedInject constructor(
private val getTracksUseCase: GetTracksUseCase,
private val checkNewChaptersUseCase: CheckNewChaptersUseCase,
private val workManager: WorkManager,
@TrackerLogger private val logger: FileLogger,
private val localRepositoryLazy: Lazy<LocalMangaRepository>,
private val downloadSchedulerLazy: Lazy<DownloadWorker.Scheduler>,
) : CoroutineWorker(context, workerParams) {
@@ -90,17 +88,15 @@ class TrackWorker @AssistedInject constructor(
override suspend fun doWork(): Result {
notificationHelper.updateChannels()
val isForeground = trySetForeground()
logger.log("doWork(): attempt $runAttemptCount")
return try {
doWorkImpl(isFullRun = isForeground && TAG_ONESHOT in tags)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
logger.log("fatal", e)
e.printStackTraceDebug()
Result.failure()
} finally {
withContext(NonCancellable) {
logger.flush()
notificationManager.cancel(WORKER_NOTIFICATION_ID)
}
}
@@ -111,7 +107,6 @@ class TrackWorker @AssistedInject constructor(
return Result.success()
}
val tracks = getTracksUseCase(if (isFullRun) Int.MAX_VALUE else BATCH_SIZE)
logger.log("Total ${tracks.size} tracks")
if (tracks.isEmpty()) {
return Result.success()
}
@@ -154,7 +149,6 @@ class TrackWorker @AssistedInject constructor(
when (it) {
is MangaUpdates.Failure -> {
val e = it.error
logger.log("checkUpdatesAsync", e)
if (e is CloudFlareProtectedException) {
CaptchaNotifier(applicationContext).notify(e)
}
@@ -260,6 +254,7 @@ class TrackWorker @AssistedInject constructor(
downloadSchedulerLazy.get().schedule(
manga = mangaUpdates.manga,
chaptersIds = mangaUpdates.newChapters.mapToSet { it.id },
isPaused = false,
isSilent = true,
)
}

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 34% of 12% = ~4% -->
<item android:alpha="0.34" android:color="?attr/colorPrimary" />
</selector>

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selector_overlay">
<item
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceContainerHighest" />
<corners
android:bottomLeftRadius="@dimen/m3_card_corner"
android:bottomRightRadius="@dimen/m3_card_corner" />
<padding
android:left="@dimen/list_spacing_small"
android:right="@dimen/list_spacing_small" />
</shape>
</item>
<item
android:id="@android:id/mask"
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="@color/selector_overlay" />
<corners
android:bottomLeftRadius="@dimen/m3_card_corner"
android:bottomRightRadius="@dimen/m3_card_corner" />
</shape>
</item>
</ripple>

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selector_overlay">
<item
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceContainerHighest" />
<corners android:radius="@dimen/m3_card_corner" />
<padding
android:left="@dimen/list_spacing_small"
android:right="@dimen/list_spacing_small" />
</shape>
</item>
<item
android:id="@android:id/mask"
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="@color/selector_overlay" />
<corners android:radius="@dimen/m3_card_corner" />
</shape>
</item>
</ripple>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selector_overlay">
<item
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceContainerHighest" />
<padding
android:left="@dimen/list_spacing_small"
android:right="@dimen/list_spacing_small" />
</shape>
</item>
<item
android:id="@android:id/mask"
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="@color/selector_overlay" />
</shape>
</item>
</ripple>

View File

@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/selector_overlay">
<item
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="?colorSurfaceContainerHighest" />
<corners
android:topLeftRadius="@dimen/m3_card_corner"
android:topRightRadius="@dimen/m3_card_corner" />
<padding
android:left="@dimen/list_spacing_small"
android:right="@dimen/list_spacing_small" />
</shape>
</item>
<item
android:id="@android:id/mask"
android:left="@dimen/list_spacing_large"
android:right="@dimen/list_spacing_large">
<shape android:shape="rectangle">
<solid android:color="@color/selector_overlay" />
<corners
android:topLeftRadius="@dimen/m3_card_corner"
android:topRightRadius="@dimen/m3_card_corner" />
</shape>
</item>
</ripple>

View File

@@ -1,11 +0,0 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19.27,5.33C17.94,4.71 16.5,4.26 15,4c-0.03,0 -0.05,0.01 -0.07,0.03c-0.18,0.33 -0.39,0.76 -0.53,1.09c-1.61,-0.24 -3.22,-0.24 -4.8,0C9.46,4.78 9.25,4.36 9.06,4.03C9.05,4.01 9.02,4 8.99,4c-1.5,0.26 -2.93,0.71 -4.27,1.33c-0.01,0 -0.02,0.01 -0.03,0.02c-2.72,4.07 -3.47,8.03 -3.1,11.95c0,0.02 0.01,0.04 0.03,0.05c1.8,1.32 3.53,2.12 5.24,2.65c0.03,0.01 0.06,0 0.07,-0.02c0.4,-0.55 0.76,-1.13 1.07,-1.74c0.02,-0.04 0,-0.08 -0.04,-0.09c-0.57,-0.22 -1.11,-0.48 -1.64,-0.78c-0.04,-0.02 -0.04,-0.08 -0.01,-0.11c0.11,-0.08 0.22,-0.17 0.33,-0.25c0.02,-0.02 0.05,-0.02 0.07,-0.01c3.44,1.57 7.15,1.57 10.55,0c0.02,-0.01 0.05,-0.01 0.07,0.01c0.11,0.09 0.22,0.17 0.33,0.26c0.04,0.03 0.04,0.09 -0.01,0.11c-0.52,0.31 -1.07,0.56 -1.64,0.78c-0.04,0.01 -0.05,0.06 -0.04,0.09c0.32,0.61 0.68,1.19 1.07,1.74C17.07,20 17.1,20.01 17.13,20c1.72,-0.53 3.45,-1.33 5.25,-2.65c0.02,-0.01 0.03,-0.03 0.03,-0.05c0.44,-4.53 -0.73,-8.46 -3.1,-11.95C19.3,5.34 19.29,5.33 19.27,5.33zM8.52,14.91c-1.03,0 -1.89,-0.95 -1.89,-2.12s0.84,-2.12 1.89,-2.12c1.06,0 1.9,0.96 1.89,2.12C10.41,13.96 9.57,14.91 8.52,14.91zM15.49,14.91c-1.03,0 -1.89,-0.95 -1.89,-2.12s0.84,-2.12 1.89,-2.12c1.06,0 1.9,0.96 1.89,2.12C17.38,13.96 16.55,14.91 15.49,14.91z" />
</vector>

View File

@@ -1,11 +0,0 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z" />
</vector>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17 7V9H15V7H17M13 7V9H7V7H13M13 11H7V13H13V11M15 11V13H17V11H15M21 22L18 20L15 22L12 20L9 22L6 20L3 22V3H21V22M19 18.26V5H5V18.26L6 17.6L9 19.6L12 17.6L15 19.6L18 17.6L19 18.26Z" />
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="m196.03,328.68 l162,118.8c16.8,10.8 31.2,4.8 36,-16.8l66,-309.6c6,-26.4 -10.8,-38.4 -28.8,-30L46.03,239.88c-25.2,9.6 -25.2,25.2 -4.8,31.2l99.6,31.2 228,-145.2c10.8,-6 20.4,-3.6 13.2,4.8"
android:strokeWidth="1.2"
android:fillColor="#000000"/>
</vector>

View File

@@ -12,8 +12,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:liftOnScrollColor="@null"
app:liftOnScroll="false">
app:liftOnScroll="false"
app:liftOnScrollColor="@null">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
@@ -50,6 +50,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:bubbleSize="normal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="?attr/dialogPreferredPadding"
android:paddingVertical="16dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@string/onboard_text" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollIndicators="top|bottom"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:ignore="UnusedAttribute"
tools:listitem="@layout/item_source_locale" />
</LinearLayout>

Some files were not shown because too many files have changed in this diff Show More