Compare commits

..

41 Commits

Author SHA1 Message Date
Koitharu
8d5bde6e60 Revert "Update sources catalog ui"
This reverts commit 597ad01e8f.
2024-05-27 17:29:31 +03:00
Koitharu
bf740ddc93 Merge branch 'master' into devel 2024-05-27 17:25:57 +03:00
Koitharu
fddbf35e8c Fix up navigation button behavior 2024-05-27 15:54:34 +03:00
Koitharu
a47fea02d1 Update issue templates 2024-05-27 14:07:37 +03:00
Koitharu
250136cfdc Update parsers 2024-05-27 13:55:38 +03:00
Koitharu
597ad01e8f Update sources catalog ui 2024-05-27 13:39:34 +03:00
Koitharu
f7b44f2b0f Update parsers 2024-05-25 17:27:33 +03:00
Koitharu
5aab43ac93 Update settings activity ui 2024-05-25 17:27:32 +03:00
Koitharu
2d278159ea Fix crashes and improve predictive back support 2024-05-25 17:27:32 +03:00
Koitharu
da61462d79 Update parsers 2024-05-25 17:17:30 +03:00
Макар Разин
2ab0912880 Translated using Weblate (Polish)
Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Serbian)

Currently translated at 99.3% (641 of 645 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (644 of 645 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (645 of 645 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-05-25 16:15:06 +03:00
Scrambled777
3914616222 Translated using Weblate (Hindi)
Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (643 of 643 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-05-25 16:15:06 +03:00
maryush
a73b2703be Translated using Weblate (Polish)
Currently translated at 100.0% (643 of 643 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-05-25 16:15:06 +03:00
gekka
49590f6d02 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (645 of 645 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (643 of 643 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (643 of 643 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (643 of 643 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-05-25 16:15:06 +03:00
Koitharu
0faa97b08c Update settings activity ui 2024-05-25 16:11:21 +03:00
Koitharu
2ae488544b Fix crashes and improve predictive back support 2024-05-25 10:32:02 +03:00
Koitharu
a7e2cfc878 Udpate parsers 2024-05-24 08:30:24 +03:00
Koitharu
da6db9c1b4 Refactor descrambling bitmap 2024-05-23 16:55:41 +03:00
AwkwardPeak7
88b3e5cf34 implement basic methods for descrambling images 2024-05-23 16:28:42 +03:00
Koitharu
7347f0ba10 Pagination in history and favorites 2024-05-23 12:44:10 +03:00
Koitharu
4c55682552 Move tracker debug activity to common code 2024-05-22 16:42:24 +03:00
Koitharu
324031aa2a Update untranslatable strings 2024-05-22 14:13:14 +03:00
Koitharu
1355c3d75c Option to disable nsfw updates notifications 2024-05-22 13:05:33 +03:00
Infy's Tagalog Translations
8533168155 Translated using Weblate (Filipino)
Currently translated at 99.8% (640 of 641 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-05-22 13:05:06 +03:00
Asmodeus
51f6ec6e55 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (641 of 641 strings)

Co-authored-by: Asmodeus <colligare1Asmodeum@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-05-22 13:05:06 +03:00
Deivinni Silva
7e3f67c14d Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.1% (623 of 641 strings)

Co-authored-by: Deivinni Silva <deivinnimds3656@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-05-22 13:05:06 +03:00
gallegonovato
c51320f033 Translated using Weblate (Spanish)
Currently translated at 100.0% (641 of 641 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-05-22 13:05:06 +03:00
Hosted Weblate
9c50a47abc Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
Translation: Kotatsu/Strings
2024-05-22 13:05:06 +03:00
Scrambled777
473d273d18 Translated using Weblate (Hindi)
Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-05-22 13:05:06 +03:00
gekka
f19b628655 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (639 of 639 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-05-22 13:05:06 +03:00
Nicola Bortoletto
cdb6655e37 Translated using Weblate (Italian)
Currently translated at 93.4% (597 of 639 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2024-05-22 13:05:06 +03:00
maryush
4f19f7ebdf Translated using Weblate (Polish)
Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (638 of 638 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-05-22 13:05:06 +03:00
Koitharu
bf8838f943 Save and share manga cover #253 2024-05-22 12:33:23 +03:00
Koitharu
1e1e9fabdc Merge pull request #885 from ranzou06/devel 2024-05-20 18:51:21 +03:00
Koitharu
745972a717 Added 0ms.dev images proxy support #771 2024-05-20 17:03:18 +03:00
Koitharu
6055776329 Fix crashes 2024-05-20 11:31:00 +03:00
Koitharu
4074791f9a Resolve SSL excetpions 2024-05-20 11:18:38 +03:00
Clebio
b8283acd0d feat: Implement Spen integration for enhanced stylus support
TY Alexander!
2024-05-16 22:15:10 -03:00
90 changed files with 1188 additions and 446 deletions

View File

@@ -2,4 +2,4 @@ blank_issues_enabled: false
contact_links:
- name: ⚠️ Source issue
url: https://github.com/KotatsuApp/kotatsu-parsers/issues/new
about: If you have troubles with a manga parser or want to propose new manga source, please open an issue in the kotatsu-parsers repository instead
about: If you have a problem with a specific **manga source** or want to propose a new one, please open an issue in the kotatsu-parsers repository instead

View File

@@ -60,7 +60,7 @@ body:
attributes:
label: Acknowledgements
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
- label: This is not a duplicate of an existing issue. Please look through the list of [open issues](https://github.com/KotatsuApp/Kotatsu/issues) before creating a new one.
required: true
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
- label: This is not an issue with a specific manga source. Otherwise, you have to open an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
required: true

View File

@@ -20,5 +20,5 @@ body:
label: Acknowledgements
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
- label: This is not a duplicate of an existing issue. Please look through the list of [open issues](https://github.com/KotatsuApp/Kotatsu/issues) before creating a new one.
required: true

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 642
versionName = '7.0.1'
versionCode = 645
versionName = '7.1.2'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -82,7 +82,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:078b59b1e2') {
implementation('com.github.KotatsuApp:kotatsu-parsers:26be293f24') {
exclude group: 'org.json', module: 'json'
}
@@ -94,6 +94,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.0'
implementation 'androidx.fragment:fragment-ktx:1.7.1'
implementation 'androidx.transition:transition-ktx:1.5.0'
implementation 'androidx.collection:collection-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0'
implementation 'androidx.lifecycle:lifecycle-service:2.8.0'

View File

@@ -1,12 +0,0 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tracker.ui.debug.TrackerDebugActivity"
android:label="@string/check_for_new_chapters" />
</application>
</manifest>

View File

@@ -0,0 +1,33 @@
package org.koitharu.kotatsu.settings
import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import leakcanary.LeakCanary
import org.koitharu.kotatsu.R
import org.koitharu.workinspector.WorkInspector
class SettingsMenuProvider(
private val context: Context,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_settings, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_leaks -> {
context.startActivity(LeakCanary.newLeakDisplayActivityIntent())
true
}
R.id.action_works -> {
context.startActivity(WorkInspector.getIntent(context))
true
}
else -> false
}
}

View File

@@ -8,14 +8,9 @@
android:title="@string/leak_canary_display_activity_label"
app:showAsAction="never" />
<item
android:id="@id/action_tracker"
android:title="@string/check_for_new_chapters"
app:showAsAction="never" />
<item
android:id="@id/action_works"
android:title="Works"
android:title="@string/wi_lib_name"
app:showAsAction="never" />
</menu>

View File

@@ -100,6 +100,13 @@
<intent-filter>
<action android:name="${applicationId}.action.READ_MANGA" />
</intent-filter>
<intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter>
<meta-data
android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/remote_action" />
</activity>
<activity
android:name="org.koitharu.kotatsu.search.ui.SearchActivity"
@@ -248,6 +255,9 @@
<activity
android:name="org.koitharu.kotatsu.settings.about.AppUpdateActivity"
android:label="@string/app_update_available" />
<activity
android:name="org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity"
android:label="@string/tracker_debug_info" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"

View File

@@ -108,8 +108,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
override fun onDestroy() {
super.onDestroy()
viewBinding.webView.stopLoading()
viewBinding.webView.destroy()
if (hasViewBinding()) {
viewBinding.webView.stopLoading()
viewBinding.webView.destroy()
}
}
override fun onLoadingStateChanged(isLoading: Boolean) {

View File

@@ -27,8 +27,8 @@ import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl

View File

@@ -16,16 +16,16 @@ class MemoryContentCache @Inject constructor(application: Application) : Compone
private val isLowRam = application.isLowRamDevice()
init {
application.registerComponentCallbacks(this)
}
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
private val pagesCache =
ExpiringLruCache<SafeDeferred<List<MangaPage>>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
private val relatedMangaCache =
ExpiringLruCache<SafeDeferred<List<Manga>>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
init {
application.registerComponentCallbacks(this)
}
suspend fun getDetails(source: MangaSource, url: String): Manga? {
return detailsCache[Key(source, url)]?.awaitOrNull()
}

View File

@@ -1,36 +1,48 @@
package org.koitharu.kotatsu.core.exceptions.resolve
import android.content.Context
import android.widget.Toast
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.collection.ArrayMap
import androidx.collection.MutableScatterMap
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseActivity.BaseActivityEntryPoint
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import java.security.cert.CertPathValidatorException
import javax.net.ssl.SSLException
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
private val continuations = ArrayMap<String, Continuation<Boolean>>(1)
private val continuations = MutableScatterMap<String, Continuation<Boolean>>(1)
private val activity: FragmentActivity?
private val fragment: Fragment?
private val sourceAuthContract: ActivityResultLauncher<MangaSource>
private val cloudflareContract: ActivityResultLauncher<CloudFlareProtectedException>
val context: Context?
get() = activity ?: fragment?.context
constructor(activity: FragmentActivity) {
this.activity = activity
fragment = null
@@ -56,6 +68,12 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
suspend fun resolve(e: Throwable): Boolean = when (e) {
is CloudFlareProtectedException -> resolveCF(e)
is AuthRequiredException -> resolveAuthException(e.source)
is SSLException,
is CertPathValidatorException -> {
showSslErrorDialog()
false
}
is NotFoundException -> {
openInBrowser(e.url)
false
@@ -80,13 +98,37 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
}
private fun openInBrowser(url: String) {
val context = activity ?: fragment?.activity ?: return
context.startActivity(BrowserActivity.newIntent(context, url, null, null))
context?.run {
startActivity(BrowserActivity.newIntent(this, url, null, null))
}
}
private fun openAlternatives(manga: Manga) {
val context = activity ?: fragment?.activity ?: return
context.startActivity(AlternativesActivity.newIntent(context, manga))
context?.run {
startActivity(AlternativesActivity.newIntent(this, manga))
}
}
private fun showSslErrorDialog() {
val ctx = context ?: return
val settings = getAppSettings(ctx)
if (settings.isSSLBypassEnabled) {
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) { _, _ ->
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()
}
private fun getAppSettings(context: Context): AppSettings {
return EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(context).settings
}
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
@@ -99,6 +141,9 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
is AuthRequiredException -> R.string.sign_in
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
is SSLException,
is CertPathValidatorException -> R.string.fix
else -> 0
}

View File

@@ -4,8 +4,10 @@ import android.util.Log
import dagger.Lazy
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -13,6 +15,7 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mergeWith
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.net.IDN
import javax.inject.Inject
import javax.inject.Singleton
@@ -23,7 +26,7 @@ class CommonHeadersInterceptor @Inject constructor(
private val mangaLoaderContextLazy: Lazy<MangaLoaderContextImpl>,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
override fun intercept(chain: Chain): Response {
val request = chain.request()
val source = request.tag(MangaSource::class.java)
val repository = if (source != null) {
@@ -46,7 +49,7 @@ class CommonHeadersInterceptor @Inject constructor(
headersBuilder.trySet(CommonHeaders.REFERER, "https://$idn/")
}
val newRequest = request.newBuilder().headers(headersBuilder.build()).build()
return repository?.intercept(ProxyChain(chain, newRequest)) ?: chain.proceed(newRequest)
return repository?.interceptSafe(ProxyChain(chain, newRequest)) ?: chain.proceed(newRequest)
}
private fun Headers.Builder.trySet(name: String, value: String) = try {
@@ -55,10 +58,21 @@ class CommonHeadersInterceptor @Inject constructor(
e.printStackTraceDebug()
}
private fun Interceptor.interceptSafe(chain: Chain): Response = runCatchingCancellable {
intercept(chain)
}.getOrElse { e ->
if (e is IOException) {
throw e
} else {
// only IOException can be safely thrown from an Interceptor
throw IOException("Error in interceptor: ${e.message}", e)
}
}
private class ProxyChain(
private val delegate: Interceptor.Chain,
private val delegate: Chain,
private val request: Request,
) : Interceptor.Chain by delegate {
) : Chain by delegate {
override fun request(): Request = request
}

View File

@@ -83,6 +83,11 @@ class DoHManager(
tryGetByIp("2a10:50c0::2:ff"),
),
).build()
DoHProvider.ZERO_MS -> DnsOverHttps.Builder().client(bootstrapClient)
.url("https://0ms.dev/dns-query".toHttpUrl())
.resolvePublicAddresses(true)
.build()
}
private fun tryGetByIp(ip: String): InetAddress? = try {

View File

@@ -2,5 +2,5 @@ package org.koitharu.kotatsu.core.network
enum class DoHProvider {
NONE, GOOGLE, CLOUDFLARE, ADGUARD
}
NONE, GOOGLE, CLOUDFLARE, ADGUARD, ZERO_MS
}

View File

@@ -1,106 +0,0 @@
package org.koitharu.kotatsu.core.network
import android.util.Log
import androidx.collection.ArraySet
import coil.intercept.Interceptor
import coil.request.ErrorResult
import coil.request.ImageResult
import coil.request.SuccessResult
import coil.size.Dimension
import coil.size.isOriginal
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Collections
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ImageProxyInterceptor @Inject constructor(
private val settings: AppSettings,
) : Interceptor {
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = chain.request
if (!settings.isImagesProxyEnabled) {
return chain.proceed(request)
}
val url: HttpUrl? = when (val data = request.data) {
is HttpUrl -> data
is String -> data.toHttpUrlOrNull()
else -> null
}
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
return chain.proceed(request)
}
val newUrl = HttpUrl.Builder()
.scheme("https")
.host("wsrv.nl")
.addQueryParameter("url", url.toString())
.addQueryParameter("we", null)
val size = request.sizeResolver.size()
if (!size.isOriginal) {
newUrl.addQueryParameter("crop", "cover")
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
}
val newRequest = request.newBuilder()
.data(newUrl.build())
.build()
val result = chain.proceed(newRequest)
return if (result is SuccessResult) {
result
} else {
logDebug((result as? ErrorResult)?.throwable)
chain.proceed(request).also {
if (it is SuccessResult) {
blacklist.add(url.host)
}
}
}
}
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
if (!settings.isImagesProxyEnabled) {
return okHttp.newCall(request).await()
}
val sourceUrl = request.url
val targetUrl = HttpUrl.Builder()
.scheme("https")
.host("wsrv.nl")
.addQueryParameter("url", sourceUrl.toString())
.addQueryParameter("we", null)
val newRequest = request.newBuilder()
.url(targetUrl.build())
.build()
return runCatchingCancellable {
okHttp.doCall(newRequest)
}.recover {
logDebug(it)
okHttp.doCall(request).also {
blacklist.add(sourceUrl.host)
}
}.getOrThrow()
}
private suspend fun OkHttpClient.doCall(request: Request): Response {
return newCall(request).await().ensureSuccess()
}
private fun logDebug(e: Throwable?) {
if (BuildConfig.DEBUG) {
Log.w("ImageProxy", e.toString())
}
}
}

View File

@@ -15,6 +15,8 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.imageproxy.RealImageProxyInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread
import org.koitharu.kotatsu.local.data.LocalStorageManager
@@ -29,6 +31,9 @@ interface NetworkModule {
@Binds
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
@Binds
fun bindImageProxyInterceptor(impl: RealImageProxyInterceptor): ImageProxyInterceptor
companion object {
@Provides

View File

@@ -0,0 +1,87 @@
package org.koitharu.kotatsu.core.network.imageproxy
import android.util.Log
import androidx.collection.ArraySet
import coil.intercept.Interceptor
import coil.network.HttpException
import coil.request.ErrorResult
import coil.request.ImageRequest
import coil.request.ImageResult
import coil.request.SuccessResult
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.net.HttpURLConnection
import java.util.Collections
abstract class BaseImageProxyInterceptor : ImageProxyInterceptor {
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
final override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val request = chain.request
val url: HttpUrl? = when (val data = request.data) {
is HttpUrl -> data
is String -> data.toHttpUrlOrNull()
else -> null
}
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
return chain.proceed(request)
}
val newRequest = onInterceptImageRequest(request, url)
return when (val result = chain.proceed(newRequest)) {
is SuccessResult -> result
is ErrorResult -> {
logDebug(result.throwable, newRequest.data)
chain.proceed(request).also {
if (it is SuccessResult && result.throwable.isBlockedByServer()) {
blacklist.add(url.host)
}
}
}
}
}
final override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
val newRequest = onInterceptPageRequest(request)
return runCatchingCancellable {
okHttp.doCall(newRequest)
}.recover { error ->
logDebug(error, newRequest.url)
okHttp.doCall(request).also {
if (error.isBlockedByServer()) {
blacklist.add(request.url.host)
}
}
}.getOrThrow()
}
protected abstract suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest
protected abstract suspend fun onInterceptPageRequest(request: Request): Request
private suspend fun OkHttpClient.doCall(request: Request): Response {
return newCall(request).await().ensureSuccess()
}
private fun logDebug(e: Throwable, url: Any) {
if (BuildConfig.DEBUG) {
Log.w("ImageProxy", "${e.message}: $url", e)
}
}
private fun Throwable.isBlockedByServer(): Boolean {
return this is CloudFlareBlockedException
|| (this is HttpException && response.code == HttpURLConnection.HTTP_FORBIDDEN)
|| (this is HttpStatusException && statusCode == HttpURLConnection.HTTP_FORBIDDEN)
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.network.imageproxy
import coil.intercept.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
interface ImageProxyInterceptor : Interceptor {
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response
}

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.core.network.imageproxy
import coil.intercept.Interceptor
import coil.request.ImageResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.parsers.util.await
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RealImageProxyInterceptor @Inject constructor(
private val settings: AppSettings,
) : ImageProxyInterceptor {
private val delegate = settings.observeAsStateFlow(
scope = processLifecycleScope + Dispatchers.Default,
key = AppSettings.KEY_IMAGES_PROXY,
valueProducer = { createDelegate() },
)
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
return delegate.value?.intercept(chain) ?: chain.proceed(chain.request)
}
override suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
return delegate.value?.interceptPageRequest(request, okHttp) ?: okHttp.newCall(request).await()
}
private fun createDelegate(): ImageProxyInterceptor? = when (val proxy = settings.imagesProxy) {
-1 -> null
0 -> WsrvNlProxyInterceptor()
1 -> ZeroMsProxyInterceptor()
else -> error("Unsupported images proxy $proxy")
}
}

View File

@@ -0,0 +1,40 @@
package org.koitharu.kotatsu.core.network.imageproxy
import coil.request.ImageRequest
import coil.size.Dimension
import coil.size.isOriginal
import okhttp3.HttpUrl
import okhttp3.Request
class WsrvNlProxyInterceptor : BaseImageProxyInterceptor() {
override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest {
val newUrl = HttpUrl.Builder()
.scheme("https")
.host("wsrv.nl")
.addQueryParameter("url", url.toString())
.addQueryParameter("we", null)
val size = request.sizeResolver.size()
if (!size.isOriginal) {
newUrl.addQueryParameter("crop", "cover")
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
}
return request.newBuilder()
.data(newUrl.build())
.build()
}
override suspend fun onInterceptPageRequest(request: Request): Request {
val sourceUrl = request.url
val targetUrl = HttpUrl.Builder()
.scheme("https")
.host("wsrv.nl")
.addQueryParameter("url", sourceUrl.toString())
.addQueryParameter("we", null)
return request.newBuilder()
.url(targetUrl.build())
.build()
}
}

View File

@@ -0,0 +1,26 @@
package org.koitharu.kotatsu.core.network.imageproxy
import coil.request.ImageRequest
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
class ZeroMsProxyInterceptor : BaseImageProxyInterceptor() {
override suspend fun onInterceptImageRequest(request: ImageRequest, url: HttpUrl): ImageRequest {
if (url.host == "x.0ms.dev" || url.host == "0ms.dev") {
return request
}
val newUrl = ("https://x.0ms.dev/q70/$url").toHttpUrl()
return request.newBuilder()
.data(newUrl)
.build()
}
override suspend fun onInterceptPageRequest(request: Request): Request {
val newUrl = ("https://x.0ms.dev/q70/${request.url}").toHttpUrl()
return request.newBuilder()
.url(newUrl)
.build()
}
}

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.core.parser
import android.graphics.Canvas
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.bitmap.Rect
import java.io.OutputStream
import android.graphics.Bitmap as AndroidBitmap
import android.graphics.Rect as AndroidRect
class BitmapWrapper private constructor(
private val androidBitmap: AndroidBitmap,
) : Bitmap {
private val canvas by lazy { Canvas(androidBitmap) } // is not always used, so initialized lazily
override val height: Int
get() = androidBitmap.height
override val width: Int
get() = androidBitmap.width
override fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect) {
val androidSourceBitmap = (sourceBitmap as BitmapWrapper).androidBitmap
canvas.drawBitmap(androidSourceBitmap, src.toAndroidRect(), dst.toAndroidRect(), null)
}
fun compressTo(output: OutputStream) {
androidBitmap.compress(AndroidBitmap.CompressFormat.PNG, 100, output)
}
companion object {
fun create(width: Int, height: Int): Bitmap = BitmapWrapper(
AndroidBitmap.createBitmap(width, height, AndroidBitmap.Config.ARGB_8888),
)
fun create(bitmap: AndroidBitmap): Bitmap = BitmapWrapper(
if (bitmap.isMutable) bitmap else bitmap.copy(AndroidBitmap.Config.ARGB_8888, true),
)
private fun Rect.toAndroidRect() = AndroidRect(left, top, right, bottom)
}
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.BitmapFactory
import android.util.Base64
import android.webkit.WebView
import androidx.annotation.MainThread
@@ -10,15 +11,21 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody
import okio.Buffer
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.requireBody
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
@@ -68,6 +75,27 @@ class MangaLoaderContextImpl @Inject constructor(
return LocaleListCompat.getAdjustedDefault().toList()
}
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
val image = response.requireBody().byteStream()
val opts = BitmapFactory.Options()
opts.inMutable = true
val bitmap = BitmapFactory.decodeStream(image, null, opts) ?: error("Cannot decode bitmap")
val result = redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper
val body = Buffer().also {
result.compressTo(it.outputStream())
}.asResponseBody("image/jpeg".toMediaType())
return response.newBuilder()
.body(body)
.build()
}
override fun createBitmap(width: Int, height: Int): Bitmap {
return BitmapWrapper.create(width, height)
}
@MainThread
private fun obtainWebView(): WebView {
return webViewCached?.get() ?: WebView(androidContext).also {

View File

@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.requireBody
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
@@ -150,10 +151,6 @@ class FaviconFetcher(
return if (networkResponse != null) DataSource.NETWORK else DataSource.DISK
}
private fun Response.requireBody(): ResponseBody {
return checkNotNull(body) { "response body == null" }
}
private fun Size.toCacheKey() = buildString {
append(width.toString())
append('x')

View File

@@ -155,6 +155,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isTrackerNotificationsEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
val isTrackerNsfwDisabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NO_NSFW, false)
var notificationSound: Uri
get() = prefs.getString(KEY_NOTIFICATIONS_SOUND, null)?.toUriOrNull()
?: Settings.System.DEFAULT_NOTIFICATION_URI
@@ -380,14 +383,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
val isImagesProxyEnabled: Boolean
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
val imagesProxy: Int
get() {
val raw = prefs.getString(KEY_IMAGES_PROXY, null)?.toIntOrNull()
return raw ?: if (prefs.getBoolean(KEY_IMAGES_PROXY_OLD, false)) 0 else -1
}
val dnsOverHttps: DoHProvider
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
val isSSLBypassEnabled: Boolean
var isSSLBypassEnabled: Boolean
get() = prefs.getBoolean(KEY_SSL_BYPASS, false)
set(value) = prefs.edit { putBoolean(KEY_SSL_BYPASS, value) }
val proxyType: Proxy.Type
get() {
@@ -547,8 +554,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
companion object {
const val PAGE_SWITCH_VOLUME_KEYS = "volume"
const val TRACK_HISTORY = "history"
const val TRACK_FAVOURITES = "favourites"
@@ -585,6 +590,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_TRACK_CATEGORIES = "track_categories"
const val KEY_TRACK_WARNING = "track_warning"
const val KEY_TRACKER_NOTIFICATIONS = "tracker_notifications"
const val KEY_TRACKER_NO_NSFW = "tracker_no_nsfw"
const val KEY_NOTIFICATIONS_SETTINGS = "notifications_settings"
const val KEY_NOTIFICATIONS_SOUND = "notifications_sound"
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
@@ -597,7 +603,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
const val KEY_PROTECT_APP = "protect_app"
const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio"
const val KEY_APP_VERSION = "app_version"
const val KEY_ZOOM_MODE = "zoom_mode"
const val KEY_BACKUP = "backup"
const val KEY_RESTORE = "restore"
@@ -647,7 +652,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging"
const val KEY_LOGS_SHARE = "logs_share"
const val KEY_SOURCES_GRID = "sources_grid"
const val KEY_SOURCES_NEW = "sources_new"
const val KEY_UPDATES_UNSTABLE = "updates_unstable"
@@ -662,7 +666,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROXY_AUTH = "proxy_auth"
const val KEY_PROXY_LOGIN = "proxy_login"
const val KEY_PROXY_PASSWORD = "proxy_password"
const val KEY_IMAGES_PROXY = "images_proxy"
const val KEY_IMAGES_PROXY = "images_proxy_2"
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
const val KEY_DISABLE_NSFW = "no_nsfw"
const val KEY_RELATED_MANGA = "related_manga"
@@ -676,7 +680,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_CF_CONTRAST = "cf_contrast"
const val KEY_CF_INVERTED = "cf_inverted"
const val KEY_CF_GRAYSCALE = "cf_grayscale"
const val KEY_IGNORE_DOZE = "ignore_dose"
const val KEY_PAGES_TAB = "pages_tab"
const val KEY_DETAILS_TAB = "details_tab"
const val KEY_DETAILS_LAST_TAB = "details_last_tab"
@@ -684,9 +687,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PAGES_SAVE_DIR = "pages_dir"
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
const val KEY_STATS_ENABLED = "stats_on"
const val KEY_APP_UPDATE = "app_update"
const val KEY_APP_TRANSLATION = "about_app_translation"
const val KEY_FEED_HEADER = "feed_header"
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
// keys for non-persistent preferences
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"
// old keys are for migration only
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
}
}

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.prefs
import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.core.content.edit
import okhttp3.internal.isSensitiveHeader
import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.putEnumValue
@@ -12,6 +11,7 @@ import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.settings.utils.validation.DomainValidator
class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig {
@@ -31,7 +31,11 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
.ifNullOrEmpty { key.defaultValue }
.sanitizeHeaderValue()
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue)
?.trim()
?.takeIf { DomainValidator.isValidDomain(it) }
?: key.defaultValue
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
} as T

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.ui
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
@@ -92,10 +93,20 @@ abstract class BaseActivity<B : ViewBinding> :
}
override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// TODO fix behavior on Android 14
dispatchNavigateUp()
return true
}
val fm = supportFragmentManager
if (fm.isStateSaved) {
return false
}
dispatchNavigateUp()
if (fm.backStackEntryCount > 0) {
fm.popBackStack()
} else {
dispatchNavigateUp()
}
return true
}
@@ -159,6 +170,8 @@ abstract class BaseActivity<B : ViewBinding> :
}
}
protected fun hasViewBinding() = ::viewBinding.isInitialized
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BaseActivityEntryPoint {

View File

@@ -63,7 +63,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
)
}
protected fun setTitle(title: CharSequence?) {
protected open fun setTitle(title: CharSequence?) {
(activity as? SettingsActivity)?.setSectionTitle(title)
}

View File

@@ -1,22 +1,34 @@
package org.koitharu.kotatsu.core.ui.sheet
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import androidx.activity.BackEventCompat
import androidx.activity.OnBackPressedCallback
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
class BottomSheetCollapseCallback(
private val behavior: BottomSheetBehavior<*>,
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {
private val sheet: ViewGroup,
private val behavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(sheet),
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED || behavior.state == STATE_HALF_EXPANDED) {
init {
behavior.addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
@SuppressLint("SwitchIntDef")
override fun onStateChanged(view: View, state: Int) {
isEnabled = state == STATE_EXPANDED || state == STATE_HALF_EXPANDED
when (state) {
STATE_EXPANDED,
STATE_HALF_EXPANDED -> isEnabled = true
STATE_COLLAPSED,
STATE_HIDDEN -> isEnabled = false
}
}
override fun onSlide(p0: View, p1: Float) = Unit
@@ -24,7 +36,11 @@ class BottomSheetCollapseCallback(
)
}
override fun handleOnBackPressed() {
behavior.state = STATE_COLLAPSED
}
override fun handleOnBackPressed() = behavior.handleBackInvoked()
override fun handleOnBackCancelled() = behavior.cancelBackProgress()
override fun handleOnBackProgressed(backEvent: BackEventCompat) = behavior.updateBackProgress(backEvent)
override fun handleOnBackStarted(backEvent: BackEventCompat) = behavior.startBackProgress(backEvent)
}

View File

@@ -1,13 +1,12 @@
package org.koitharu.kotatsu.core.util.ext
import okhttp3.Cookie
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly
import okhttp3.internal.isSensitiveHeader
import okio.IOException
import org.json.JSONObject
import org.jsoup.HttpStatusException
@@ -42,6 +41,8 @@ fun Response.ensureSuccess() = apply {
}
}
fun Response.requireBody(): ResponseBody = checkNotNull(body) { "Response body is null" }
fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
c.name(name)
c.value(value)

View File

@@ -25,12 +25,12 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil.transform.CircleCropTransformation
import coil.util.CoilUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@@ -134,7 +134,7 @@ class DetailsActivity :
viewBinding.buttonRead.setOnClickListener(this)
viewBinding.buttonRead.setOnLongClickListener(this)
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
// viewBinding.buttonDownload?.setOnClickListener(this)
viewBinding.buttonDownload?.setOnClickListener(this)
viewBinding.infoLayout.chipBranch.setOnClickListener(this)
viewBinding.infoLayout.chipSize.setOnClickListener(this)
viewBinding.infoLayout.chipSource.setOnClickListener(this)
@@ -152,8 +152,8 @@ class DetailsActivity :
viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
viewBinding.chipsTags.onChipClickListener = this
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
viewBinding.layoutBottomSheet?.let { BottomSheetBehavior.from(it) }?.let { behavior ->
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(behavior))
viewBinding.containerBottomSheet?.let { sheet ->
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet))
}
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
@@ -201,7 +201,7 @@ class DetailsActivity :
when (v.id) {
R.id.button_read -> openReader(isIncognitoMode = false)
R.id.chip_branch -> showBranchPopupMenu(v)
// R.id.button_download -> DownloadDialogHelper(v, viewModel).show(menuProvider)
R.id.button_download -> DownloadDialogHelper(v, viewModel).show(menuProvider)
R.id.chip_author -> {
val manga = viewModel.manga.value ?: return
@@ -408,18 +408,18 @@ class DetailsActivity :
}
private fun onLoadingStateChanged(isLoading: Boolean) {
// val button = null ?: return
// if (isLoading) {
// button.setImageDrawable(
// CircularProgressDrawable(this).also {
// it.setStyle(CircularProgressDrawable.LARGE)
// it.setColorSchemeColors(getThemeColor(materialR.attr.colorControlNormal))
// it.start()
// },
// )
// } else {
// button.setImageResource(R.drawable.ic_download)
// }
val button = viewBinding.buttonDownload ?: return
if (isLoading) {
button.setImageDrawable(
CircularProgressDrawable(this).also {
it.setStyle(CircularProgressDrawable.LARGE)
it.setColorSchemeColors(getThemeColor(materialR.attr.colorControlNormal))
it.start()
},
)
} else {
button.setImageResource(R.drawable.ic_download)
}
}
private fun onScrobblingInfoChanged(scrobblings: List<ScrobblingInfo>) {
@@ -535,7 +535,7 @@ class DetailsActivity :
val isFirstCall = buttonRead.tag == null
buttonRead.tag = Unit
buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, !isFirstCall)
// buttonDownload?.isEnabled = info.isValid && info.canDownload
buttonDownload?.isEnabled = info.isValid && info.canDownload
buttonRead.isEnabled = info.isValid
}

View File

@@ -49,7 +49,6 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(), Actio
override fun onViewBindingCreated(binding: SheetChaptersPagesBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
disableFitToContents()
binding.headerBar.isVisible = dialog != null
val args = arguments ?: Bundle.EMPTY
var defaultTab = args.getInt(ARG_TAB, settings.defaultDetailsTab)

View File

@@ -19,8 +19,8 @@ import okhttp3.OkHttpClient
import okio.Path.Companion.toOkioPath
import okio.buffer
import okio.source
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.data.isFileUri

View File

@@ -27,15 +27,20 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit")
abstract suspend fun findLast(limit: Int): List<FavouriteManga>
fun observeAll(order: ListSortOrder): Flow<List<FavouriteManga>> {
fun observeAll(order: ListSortOrder, limit: Int): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order)
@Language("RoomSql")
val query = SimpleSQLiteQuery(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy",
)
return observeAllImpl(query)
val query = buildString {
append(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY ",
)
append(orderBy)
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
}
@Transaction
@@ -52,16 +57,21 @@ abstract class FavouritesDao {
)
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<FavouriteManga>> {
fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order)
val query = buildString {
append(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY ",
)
append(orderBy)
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
@Language("RoomSql")
val query = SimpleSQLiteQuery(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy",
arrayOf<Any>(categoryId),
)
return observeAllImpl(query)
return observeAllImpl(SimpleSQLiteQuery(query, arrayOf<Any>(categoryId)))
}
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {

View File

@@ -38,8 +38,8 @@ class FavouritesRepository @Inject constructor(
return entities.toMangaList()
}
fun observeAll(order: ListSortOrder): Flow<List<Manga>> {
return db.getFavouritesDao().observeAll(order)
fun observeAll(order: ListSortOrder, limit: Int): Flow<List<Manga>> {
return db.getFavouritesDao().observeAll(order, limit)
.mapItems { it.toManga() }
}
@@ -48,14 +48,14 @@ class FavouritesRepository @Inject constructor(
return entities.toMangaList()
}
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<Manga>> {
return db.getFavouritesDao().observeAll(categoryId, order)
fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow<List<Manga>> {
return db.getFavouritesDao().observeAll(categoryId, order, limit)
.mapItems { it.toManga() }
}
fun observeAll(categoryId: Long): Flow<List<Manga>> {
fun observeAll(categoryId: Long, limit: Int): Flow<List<Manga>> {
return observeOrder(categoryId)
.flatMapLatest { order -> observeAll(categoryId, order) }
.flatMapLatest { order -> observeAll(categoryId, order, limit) }
}
fun observeMangaCount(): Flow<Int> {
@@ -63,12 +63,6 @@ class FavouritesRepository @Inject constructor(
.distinctUntilChanged()
}
suspend fun getCategories(): List<FavouriteCategory> {
return db.getFavouriteCategoriesDao().findAll().map {
it.toFavouriteCategory()
}
}
fun observeCategories(): Flow<List<FavouriteCategory>> {
return db.getFavouriteCategoriesDao().observeAll().mapItems {
it.toFavouriteCategory()

View File

@@ -33,7 +33,7 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
binding.recyclerView.isVP2BugWorkaroundEnabled = true
}
override fun onScrolledToEnd() = Unit
override fun onScrolledToEnd() = viewModel.requestMoreItems()
override fun onFilterClick(view: View?) {
val menu = PopupMenu(view?.context ?: return, view)

View File

@@ -32,8 +32,11 @@ import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
private const val PAGE_SIZE = 20
@HiltViewModel
class FavouritesListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@@ -46,6 +49,8 @@ class FavouritesListViewModel @Inject constructor(
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
private val refreshTrigger = MutableStateFlow(Any())
private val limit = MutableStateFlow(PAGE_SIZE)
private val isReady = AtomicBoolean(false)
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_FAVORITES) { favoritesListMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.favoritesListMode)
@@ -61,13 +66,7 @@ class FavouritesListViewModel @Inject constructor(
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
override val content = combine(
if (categoryId == NO_ID) {
sortOrder.filterNotNull().flatMapLatest {
repository.observeAll(it)
}
} else {
repository.observeAll(categoryId)
},
observeFavorites(),
listMode,
refreshTrigger,
) { list, mode, _ ->
@@ -85,7 +84,10 @@ class FavouritesListViewModel @Inject constructor(
),
)
else -> list.toUi(mode, listExtraProvider)
else -> {
isReady.set(true)
list.toUi(mode, listExtraProvider)
}
}
}.catch {
emit(listOf(it.toErrorState(canRetry = false)))
@@ -126,4 +128,19 @@ class FavouritesListViewModel @Inject constructor(
repository.setCategoryOrder(categoryId, order)
}
}
fun requestMoreItems() {
if (isReady.compareAndSet(true, false)) {
limit.value += PAGE_SIZE
}
}
private fun observeFavorites() = if (categoryId == NO_ID) {
combine(sortOrder.filterNotNull(), limit, ::Pair)
.flatMapLatest { repository.observeAll(it.first, it.second) }
} else {
limit.flatMapLatest {
repository.observeAll(categoryId, it)
}
}
}

View File

@@ -9,7 +9,6 @@ import androidx.room.Transaction
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.list.domain.ListSortOrder
@@ -28,8 +27,7 @@ abstract class HistoryDao {
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
// TODO pagination
fun observeAll(order: ListSortOrder): Flow<List<HistoryWithManga>> {
fun observeAll(order: ListSortOrder, limit: Int): Flow<List<HistoryWithManga>> {
val orderBy = when (order) {
ListSortOrder.LAST_READ -> "history.updated_at DESC"
ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC"
@@ -43,13 +41,18 @@ abstract class HistoryDao {
ListSortOrder.UPDATED -> "IFNULL((SELECT last_chapter_date FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC"
else -> throw IllegalArgumentException("Sort order $order is not supported")
}
@Language("RoomSql")
val query = SimpleSQLiteQuery(
"SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " +
"WHERE history.deleted_at = 0 GROUP BY history.manga_id ORDER BY $orderBy",
)
return observeAllImpl(query)
val query = buildString {
append(
"SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " +
"WHERE history.deleted_at = 0 GROUP BY history.manga_id ORDER BY ",
)
append(orderBy)
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
}
@Query("SELECT manga_id FROM history WHERE deleted_at = 0")

View File

@@ -74,8 +74,8 @@ class HistoryRepository @Inject constructor(
}
}
fun observeAllWithHistory(order: ListSortOrder): Flow<List<MangaWithHistory>> {
return db.getHistoryDao().observeAll(order).mapItems {
fun observeAllWithHistory(order: ListSortOrder, limit: Int): Flow<List<MangaWithHistory>> {
return db.getHistoryDao().observeAll(order, limit).mapItems {
MangaWithHistory(
it.manga.toManga(it.tags.toMangaTags()),
it.history.toMangaHistory(),

View File

@@ -32,7 +32,7 @@ class HistoryListFragment : MangaListFragment() {
viewModel.isStatsEnabled.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
}
override fun onScrolledToEnd() = Unit
override fun onScrolledToEnd() = viewModel.requestMoreItems()
override fun onEmptyActionClick() {
startActivity(NetworkManageIntent())

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.history.ui
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
@@ -43,8 +44,11 @@ import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga
import java.time.Instant
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
private const val PAGE_SIZE = 20
@HiltViewModel
class HistoryListViewModel @Inject constructor(
private val repository: HistoryRepository,
@@ -62,8 +66,11 @@ class HistoryListViewModel @Inject constructor(
valueProducer = { historySortOrder },
)
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_HISTORY) { historyListMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.historyListMode)
override val listMode = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_LIST_MODE_HISTORY,
valueProducer = { historyListMode },
)
private val isGroupingEnabled = settings.observeAsFlow(
key = AppSettings.KEY_HISTORY_GROUPING,
@@ -72,6 +79,9 @@ class HistoryListViewModel @Inject constructor(
g && s.isGroupingSupported()
}
private val limit = MutableStateFlow(PAGE_SIZE)
private val isReady = AtomicBoolean(false)
val isStatsEnabled = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_STATS_ENABLED,
@@ -79,7 +89,7 @@ class HistoryListViewModel @Inject constructor(
)
override val content = combine(
sortOrder.flatMapLatest { repository.observeAllWithHistory(it) },
observeHistory(),
isGroupingEnabled,
listMode,
networkState,
@@ -95,7 +105,10 @@ class HistoryListViewModel @Inject constructor(
),
)
else -> mapList(list, grouped, mode, online, incognito)
else -> {
isReady.set(true)
mapList(list, grouped, mode, online, incognito)
}
}
}.onStart {
loadingCounter.increment()
@@ -138,6 +151,15 @@ class HistoryListViewModel @Inject constructor(
}
}
fun requestMoreItems() {
if (isReady.compareAndSet(true, false)) {
limit.value += PAGE_SIZE
}
}
private fun observeHistory() = combine(sortOrder, limit, ::Pair)
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second) }
private suspend fun mapList(
list: List<MangaWithHistory>,
grouped: Boolean,

View File

@@ -7,11 +7,12 @@ import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader
import coil.request.CachePolicy
import coil.request.ErrorResult
@@ -20,17 +21,26 @@ import coil.request.SuccessResult
import coil.target.ViewTarget
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.PopupMenuMediator
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getDisplayIcon
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ActivityImageBinding
import org.koitharu.kotatsu.databinding.ItemErrorStateBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listener, View.OnClickListener {
@@ -39,27 +49,45 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
lateinit var coil: ImageLoader
private var errorBinding: ItemErrorStateBinding? = null
private val viewModel: ImageViewModel by viewModels()
private lateinit var menuMediator: PopupMenuMediator
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityImageBinding.inflate(layoutInflater))
viewBinding.buttonBack.setOnClickListener(this)
loadImage(intent.data)
viewBinding.buttonMenu.setOnClickListener(this)
val imageUrl = requireNotNull(intent.data)
val menuProvider = ImageMenuProvider(
activity = this,
snackbarHost = viewBinding.root,
viewModel = viewModel,
)
menuMediator = PopupMenuMediator(menuProvider)
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.root, null))
viewModel.onImageSaved.observeEvent(this, ::onImageSaved)
loadImage(imageUrl)
}
override fun onWindowInsetsChanged(insets: Insets) {
with(viewBinding.buttonBack) {
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top + marginBottom
leftMargin = insets.left + marginBottom
rightMargin = insets.right + marginBottom
}
viewBinding.buttonBack.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top + bottomMargin
leftMargin = insets.left + bottomMargin
rightMargin = insets.right + bottomMargin
}
viewBinding.buttonMenu.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top + bottomMargin
leftMargin = insets.left + bottomMargin
rightMargin = insets.right + bottomMargin
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_back -> dispatchNavigateUp()
R.id.button_menu -> menuMediator.onLongClick(v)
else -> loadImage(intent.data)
}
}
@@ -92,11 +120,34 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
.memoryCachePolicy(CachePolicy.DISABLED)
.lifecycle(this)
.listener(this)
.tag(intent.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE))
.source(intent.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE))
.target(SsivTarget(viewBinding.ssiv))
.enqueueWith(coil)
}
private fun onImageSaved(uri: Uri) {
Snackbar.make(viewBinding.root, R.string.page_saved, Snackbar.LENGTH_LONG)
.setAction(R.string.share) {
ShareHelper(this).shareImage(uri)
}.show()
}
private fun onLoadingStateChanged(isLoading: Boolean) {
val button = viewBinding.buttonMenu
button.isClickable = !isLoading
if (isLoading) {
button.setImageDrawable(
CircularProgressDrawable(this).also {
it.setStyle(CircularProgressDrawable.LARGE)
it.setColorSchemeColors(getThemeColor(com.google.android.material.R.attr.colorControlNormal))
it.start()
},
)
} else {
button.setImageResource(materialR.drawable.abc_ic_menu_overflow_material)
}
}
private class SsivTarget(
override val view: SubsamplingScaleImageView,
) : ViewTarget<SubsamplingScaleImageView> {
@@ -124,7 +175,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>(), ImageRequest.Listene
companion object {
private const val EXTRA_SOURCE = "source"
const val EXTRA_SOURCE = "source"
fun newIntent(context: Context, url: String, source: MangaSource?): Intent {
return Intent(context, ImageActivity::class.java)

View File

@@ -0,0 +1,68 @@
package org.koitharu.kotatsu.image.ui
import android.Manifest
import android.os.Build
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.MenuProvider
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.local.data.isZipUri
class ImageMenuProvider(
private val activity: ComponentActivity,
private val snackbarHost: View,
private val viewModel: ImageViewModel,
) : MenuProvider {
private val permissionLauncher = activity.registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted ->
if (isGranted) {
saveImage()
}
}
private val saveLauncher = activity.registerForActivityResult(
ActivityResultContracts.CreateDocument("image/png"),
) { uri ->
if (uri != null) {
viewModel.saveImage(uri)
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_image, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_save -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
saveImage()
}
true
}
else -> false
}
private fun saveImage() {
val name = activity.intent.data?.let {
if (it.isZipUri()) {
it.fragment
} else {
it.lastPathSegment
}?.substringBeforeLast('.')?.plus(".png")
}
if (name == null || !saveLauncher.tryLaunch(name)) {
Snackbar.make(snackbarHost, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
}
}

View File

@@ -0,0 +1,50 @@
package org.koitharu.kotatsu.image.ui
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.SavedStateHandle
import coil.ImageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.ui.BaseActivity
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.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.source
import javax.inject.Inject
@HiltViewModel
class ImageViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val savedStateHandle: SavedStateHandle,
private val coil: ImageLoader,
) : BaseViewModel() {
val onImageSaved = MutableEventFlow<Uri>()
fun saveImage(destination: Uri) {
launchLoadingJob(Dispatchers.Default) {
val request = ImageRequest.Builder(context)
.memoryCachePolicy(CachePolicy.READ_ONLY)
.data(savedStateHandle.require<Uri>(BaseActivity.EXTRA_DATA))
.memoryCachePolicy(CachePolicy.DISABLED)
.source(savedStateHandle[ImageActivity.EXTRA_SOURCE])
.build()
val bitmap = coil.execute(request).getDrawableOrThrow().toBitmap()
runInterruptible(Dispatchers.IO) {
context.contentResolver.openOutputStream(destination)?.use { output ->
check(bitmap.compress(Bitmap.CompressFormat.PNG, 100, output))
} ?: error("Cannot open output stream")
}
onImageSaved.call(destination)
}
}
}

View File

@@ -7,8 +7,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.core.model.findChapter
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings

View File

@@ -27,8 +27,8 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okio.use
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings

View File

@@ -36,6 +36,17 @@ class ReaderControlDelegate(
}
fun onKeyDown(keyCode: Int): Boolean = when (keyCode) {
KeyEvent.KEYCODE_R -> {
listener.switchPageBy(1)
true
}
KeyEvent.KEYCODE_L -> {
listener.switchPageBy(-1)
true
}
KeyEvent.KEYCODE_VOLUME_UP -> if (settings.isReaderVolumeButtonsEnabled) {
listener.switchPageBy(-1)
true

View File

@@ -43,6 +43,12 @@ class RootSettingsFragment : BasePreferenceFragment(0) {
}
}
override fun setTitle(title: CharSequence?) {
if (!resources.getBoolean(R.bool.is_tablet)) {
super.setTitle(title)
}
}
private fun bindPreferenceSummary(key: String, @StringRes vararg items: Int) {
findPreference<Preference>(key)?.summary = items.joinToString { getString(it) }
}

View File

@@ -1,11 +1,8 @@
package org.koitharu.kotatsu.settings
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
@@ -44,9 +41,12 @@ class SettingsActivity :
private val isMasterDetails
get() = viewBinding.containerMaster != null
private var screenPadding = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivitySettingsBinding.inflate(layoutInflater))
screenPadding = resources.getDimensionPixelOffset(R.dimen.screen_padding)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
val currentFragment = fm.findFragmentById(R.id.container)
@@ -59,38 +59,7 @@ class SettingsActivity :
replace(R.id.container_master, RootSettingsFragment())
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_settings, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_leaks -> {
val intent = Intent()
intent.component = ComponentName(this, "leakcanary.internal.activity.LeakActivity")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(intent)
true
}
R.id.action_tracker -> {
val intent = Intent()
intent.component = ComponentName(this, "org.koitharu.kotatsu.tracker.ui.debug.TrackerDebugActivity")
startActivity(intent)
true
}
R.id.action_works -> {
val intent = Intent()
intent.component = ComponentName(this, "org.koitharu.workinspector.WorkInspectorActivity")
startActivity(intent)
true
}
else -> super.onOptionsItemSelected(item)
addMenuProvider(SettingsMenuProvider(this))
}
override fun onPreferenceStartFragment(
@@ -109,8 +78,8 @@ class SettingsActivity :
left = insets.left,
right = insets.right,
)
viewBinding.cardDetails?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = marginStart + insets.bottom
viewBinding.textViewHeader?.updateLayoutParams<MarginLayoutParams> {
topMargin = screenPadding + insets.top
}
}
@@ -125,7 +94,7 @@ class SettingsActivity :
supportFragmentManager.commit {
setReorderingAllowed(true)
replace(R.id.container, fragment)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
if (!isMasterDetails || (hasFragment && !isFromRoot)) {
addToBackStack(null)
}

View File

@@ -20,6 +20,7 @@ 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
@@ -40,6 +41,12 @@ 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?) {
@@ -67,6 +74,12 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
true
}
AppSettings.KEY_TRACKER_DEBUG -> {
startActivity(Intent(preference.context, TrackerDebugActivity::class.java))
true
}
else -> super.onPreferenceTreeClick(preference)
}
}

View File

@@ -11,7 +11,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
@@ -41,7 +40,13 @@ class MangaDirectorySelectDialog : AlertDialogFragment<DialogDirectorySelectBind
) {
if (it) {
viewModel.refresh()
pickFileTreeLauncher.launch(null)
if (!pickFileTreeLauncher.tryLaunch(null)) {
Toast.makeText(
context ?: return@registerForActivityResult,
R.string.operation_not_supported,
Toast.LENGTH_SHORT,
).show()
}
}
}

View File

@@ -11,20 +11,24 @@ class DomainValidator : EditTextValidator() {
if (trimmed.isEmpty()) {
return ValidationResult.Success
}
return if (!checkCharacters(trimmed)) {
return if (!isValidDomain(trimmed)) {
ValidationResult.Failed(context.getString(R.string.invalid_domain_message))
} else {
ValidationResult.Success
}
}
private fun checkCharacters(value: String): Boolean = runCatching {
val parts = value.split(':')
require(parts.size <= 2)
val urlBuilder = HttpUrl.Builder()
urlBuilder.host(parts.first())
if (parts.size == 2) {
urlBuilder.port(parts[1].toInt())
}
}.isSuccess
companion object {
fun isValidDomain(value: String): Boolean = runCatching {
require(value.isNotEmpty())
val parts = value.split(':')
require(parts.size <= 2)
val urlBuilder = HttpUrl.Builder()
urlBuilder.host(parts.first())
if (parts.size == 2) {
urlBuilder.port(parts[1].toInt())
}
}.isSuccess
}
}

View File

@@ -30,10 +30,6 @@ class Tracker @Inject constructor(
return repository.getTracks(offset = 0, limit = limit)
}
suspend fun gc() {
repository.gc()
}
suspend fun fetchUpdates(
track: MangaTracking,
commit: Boolean

View File

@@ -32,6 +32,7 @@ class TrackerDebugActivity : BaseActivity<ActivityTrackerDebugBinding>(), OnList
val tracksAdapter = BaseListAdapter<TrackDebugItem>()
.addDelegate(ListItemType.FEED, trackDebugAD(this, coil, this))
with(viewBinding.recyclerView) {
setHasFixedSize(true)
adapter = tracksAdapter
addItemDecoration(TypedListSpacingDecoration(context, false))
}

View File

@@ -49,6 +49,9 @@ class TrackerNotificationHelper @Inject constructor(
if (newChapters.isEmpty() || !applicationContext.checkNotificationPermission(CHANNEL_ID)) {
return null
}
if (manga.isNsfw && settings.isTrackerNsfwDisabled) {
return null
}
val id = manga.url.hashCode()
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
val summary = applicationContext.resources.getQuantityString(

View File

@@ -13,7 +13,7 @@
android:elevation="0dp"
android:fitsSystemWindows="true"
app:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toEndOf="@id/container_master"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:liftOnScroll="false">
@@ -30,50 +30,46 @@
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/card_details"
app:layout_constraintEnd_toStartOf="@id/container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_constraintWidth_max="400dp"
app:layout_constraintWidth_min="320dp"
app:layout_constraintWidth_percent="0.35" />
app:layout_constraintWidth_percent="0.4" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_details"
<TextView
android:id="@+id/textView_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:layout_marginTop="@dimen/screen_padding"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:gravity="center_vertical|start"
android:padding="8dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/container_master"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/appearance" />
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="@dimen/side_card_offset"
android:layout_marginTop="2dp"
android:layout_marginEnd="@dimen/side_card_offset"
android:layout_marginBottom="@dimen/side_card_offset"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/container_master"
app:layout_constraintTop_toBottomOf="@id/appbar">
app:layout_constraintTop_toBottomOf="@id/textView_header"
tools:layout="@layout/fragment_settings_sources" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:gravity="center_vertical|start"
android:padding="8dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
tools:text="@string/appearance" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:background="?colorSurfaceDim"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/container_master"
app:layout_constraintStart_toEndOf="@id/container_master"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -177,6 +177,44 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" />
<org.koitharu.kotatsu.core.ui.widgets.ProgressButton
android:id="@+id/button_read"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="@dimen/margin_normal"
android:layout_marginEnd="12dp"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:paddingHorizontal="6dp"
android:paddingVertical="8dp"
app:baseColor="?colorSecondaryContainer"
app:layout_constraintEnd_toStartOf="@id/button_download"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/info_layout"
app:progressColor="?colorPrimary"
app:subtitleTextAppearance="?textAppearanceBodySmall"
app:titleTextAppearance="?textAppearanceButton"
tools:max="100"
tools:progress="40"
tools:subtitle="12 chapters"
tools:title="@string/read" />
<ImageView
android:id="@+id/button_download"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_circle_button"
android:backgroundTint="?colorSecondaryContainer"
android:contentDescription="@string/download"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/button_read"
app:srcCompat="@drawable/ic_download" />
<TextView
android:id="@+id/textView_description_title"
android:layout_width="0dp"
@@ -191,7 +229,7 @@
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:layout_constraintEnd_toStartOf="@id/button_description_more"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/info_layout" />
app:layout_constraintTop_toBottomOf="@id/button_read" />
<Button
android:id="@+id/button_description_more"
@@ -358,78 +396,19 @@
</androidx.core.widget.NestedScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_bottom_sheet"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_bottom_sheet"
android:name="org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet"
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="6dp"
android:nestedScrollingEnabled="false"
android:outlineProvider="background"
app:behavior_fitToContents="false"
app:behavior_hideable="false"
app:behavior_peekHeight="@dimen/details_bs_peek_height"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle_sheet"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/button_download"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:background="@drawable/bg_circle_button"
android:backgroundTint="?colorSecondaryContainer"
android:contentDescription="@string/download"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/button_read"
app:srcCompat="@drawable/ic_download" />
<org.koitharu.kotatsu.core.ui.widgets.ProgressButton
android:id="@+id/button_read"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="@dimen/margin_normal"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:minWidth="240dp"
android:paddingHorizontal="6dp"
android:paddingVertical="8dp"
app:baseColor="?colorSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_download"
app:layout_constraintTop_toBottomOf="@id/drag_handle_sheet"
app:progressColor="?colorPrimary"
app:subtitleTextAppearance="?textAppearanceBodySmall"
app:titleTextAppearance="?textAppearanceButton"
tools:max="100"
tools:progress="40"
tools:subtitle="12 chapters"
tools:title="@string/read" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_bottom_sheet"
android:name="org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_below="@id/drag_handle_sheet"
android:elevation="6dp"
android:visibility="gone"
android:nestedScrollingEnabled="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/drag_handle_sheet"
tools:layout="@layout/sheet_chapters_pages" />
</androidx.constraintlayout.widget.ConstraintLayout>
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
tools:layout="@layout/sheet_chapters_pages" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -24,6 +24,18 @@
android:scaleType="center"
app:srcCompat="?homeAsUpIndicator" />
<ImageButton
android:id="@+id/button_menu"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_margin="@dimen/screen_padding"
android:background="@drawable/bg_circle_button"
android:contentDescription="@string/back"
android:elevation="@dimen/m3_sys_elevation_level1"
android:scaleType="center"
app:srcCompat="@drawable/abc_ic_menu_overflow_material" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progressBar"
android:layout_width="wrap_content"

View File

@@ -9,12 +9,13 @@
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_height="wrap_content"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -14,6 +14,7 @@
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_save"
android:title="@string/save" />
</menu>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu />

View File

@@ -632,4 +632,13 @@
<string name="new_chapters_pattern">%1$s: %2$d</string>
<string name="pin_navigation_ui">Замацаваць інтэрфейс навігацыі</string>
<string name="blocked_by_server_message">Вы заблакаваныя серверам. Паспрабуйце выкарыстоўваць іншае сеткавае падлучэнне (VPN, проксі і т. д.)</string>
<string name="ignore_ssl_errors_summary">Вы можаце адключыць праверку SSL-сертыфіката, калі пры доступе да сеткавых рэсурсаў узнікаюць праблемы, звязаныя з SSL. Гэта можа паўплываць на вашую бяспеку. Пасля змены гэтага параметра запатрабуецца перазагрузка праграмы.</string>
<string name="disable_nsfw_notifications_summary">Не паказваць апавяшчэння аб абнаўленнях мангі NSFW</string>
<string name="tracker_debug_info">Часопіс праверкі новых раздзелаў</string>
<string name="tracker_debug_info_summary">Адладкавая інфармацыя аб фонавай праверцы наяўнасці новых раздзелаў</string>
<string name="disable_connectivity_check">Адключыць праверку падключэння</string>
<string name="disable_connectivity_check_summary">Прапусціць праверкі падключэння ў выпадку праблем з падключэннем (напрыклад, пераход у аўтаномны рэжым, нават калі сетка падключана)</string>
<string name="disable_nsfw_notifications">Адключыць апавяшчэння NSFW</string>
<string name="disable">Адкл.</string>
<string name="sources_disabled">Крыніцы адключаны</string>
</resources>

View File

@@ -301,7 +301,7 @@
<string name="categories_delete_confirm">¿Estás seguro de que quieres eliminar las categorías favoritas seleccionadas\?
\nTodos los mangas en ella se perderán y esto no se puede deshacer.</string>
<string name="explore">Explorar</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="memory_usage_pattern">%1$s - %2$s</string>
<string name="exit_confirmation_summary">Pulse dos veces «Atrás» para salir de la aplicación</string>
<string name="exit_confirmation">Confirmación de salida</string>
<string name="pages_cache">Caché de páginas</string>
@@ -634,4 +634,11 @@
<string name="blocked_by_server_message">Estás bloqueado por el servidor. Intente utilizar una conexión de red diferente (VPN, Proxy, etc.)</string>
<string name="disable">Desactivar</string>
<string name="sources_disabled">Fuentes deshabilitadas</string>
<string name="disable_connectivity_check">Desactivar el control de conectividad</string>
<string name="ignore_ssl_errors_summary">Puede desactivar la verificación de certificados SSL en caso de que tenga problemas relacionados con SSL al acceder a recursos de red. Esto puede afectar a su seguridad. Es necesario reiniciar la aplicación después de cambiar esta configuración.</string>
<string name="disable_connectivity_check_summary">Omitir la comprobación de la conectividad en caso de que tenga problemas con ella (por ejemplo, si pasa al modo sin conexión aunque la red esté conectada)</string>
<string name="disable_nsfw_notifications">Deshabilitar notificaciones NSFW</string>
<string name="disable_nsfw_notifications_summary">No mostrar notificaciones sobre actualizaciones de manga NSFW</string>
<string name="tracker_debug_info">Comprobando el registro de nuevos capítulos</string>
<string name="tracker_debug_info_summary">Información de depuración sobre verificaciones de antecedentes para nuevos capítulos</string>
</resources>

View File

@@ -634,4 +634,7 @@
<string name="blocked_by_server_message">Hinarangan ka ng server. Subukang gumamit ng ibang koneksyon sa network (VPN, Proxy, atbp.)</string>
<string name="disable">Di paganahin</string>
<string name="sources_disabled">Di pinagana ang mga source</string>
<string name="disable_connectivity_check">Di paganahin ang pagtingin sa koneksyon</string>
<string name="disable_connectivity_check_summary">Laktawan ang pagsuri sa koneksyon kung sakaling mayroon kang isyu rito (hal. pagpunta sa offline mode kahit na nakakonekta sa network)</string>
<string name="ignore_ssl_errors_summary">Maaaring di paganahin ang pag-verify ng mga SSL certificate kung sakaling makaharap ka ng mga isyu na nauugnay sa SSL kapag nag-a-access ng mga network resource. Ito ay makaapekto sa iyong seguridad. Kinakailangang mag-restart ang aplikasyon pagkatapos baguhin ang setting na ito.</string>
</resources>

View File

@@ -634,4 +634,11 @@
<string name="blocked_by_server_message">आपको सर्वर द्वारा अवरुद्ध कर दिया गया है। किसी भिन्न नेटवर्क कनेक्शन (VPN, प्रॉक्सी, आदि) का उपयोग करने का प्रयास करें</string>
<string name="disable">अक्षम करें</string>
<string name="sources_disabled">स्रोत अक्षम</string>
<string name="disable_connectivity_check">कनेक्टिविटी जांच अक्षम करें</string>
<string name="ignore_ssl_errors_summary">यदि नेटवर्क संसाधनों तक पहुँचने के दौरान आपको SSL से संबंधित समस्याओं का सामना करना पड़ता है तो आप SSL प्रमाणपत्र सत्यापन को अक्षम कर सकते हैं। इससे आपकी सुरक्षा प्रभावित हो सकती है। इस सेटिंग को बदलने के बाद एप्लिकेशन को पुनरारंभ करना आवश्यक है।</string>
<string name="disable_connectivity_check_summary">यदि आपको कनेक्टिविटी से जुड़ी कोई समस्या है तो कनेक्टिविटी जांच को छोड़ दें (उदाहरण के लिए नेटवर्क कनेक्ट होने के बावजूद ऑफ़लाइन मोड में जाना)</string>
<string name="disable_nsfw_notifications">NSFW सूचनाएं अक्षम करें</string>
<string name="disable_nsfw_notifications_summary">NSFW मंगा अपडेट के बारे में सूचनाएं न दिखाएं</string>
<string name="tracker_debug_info">नए अध्याय लॉग की जांच की जा रही है</string>
<string name="tracker_debug_info_summary">नए अध्यायों के लिए पृष्ठभूमि जांच के बारे में जानकारी डीबग करें</string>
</resources>

View File

@@ -307,7 +307,7 @@
<string name="other_cache">Altra cache</string>
<string name="storage_usage">Utilizzo dello spazio di archiviazione</string>
<string name="available">Disponibile</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="memory_usage_pattern">%1$s - %2$s</string>
<string name="removed_from_favourites">Rimosso dai preferiti</string>
<string name="options">Opzioni</string>
<string name="no_chapters">Nessun capitolo</string>
@@ -318,7 +318,7 @@
<string name="importing_manga">Importazione di manga</string>
<string name="import_will_start_soon">L\'importazione inizierà presto</string>
<string name="feed">Flusso</string>
<string name="reader_control_ltr_summary">Toccando il bordo destro o premendo il tasto destro si passa sempre alla pagina successiva</string>
<string name="reader_control_ltr_summary">Toccando il bordo destro o premendo il tasto destro si passa sempre alla pagina successiva.</string>
<string name="contrast">Contrasto</string>
<string name="reset">Ripristina</string>
<string name="reader_slider">Mostra il cursore di cambio pagina</string>
@@ -538,4 +538,59 @@
<string name="volume_">Volume %d</string>
<string name="category_hidden_done">Questa categoria è stata nascosta dalla schermata principale ed è accessibile tramite Menù → Gestisci categorie</string>
<string name="remaining_time_pattern">%1$s %2$s</string>
<string name="last_used">Ultimo utilizzo</string>
<string name="incognito_mode_hint">I tuoi progressi di lettura non saranno salvati</string>
<string name="vertical">Verticale</string>
<string name="last_read">Ultima lettura</string>
<string name="reading_time_estimation_summary">Il tempo di lettura stimato potrebbe essere inaccurato</string>
<string name="reading_time_estimation">Mostra il tempo di lettura stimato</string>
<string name="email_password_enter_hint">Inserisci la tua email e password per continuare</string>
<string name="show_menu">Mostra menu</string>
<string name="switch_pages_volume_buttons_summary">Usa i pulsanti del volume per cambiare pagine</string>
<string name="tap_action">Azioni tap</string>
<string name="long_tap_action">Azioni tap prolungato</string>
<string name="none">Nessuno</string>
<string name="fullscreen_mode">Modalità schermo interno</string>
<string name="chapters_grid_view">Vista griglia</string>
<string name="prev_chapter">Capitolo precedente</string>
<string name="reader_actions_summary">Configura le azioni per le aree di schermo cliccabili</string>
<string name="suggestions_unavailable_text">La funzione di suggerimento è disabilitata</string>
<string name="show_labels_in_navbar">Mostra le etichette nella barra di navigazione</string>
<string name="delete_read_chapters_summary">Rimuovi i capitoli già letti dalla memoria locale per liberare spazio</string>
<string name="delete_read_chapters_prompt">Questo eliminerà in modo permanente tutti i capitoli segnati come già letti dalla memoria locale. Puoi scaricarli di nuovo, ma i capitoli importati potrebbero essere persi per sempre</string>
<string name="order_oldest">Meno recenti</string>
<string name="long_ago_read">Letto molto tempo fa</string>
<string name="unsupported_source">Questa fonte manga non è supportata</string>
<string name="hours_short">%d ore</string>
<string name="minutes_short">%d m</string>
<string name="show_updated">Mostra aggiornato</string>
<string name="toggle_ui">Mostra/nascondi UI</string>
<string name="next_chapter">Prossimo capitolo</string>
<string name="prev_page">Pagina precedente</string>
<string name="next_page">Prossima pagina</string>
<string name="reader_actions">Azioni del lettore</string>
<string name="switch_pages_volume_buttons">Abilita pulsanti del volume</string>
<string name="reader_fullscreen_summary">Nascondi le barre di stato e di notifica</string>
<string name="check_for_new_chapters_disabled">Il controllo di nuovi capitoli è disabilitato</string>
<string name="delete_read_chapters_auto">Elimina i capitoli già letti automaticamente</string>
<string name="runs_on_app_start">Esegui quando l\'applicazione viene avviata</string>
<string name="split_by_translations">Dividi per traduzioni</string>
<string name="split_by_translations_summary">Mostra separatamente i capitoli con diverse traduzioni, invece che in un\'unica lista</string>
<string name="unread">Non letto</string>
<string name="show_pages_thumbs">Mostra le anteprime delle pagine</string>
<string name="show_pages_thumbs_summary">Abilita la tab \"Pagine\" nella schermata di dettaglio</string>
<string name="unsupported_backup_message">Si prega di selezione un file backup di Kotatsu corretto</string>
<string name="hours_minutes_short">%1$d h %2$d m</string>
<string name="fix">Aggiustamenti</string>
<string name="missing_storage_permission">Non sono presenti i permessi per accedere al manga nella memoria esterna</string>
<string name="webtoon_gaps">Gap in modalità webtoon</string>
<string name="webtoon_gaps_summary">Mostra gap verticali tra le pagine in modalità webotoon</string>
<string name="config_reset_confirm">Ripristinare le impostazioni ai valori predefiniti? Questo procedimento è irreversibile.</string>
<string name="use_two_pages_landscape">Usa il layout a due pagine con l\'orientamento orizzontale (beta)</string>
<string name="error_no_data_received">Non è stato ricevuto nessun dato dal server</string>
<string name="enable_source">Attiva fonte</string>
<string name="less_frequently">Meno frequente</string>
<string name="more_frequently">Più frequente</string>
<string name="frequency_of_check">Frequenza di controllo</string>
<string name="new_chapters_pattern">%1$s: %2$d</string>
</resources>

View File

@@ -236,7 +236,7 @@
<string name="invalid_domain_message">Nieważna domena</string>
<string name="reorder">Zmień kolejność</string>
<string name="exit_confirmation">Potwierdzenie wyjścia</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="memory_usage_pattern">%1$s - %2$s</string>
<string name="reader_info_pattern">Rozdz. %1$d/%2$d Str. %3$d/%4$d</string>
<string name="network_unavailable_hint">Włącz Wi-Fi lub sieć komórkową, aby czytać mangę online</string>
<string name="_import">Importuj</string>
@@ -632,4 +632,13 @@
<string name="pin_navigation_ui_summary">Nie ukrywaj paska nawigacji i paska wyszukiwania podczas przewijania</string>
<string name="search_suggestions">Sugestie wyszukiwania</string>
<string name="blocked_by_server_message">Jesteś zablokowany przez serwer. Spróbuj użyć innego połączenia sieciowego (VPN, proxy itp.)</string>
<string name="sources_disabled">Źródła wyłączone</string>
<string name="disable">Wyłączone</string>
<string name="disable_connectivity_check">Wyłącz sprawdzanie łączności</string>
<string name="ignore_ssl_errors_summary">Możesz wyłączyć weryfikację certyfikatów SSL w przypadku wystąpienia problemów związanych z SSL podczas uzyskiwania dostępu do zasobów sieciowych. Może to mieć wpływ na Twoje bezpieczeństwo. Po zmianie tego ustawienia wymagane jest ponowne uruchomienie aplikacji.</string>
<string name="disable_connectivity_check_summary">Pomiń sprawdzanie łączności w przypadku problemów z nią (np. przejście do trybu offline, mimo że sieć jest podłączona)</string>
<string name="disable_nsfw_notifications">Wyłącz powiadomienia NSFW</string>
<string name="disable_nsfw_notifications_summary">Nie pokazuj powiadomień o aktualizacjach mangi NSFW</string>
<string name="tracker_debug_info_summary">Debuguj informacje o sprawdzaniu dostępności nowych rozdziałów</string>
<string name="tracker_debug_info">Dziennik sprawdzania nowych rozdziałów</string>
</resources>

View File

@@ -308,7 +308,7 @@
<string name="reader_info_pattern">Cap. %1$d/%2$d Pág. %3$d/%4$d</string>
<string name="reader_info_bar">Mostrar barra de informações no leitor</string>
<string name="folder_with_images">Pasta com imagens</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="memory_usage_pattern">%1$s - %2$s</string>
<string name="comics_archive">Arquivo de quadrinhos</string>
<string name="importing_manga">Importando mangá(s)</string>
<string name="import_completed">Importação completa</string>
@@ -619,4 +619,22 @@
<string name="delete_read_chapters_prompt">Isso irá apagar permanentemente todos os capítulos marcados como lidos do armazenamento local. Você pode baixá-los novamente mais tarde, mas os capítulos importados podem ser perdidos para sempre</string>
<string name="last_used">Usado pela última vez</string>
<string name="show_updated">Mostrar atualização</string>
<string name="disable_connectivity_check">Desativar a verificação de conectividade</string>
<string name="disable_connectivity_check_summary">Ignore a verificação de conectividade caso tenha problemas com ela (por exemplo, entrar no modo off-line mesmo que a rede esteja conectada)</string>
<string name="webtoon_gaps">Lacunas no modo webtoon</string>
<string name="webtoon_gaps_summary">Mostrar espaços verticais entre as páginas no modo webtoon</string>
<string name="authors">Autores</string>
<string name="ignore_ssl_errors_summary">Você pode desativar a verificação de certificados SSL caso tenha problemas relacionados a SSL ao acessar recursos de rede. Isso pode afetar sua segurança. É necessário reiniciar o aplicativo após alterar essa configuração.</string>
<string name="search_suggestions">Sugestões de Pesquisa</string>
<string name="recent_queries">Consultas recentes</string>
<string name="suggested_queries">Consultas sugeridas</string>
<string name="disable">Desativar</string>
<string name="sources_disabled">Fontes desativadas</string>
<string name="blocked_by_server_message">Você está bloqueado pelo servidor. Tente usar uma conexão de internet diferente (VPN, Proxy, etc.)</string>
<string name="less_frequently">Com menos frequência</string>
<string name="more_frequently">Com mais frequência</string>
<string name="frequency_of_check">Frequência de verificação</string>
<string name="new_chapters_pattern">%1$s: %2$d</string>
<string name="pin_navigation_ui">Fixar interface de navegação</string>
<string name="pin_navigation_ui_summary">Não esconder barra de navegação e visualização de pesquisa ao rolar</string>
</resources>

View File

@@ -628,7 +628,6 @@
<string name="less_frequently">Menos frequência</string>
<string name="more_frequently">Mais frequência</string>
<string name="frequency_of_check">Frequência de verificação</string>
<string name="new_chapters_pattern">novo padrão de capítulos</string>
<string name="pin_navigation_ui">Fixar interface de navegação</string>
<string name="pin_navigation_ui_summary">Ao rolar, não esconda a barra de navegação e a visualização de busca</string>
</resources>
</resources>

View File

@@ -632,4 +632,13 @@
<string name="pin_navigation_ui_summary">Не прятать навигационную панель и строку поиска при прокрутке</string>
<string name="new_chapters_pattern">%1$s: %2$d</string>
<string name="blocked_by_server_message">Вы заблокированы сервером. Попробуйте использовать другое сетевое подключение (VPN, прокси и т. д.)</string>
<string name="disable_connectivity_check">Отключить проверку подключения</string>
<string name="ignore_ssl_errors_summary">Вы можете отключить проверку сертификата SSL, если при доступе к сетевым ресурсам возникают проблемы, связанные с SSL. Это может повлиять на вашу безопасность. После изменения этого параметра потребуется перезагрузка приложения.</string>
<string name="tracker_debug_info_summary">Отладочная информация о фоновой проверке наличия новых глав</string>
<string name="tracker_debug_info">Журнал проверки новых глав</string>
<string name="disable_connectivity_check_summary">Пропустить проверки подключения в случае проблем с подключением (например, переход в автономный режим, даже если сеть подключена)</string>
<string name="disable_nsfw_notifications">Отключить уведомления NSFW</string>
<string name="disable_nsfw_notifications_summary">Не показывать уведомления об обновлениях манги NSFW</string>
<string name="disable">Откл.</string>
<string name="sources_disabled">Источники отключены</string>
</resources>

View File

@@ -634,4 +634,7 @@
<string name="blocked_by_server_message">Блокирао вас је послуживач. Покушај да користиш другу мрежну везу (ВПН, прокси, итд.)</string>
<string name="disable">Онемогући</string>
<string name="sources_disabled">Извори су онемогућени</string>
<string name="disable_connectivity_check">Онемогућите проверу везе</string>
<string name="disable_nsfw_notifications">Онемогућите НСФВ обавештења</string>
<string name="disable_nsfw_notifications_summary">Не приказуј обавештења за ажурирања НСФВ манге</string>
</resources>

View File

@@ -634,4 +634,11 @@
<string name="blocked_by_server_message">Sunucu tarafından engellendiniz. Farklı bir ağ bağlantısı kullanmayı deneyin (VPN, vekil sunucu, vb.)</string>
<string name="disable">Devre dışı bırak</string>
<string name="sources_disabled">Kaynaklar devre dışı bırakıldı</string>
<string name="disable_connectivity_check">Bağlantı denetimini devre dışı bırak</string>
<string name="ignore_ssl_errors_summary">Ağ kaynaklarına erişirken SSL ile ilgili bir sorunla karşılaşmanız durumunda SSL sertifikaları doğrulamasını devre dışı bırakabilirsiniz. Bu durum güvenliğinizi etkileyebilir. Bu ayarı değiştirdikten sonra uygulamanın yeniden başlatılması gerekir.</string>
<string name="disable_connectivity_check_summary">Sorun yaşamanız durumunda bağlantı denetimini atlayın (örneğin, ağ bağlı olmasına rağmen çevrim dışı moda geçiş)</string>
<string name="disable_nsfw_notifications">Uygunsuz bildirimleri devre dışı bırak</string>
<string name="disable_nsfw_notifications_summary">Uygunsuz manga güncellemeleri hakkında bildirim gösterilmesin</string>
<string name="tracker_debug_info">Yeni bölümler günlüğü denetleniyor</string>
<string name="tracker_debug_info_summary">Yeni bölümler için arka plan denetimleri hakkında hata ayıklama bilgileri</string>
</resources>

View File

@@ -632,4 +632,13 @@
<string name="pin_navigation_ui">Закріпити інтерфейс навігації</string>
<string name="pin_navigation_ui_summary">Не ховати навігаційну панель та рядок пошуку під час прокручування</string>
<string name="blocked_by_server_message">Ви заблоковані сервером. Спробуйте використовувати інше підключення до мережі (VPN, проксі тощо)</string>
<string name="disable_connectivity_check">Вимкнути перевірку підключення</string>
<string name="ignore_ssl_errors_summary">Перевірку сертифіката SSL можна вимкнути, якщо під час доступу до мережних ресурсів виникають проблеми, пов\'язані з SSL. Це може вплинути на вашу безпеку. Після зміни цього параметра буде потрібно перезавантажити застосунок.</string>
<string name="disable_connectivity_check_summary">Пропустити перевірки підключення у разі проблем із підключенням (наприклад, перехід в автономний режим, навіть якщо мережа підключена)</string>
<string name="disable_nsfw_notifications">Вимкнути повідомлення NSFW</string>
<string name="tracker_debug_info">Журнал перевірки нових розділів</string>
<string name="tracker_debug_info_summary">Відлагоджувальна інформація про фонову перевірку наявності нових розділів</string>
<string name="disable_nsfw_notifications_summary">Не відображати повідомлення про оновлення манґи NSFW</string>
<string name="disable">Вимкнути</string>
<string name="sources_disabled">Джерела вимкнено</string>
</resources>

View File

@@ -634,4 +634,11 @@
<string name="blocked_by_server_message">你的访问已被服务器拦截请尝试使用不同的网络连接访问如VPN代理等</string>
<string name="disable">关闭</string>
<string name="sources_disabled">图源已关闭</string>
<string name="disable_connectivity_check">关闭连接连通性检查</string>
<string name="disable_connectivity_check_summary">若连通性检查存在问题可打开此选项(例:即使连接了网络但依旧提示网络断开)</string>
<string name="ignore_ssl_errors_summary">若在连接到在线图源时SSL证书出现问题可关闭SSL证书认证关闭后对安全性有所影响需要重启应用来更改设置。</string>
<string name="disable_nsfw_notifications">关闭成人内容提醒</string>
<string name="disable_nsfw_notifications_summary">不显示成人漫画的更新提醒</string>
<string name="tracker_debug_info">漫画更新日志</string>
<string name="tracker_debug_info_summary">记录后台漫画更新时的调试信息</string>
</resources>

View File

@@ -35,6 +35,12 @@
<item>Google</item>
<item>CloudFlare</item>
<item>AdGuard</item>
<item>0ms</item>
</string-array>
<string-array name="image_proxies" translatable="false">
<item>@string/none</item>
<item>wsrv.nl</item>
<item>0ms.dev</item>
</string-array>
<string-array name="reader_modes" translatable="false">
<item>@string/standard</item>

View File

@@ -40,6 +40,11 @@
<item>2</item>
<item>0</item>
</string-array>
<string-array name="values_image_proxies" translatable="false">
<item>-1</item>
<item>0</item>
<item>1</item>
</string-array>
<string-array name="sync_host_list" translatable="false">
<item>@string/sync_host_default</item>
<item>moe.shirizu.org</item>

View File

@@ -35,7 +35,7 @@
<dimen name="chapter_grid_width">80dp</dimen>
<dimen name="side_card_offset">8dp</dimen>
<dimen name="webtoon_pages_gap">24dp</dimen>
<dimen name="details_bs_peek_height">142dp</dimen>
<dimen name="details_bs_peek_height">92dp</dimen>
<dimen name="search_suggestions_manga_height">142dp</dimen>
<dimen name="search_suggestions_manga_spacing">6dp</dimen>

View File

@@ -214,7 +214,7 @@
<string name="various_languages">Various languages</string>
<string name="search_chapters">Find chapter</string>
<string name="chapters_empty">No chapters in this manga</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="percent_string_pattern" translatable="false">%1$s%%</string>
<string name="appearance">Appearance</string>
<string name="suggestions_updating">Suggestions updating</string>
<string name="suggestions_excluded_genres">Exclude genres</string>
@@ -301,7 +301,7 @@
<string name="other_cache">Other cache</string>
<string name="storage_usage">Storage usage</string>
<string name="available">Available</string>
<string name="memory_usage_pattern">%1$s - %2$s</string>
<string name="memory_usage_pattern" translatable="false">%1$s - %2$s</string>
<string name="removed_from_favourites">Removed from favourites</string>
<string name="options">Options</string>
<string name="not_found_404">Content not found or removed</string>
@@ -498,7 +498,7 @@
<string name="content_type_hentai">Hentai</string>
<string name="content_type_comics">Comics</string>
<string name="content_type_other">Other</string>
<string name="source_summary_pattern">%1$s, %2$s</string>
<string name="source_summary_pattern" translatable="false">%1$s, %2$s</string>
<string name="sources_catalog">Sources catalog</string>
<string name="source_enabled">Source enabled</string>
<string name="no_manga_sources_catalog_text">There are no sources available in this section, or all of it might have been already added.\nStay tuned</string>
@@ -541,7 +541,7 @@
<string name="mark_as_completed">Mark as completed</string>
<string name="mark_as_completed_prompt">Mark selected manga as completely read?\n\nWarning: current reading progress will be lost.</string>
<string name="category_hidden_done">This category was hidden from the main screen and is accessible through Menu → Manage categories</string>
<string name="remaining_time_pattern">%1$s %2$s</string>
<string name="remaining_time_pattern" translatable="false">%1$s %2$s</string>
<string name="volume_">Volume %d</string>
<string name="volume_unknown">Unknown volume</string>
<string name="incognito_mode_hint">Your reading progress will not be saved</string>
@@ -633,7 +633,7 @@
<string name="less_frequently">Less frequently</string>
<string name="more_frequently">More frequently</string>
<string name="frequency_of_check">Frequency of check</string>
<string name="new_chapters_pattern">%1$s: %2$d</string>
<string name="new_chapters_pattern" translatable="false">%1$s: %2$d</string>
<string name="pin_navigation_ui">Pin navigation UI</string>
<string name="pin_navigation_ui_summary">Do not hide navigation bar and search view on scroll</string>
<string name="search_suggestions">Search suggestions</string>
@@ -644,4 +644,10 @@
<string name="disable">Disable</string>
<string name="sources_disabled">Sources disabled</string>
<string name="disable_connectivity_check">Disable connectivity check</string>
<string name="ignore_ssl_errors_summary">You can disable SSL certificates verification in case you face an SSL-related issues when accessing network resources. This may affect your security. Application restarting is required after changing this setting.</string>
<string name="disable_connectivity_check_summary">Skip the connectivity check in case you have issues with it (e.g. going offline mode even though the network is connected)</string>
<string name="disable_nsfw_notifications">Disable NSFW notifications</string>
<string name="disable_nsfw_notifications_summary">Do not show notifications about NSFW manga updates</string>
<string name="tracker_debug_info">Checking for new chapters log</string>
<string name="tracker_debug_info_summary">Debug information about background checks for new chapters</string>
</resources>

View File

@@ -28,6 +28,12 @@
android:key="logs_share"
android:title="@string/share_logs" />
<Preference
android:key="tracker_debug"
android:persistent="false"
android:summary="@string/tracker_debug_info_summary"
android:title="@string/tracker_debug_info" />
<Preference
android:key="about_app_translation"
android:summary="@string/about_app_translation_summary"

View File

@@ -33,11 +33,13 @@
android:title="@string/dns_over_https"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="images_proxy"
android:summary="@string/images_procy_description"
android:title="@string/images_proxy_title" />
<ListPreference
android:defaultValue="-1"
android:entries="@array/image_proxies"
android:entryValues="@array/values_image_proxies"
android:key="images_proxy_2"
android:title="@string/images_proxy_title"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
@@ -47,10 +49,12 @@
<SwitchPreferenceCompat
android:key="ssl_bypass"
android:summary="@string/ignore_ssl_errors_summary"
android:title="@string/ignore_ssl_errors" />
<SwitchPreferenceCompat
android:key="no_offline"
android:summary="@string/disable_connectivity_check_summary"
android:title="@string/disable_connectivity_check" />
</PreferenceScreen>

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<SwitchPreferenceCompat
android:defaultValue="true"
@@ -44,6 +45,13 @@
android:key="notifications_settings"
android:title="@string/notifications_settings" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:dependency="tracker_enabled"
android:key="tracker_no_nsfw"
android:summary="@string/disable_nsfw_notifications_summary"
android:title="@string/disable_nsfw_notifications" />
<Preference
android:dependency="tracker_enabled"
android:key="ignore_dose"
@@ -51,7 +59,8 @@
android:summary="@string/disable_battery_optimization_summary"
android:title="@string/disable_battery_optimization"
app:allowDividerAbove="true"
app:isPreferenceVisible="false" />
app:isPreferenceVisible="false"
tools:isPrefrenceVisible="true" />
<org.koitharu.kotatsu.settings.utils.LinksPreference
android:icon="@drawable/ic_info_outline"

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<remote-actions version="1.2">
<action
id="next_filter"
label="@string/prev_page"
priority="1"
trigger_key="L">
<preference name="gesture" value="swipe_left"/>
</action>
<action
id="prev_filter"
label="@string/next_page"
priority="2"
trigger_key="R">
<preference name="gesture" value="swipe_right"/>
</action>
</remote-actions>

View File

@@ -0,0 +1,15 @@
package org.koitharu.kotatsu.settings
import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
@Suppress("UNUSED_PARAMETER")
class SettingsMenuProvider(context: Context) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) = Unit
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false
}

View File

@@ -4,7 +4,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.4.0'
classpath 'com.android.tools.build:gradle:8.4.1'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.24-1.0.20'