Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597abdb20c | ||
|
|
809e7d8701 | ||
|
|
0015c5704a | ||
|
|
a7ff1610eb | ||
|
|
22c402fc5e | ||
|
|
6e92d46a63 | ||
|
|
66ed926ea8 | ||
|
|
b7741ce2af | ||
|
|
1a17324d26 | ||
|
|
4044936481 | ||
|
|
1efe86421a | ||
|
|
34dd080f6c | ||
|
|
f4838afab0 | ||
|
|
b207eebe56 | ||
|
|
4f454ab438 | ||
|
|
1ecf416113 | ||
|
|
94670a03ff | ||
|
|
e92f165677 | ||
|
|
4a03137a25 | ||
|
|
7e6e1fb6de | ||
|
|
f477797823 | ||
|
|
125b6740a6 | ||
|
|
1618a11955 | ||
|
|
966d6e2383 | ||
|
|
2f33a135fc | ||
|
|
207ea492d5 | ||
|
|
250d5432a0 | ||
|
|
9768758ecc | ||
|
|
20852dbd12 |
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 657
|
||||
versionName = '7.4'
|
||||
versionCode = 660
|
||||
versionName = '7.4.3'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -82,20 +82,20 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:a9fc534ea7') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:98cbee11b9') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.0.10-RC'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0-RC'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.0.10'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0-RC.2'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.9.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.2'
|
||||
implementation 'androidx.transition:transition-ktx:1.5.1'
|
||||
implementation 'androidx.collection:collection-ktx:1.4.2'
|
||||
implementation 'androidx.collection:collection-ktx:1.4.3'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.8.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.4'
|
||||
@@ -109,7 +109,7 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.4'
|
||||
implementation 'androidx.webkit:webkit:1.11.0'
|
||||
|
||||
implementation 'androidx.work:work-runtime:2.9.0'
|
||||
implementation 'androidx.work:work-runtime:2.9.1'
|
||||
//noinspection GradleDependency
|
||||
implementation('com.google.guava:guava:32.0.1-android') {
|
||||
exclude group: 'com.google.guava', module: 'failureaccess'
|
||||
|
||||
@@ -45,7 +45,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
}
|
||||
val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
|
||||
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
|
||||
repository?.headers?.get(CommonHeaders.USER_AGENT)
|
||||
repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
|
||||
viewBinding.webView.configureForParser(userAgent)
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.exceptions
|
||||
|
||||
class IncompatiblePluginException(
|
||||
val name: String?,
|
||||
cause: Throwable?,
|
||||
) : RuntimeException(cause)
|
||||
@@ -38,7 +38,7 @@ class CommonHeadersInterceptor @Inject constructor(
|
||||
null
|
||||
}
|
||||
val headersBuilder = request.headers.newBuilder()
|
||||
repository?.headers?.let {
|
||||
repository?.getRequestHeaders()?.let {
|
||||
headersBuilder.mergeWith(it, replaceExisting = false)
|
||||
}
|
||||
if (headersBuilder[CommonHeaders.USER_AGENT] == null) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
@@ -65,9 +64,6 @@ class ParserMangaRepository(
|
||||
val domains: Array<out String>
|
||||
get() = parser.configKeyDomain.presetValues
|
||||
|
||||
val headers: Headers
|
||||
get() = parser.headers
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
return if (parser is Interceptor) {
|
||||
parser.intercept(chain)
|
||||
@@ -112,6 +108,8 @@ class ParserMangaRepository(
|
||||
|
||||
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
|
||||
|
||||
fun getRequestHeaders() = parser.getRequestHeaders()
|
||||
|
||||
fun getConfigKeys(): List<ConfigKey<*>> = ArrayList<ConfigKey<*>>().also {
|
||||
parser.onCreateConfig(it)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.parser.external
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.Cursor
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.core.database.getStringOrNull
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
@@ -21,9 +14,6 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.find
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.splitTwoParts
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
|
||||
@@ -33,232 +23,58 @@ class ExternalMangaRepository(
|
||||
cache: MemoryContentCache,
|
||||
) : CachingMangaRepository(cache) {
|
||||
|
||||
private val capabilities by lazy { queryCapabilities() }
|
||||
private val contentSource = ExternalPluginContentSource(contentResolver, source)
|
||||
|
||||
private val capabilities by lazy {
|
||||
runCatching {
|
||||
contentSource.getCapabilities()
|
||||
}.onFailure {
|
||||
it.printStackTraceDebug()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override val sortOrders: Set<SortOrder>
|
||||
get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL)
|
||||
|
||||
override val states: Set<MangaState>
|
||||
get() = capabilities?.availableStates.orEmpty()
|
||||
|
||||
override val contentRatings: Set<ContentRating>
|
||||
get() = capabilities?.availableContentRating.orEmpty()
|
||||
|
||||
override var defaultSortOrder: SortOrder
|
||||
get() = capabilities?.defaultSortOrder ?: SortOrder.ALPHABETICAL
|
||||
set(value) = Unit
|
||||
|
||||
override val isMultipleTagsSupported: Boolean
|
||||
get() = capabilities?.isMultipleTagsSupported ?: true
|
||||
|
||||
override val isTagsExclusionSupported: Boolean
|
||||
get() = capabilities?.isTagsExclusionSupported ?: false
|
||||
|
||||
override val isSearchSupported: Boolean
|
||||
get() = capabilities?.isSearchSupported ?: true
|
||||
|
||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> =
|
||||
runInterruptible(Dispatchers.Default) {
|
||||
val uri = "content://${source.authority}/manga".toUri().buildUpon()
|
||||
uri.appendQueryParameter("offset", offset.toString())
|
||||
when (filter) {
|
||||
is MangaListFilter.Advanced -> {
|
||||
filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) }
|
||||
filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) }
|
||||
filter.states.forEach { uri.appendQueryParameter("state", it.name) }
|
||||
filter.locale?.let { uri.appendQueryParameter("locale", it.language) }
|
||||
filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) }
|
||||
}
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
uri.appendQueryParameter("query", filter.query)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name)?.use { cursor ->
|
||||
val result = ArrayList<Manga>(cursor.count)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
result += cursor.getManga()
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
result
|
||||
}.orEmpty()
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
contentSource.getList(offset, filter)
|
||||
}
|
||||
|
||||
override suspend fun getDetailsImpl(manga: Manga): Manga = coroutineScope {
|
||||
val chapters = async { queryChapters(manga.url) }
|
||||
val details = queryDetails(manga.url)
|
||||
Manga(
|
||||
id = manga.id,
|
||||
title = details.title.ifBlank { manga.title },
|
||||
altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle },
|
||||
url = details.url.ifEmpty { manga.url },
|
||||
publicUrl = details.publicUrl.ifEmpty { manga.publicUrl },
|
||||
rating = maxOf(details.rating, manga.rating),
|
||||
isNsfw = details.isNsfw,
|
||||
coverUrl = details.coverUrl.ifEmpty { manga.coverUrl },
|
||||
tags = details.tags + manga.tags,
|
||||
state = details.state ?: manga.state,
|
||||
author = details.author.ifNullOrEmpty { manga.author },
|
||||
largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl },
|
||||
description = details.description.ifNullOrEmpty { manga.description },
|
||||
chapters = chapters.await(),
|
||||
source = source,
|
||||
)
|
||||
override suspend fun getDetailsImpl(manga: Manga): Manga = runInterruptible(Dispatchers.IO) {
|
||||
contentSource.getDetails(manga)
|
||||
}
|
||||
|
||||
override suspend fun getPagesImpl(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.Default) {
|
||||
val uri = "content://${source.authority}/chapters".toUri()
|
||||
.buildUpon()
|
||||
.appendPath(chapter.url)
|
||||
.build()
|
||||
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val result = ArrayList<MangaPage>(cursor.count)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
result += MangaPage(
|
||||
id = cursor.getLong(0),
|
||||
url = cursor.getString(1),
|
||||
preview = cursor.getStringOrNull(2),
|
||||
source = source,
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
result
|
||||
}.orEmpty()
|
||||
override suspend fun getPagesImpl(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.IO) {
|
||||
contentSource.getPages(chapter)
|
||||
}
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String = page.url
|
||||
override suspend fun getPageUrl(page: MangaPage): String = page.url // TODO
|
||||
|
||||
override suspend fun getTags(): Set<MangaTag> = runInterruptible(Dispatchers.Default) {
|
||||
val uri = "content://${source.authority}/tags".toUri()
|
||||
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val result = ArraySet<MangaTag>(cursor.count)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
result += MangaTag(
|
||||
key = cursor.getString(0),
|
||||
title = cursor.getString(1),
|
||||
source = source,
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
result
|
||||
}.orEmpty()
|
||||
override suspend fun getTags(): Set<MangaTag> = runInterruptible(Dispatchers.IO) {
|
||||
contentSource.getTags()
|
||||
}
|
||||
|
||||
override suspend fun getLocales(): Set<Locale> = emptySet()
|
||||
override suspend fun getLocales(): Set<Locale> = emptySet() // TODO
|
||||
|
||||
override suspend fun getRelatedMangaImpl(seed: Manga): List<Manga> = emptyList() // TODO
|
||||
|
||||
private suspend fun queryDetails(url: String): Manga = runInterruptible(Dispatchers.Default) {
|
||||
val uri = "content://${source.authority}/manga".toUri()
|
||||
.buildUpon()
|
||||
.appendPath(url)
|
||||
.build()
|
||||
checkNotNull(
|
||||
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
cursor.getManga()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun queryChapters(url: String): List<MangaChapter>? = runInterruptible(Dispatchers.Default) {
|
||||
val uri = "content://${source.authority}/manga/chapters".toUri()
|
||||
.buildUpon()
|
||||
.appendPath(url)
|
||||
.build()
|
||||
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val result = ArrayList<MangaChapter>(cursor.count)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
result += MangaChapter(
|
||||
id = cursor.getLong(0),
|
||||
name = cursor.getString(1),
|
||||
number = cursor.getFloat(2),
|
||||
volume = cursor.getInt(3),
|
||||
url = cursor.getString(4),
|
||||
scanlator = cursor.getStringOrNull(5),
|
||||
uploadDate = cursor.getLong(6),
|
||||
branch = cursor.getStringOrNull(7),
|
||||
source = source,
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor.getManga() = Manga(
|
||||
id = getLong(0),
|
||||
title = getString(1),
|
||||
altTitle = getStringOrNull(2),
|
||||
url = getString(3),
|
||||
publicUrl = getString(4),
|
||||
rating = getFloat(5),
|
||||
isNsfw = getInt(6) > 1,
|
||||
coverUrl = getString(7),
|
||||
tags = getStringOrNull(8)?.split(':')?.mapNotNullToSet {
|
||||
val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
|
||||
MangaTag(key = parts.first, title = parts.second, source = source)
|
||||
}.orEmpty(),
|
||||
state = getStringOrNull(9)?.let { MangaState.entries.find(it) },
|
||||
author = optString(10),
|
||||
largeCoverUrl = optString(11),
|
||||
description = optString(12),
|
||||
chapters = emptyList(),
|
||||
source = source,
|
||||
)
|
||||
|
||||
private fun Cursor.optString(columnIndex: Int): String? {
|
||||
return if (isNull(columnIndex)) {
|
||||
null
|
||||
} else {
|
||||
getString(columnIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryCapabilities(): MangaSourceCapabilities? {
|
||||
val uri = "content://${source.authority}/capabilities".toUri()
|
||||
return contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
MangaSourceCapabilities(
|
||||
availableSortOrders = cursor.getStringOrNull(0)
|
||||
?.split(',')
|
||||
?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) {
|
||||
SortOrder.entries.find(it)
|
||||
}.orEmpty(),
|
||||
availableStates = cursor.getStringOrNull(1)
|
||||
?.split(',')
|
||||
?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) {
|
||||
MangaState.entries.find(it)
|
||||
}.orEmpty(),
|
||||
availableContentRating = cursor.getStringOrNull(2)
|
||||
?.split(',')
|
||||
?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) {
|
||||
ContentRating.entries.find(it)
|
||||
}.orEmpty(),
|
||||
isMultipleTagsSupported = cursor.getInt(3) > 1,
|
||||
isTagsExclusionSupported = cursor.getInt(4) > 1,
|
||||
isSearchSupported = cursor.getInt(5) > 1,
|
||||
contentType = ContentType.entries.find(cursor.getString(6)) ?: ContentType.OTHER,
|
||||
defaultSortOrder = cursor.getStringOrNull(7)?.let {
|
||||
SortOrder.entries.find(it)
|
||||
} ?: SortOrder.ALPHABETICAL,
|
||||
sourceLocale = cursor.getStringOrNull(8)?.let { Locale(it) } ?: Locale.ROOT,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MangaSourceCapabilities(
|
||||
val availableSortOrders: Set<SortOrder>,
|
||||
val availableStates: Set<MangaState>,
|
||||
val availableContentRating: Set<ContentRating>,
|
||||
val isMultipleTagsSupported: Boolean,
|
||||
val isTagsExclusionSupported: Boolean,
|
||||
val isSearchSupported: Boolean,
|
||||
val contentType: ContentType,
|
||||
val defaultSortOrder: SortOrder,
|
||||
val sourceLocale: Locale,
|
||||
)
|
||||
}
|
||||
|
||||
291
app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt
vendored
Normal file
291
app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/ExternalPluginContentSource.kt
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
package org.koitharu.kotatsu.core.parser.external
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.Cursor
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.core.net.toUri
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocale
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.find
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.splitTwoParts
|
||||
import java.util.EnumSet
|
||||
import java.util.Locale
|
||||
|
||||
class ExternalPluginContentSource(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val source: ExternalMangaSource,
|
||||
) {
|
||||
|
||||
@Blocking
|
||||
@WorkerThread
|
||||
fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = runCatchingCompatibility {
|
||||
val uri = "content://${source.authority}/manga".toUri().buildUpon()
|
||||
uri.appendQueryParameter("offset", offset.toString())
|
||||
when (filter) {
|
||||
is MangaListFilter.Advanced -> {
|
||||
filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) }
|
||||
filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) }
|
||||
filter.states.forEach { uri.appendQueryParameter("state", it.name) }
|
||||
filter.locale?.let { uri.appendQueryParameter("locale", it.language) }
|
||||
filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) }
|
||||
}
|
||||
|
||||
is MangaListFilter.Search -> {
|
||||
uri.appendQueryParameter("query", filter.query)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name)
|
||||
.safe()
|
||||
.use { cursor ->
|
||||
val result = ArrayList<Manga>(cursor.count)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
result += cursor.getManga()
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@WorkerThread
|
||||
fun getDetails(manga: Manga) = runCatchingCompatibility {
|
||||
val chapters = queryChapters(manga.url)
|
||||
val details = queryDetails(manga.url)
|
||||
Manga(
|
||||
id = manga.id,
|
||||
title = details.title.ifBlank { manga.title },
|
||||
altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle },
|
||||
url = details.url.ifEmpty { manga.url },
|
||||
publicUrl = details.publicUrl.ifEmpty { manga.publicUrl },
|
||||
rating = maxOf(details.rating, manga.rating),
|
||||
isNsfw = details.isNsfw,
|
||||
coverUrl = details.coverUrl.ifEmpty { manga.coverUrl },
|
||||
tags = details.tags + manga.tags,
|
||||
state = details.state ?: manga.state,
|
||||
author = details.author.ifNullOrEmpty { manga.author },
|
||||
largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl },
|
||||
description = details.description.ifNullOrEmpty { manga.description },
|
||||
chapters = chapters,
|
||||
source = source,
|
||||
)
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@WorkerThread
|
||||
fun getPages(chapter: MangaChapter): List<MangaPage> = runCatchingCompatibility {
|
||||
val uri = "content://${source.authority}/chapters".toUri()
|
||||
.buildUpon()
|
||||
.appendPath(chapter.url)
|
||||
.build()
|
||||
contentResolver.query(uri, null, null, null, null)
|
||||
.safe()
|
||||
.use { cursor ->
|
||||
val result = ArrayList<MangaPage>(cursor.count)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
result += MangaPage(
|
||||
id = cursor.getLong(COLUMN_ID),
|
||||
url = cursor.getString(COLUMN_URL),
|
||||
preview = cursor.getStringOrNull(COLUMN_PREVIEW),
|
||||
source = source,
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@WorkerThread
|
||||
fun getTags(): Set<MangaTag> = runCatchingCompatibility {
|
||||
val uri = "content://${source.authority}/tags".toUri()
|
||||
contentResolver.query(uri, null, null, null, null)
|
||||
.safe()
|
||||
.use { cursor ->
|
||||
val result = ArraySet<MangaTag>(cursor.count)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
result += MangaTag(
|
||||
key = cursor.getString(COLUMN_KEY),
|
||||
title = cursor.getString(COLUMN_TITLE),
|
||||
source = source,
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fun getCapabilities(): MangaSourceCapabilities? {
|
||||
val uri = "content://${source.authority}/capabilities".toUri()
|
||||
return contentResolver.query(uri, null, null, null, null)
|
||||
.safe()
|
||||
.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
MangaSourceCapabilities(
|
||||
availableSortOrders = cursor.getStringOrNull(COLUMN_SORT_ORDERS)
|
||||
?.split(',')
|
||||
?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) {
|
||||
SortOrder.entries.find(it)
|
||||
}.orEmpty(),
|
||||
availableStates = cursor.getStringOrNull(COLUMN_STATES)
|
||||
?.split(',')
|
||||
?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) {
|
||||
MangaState.entries.find(it)
|
||||
}.orEmpty(),
|
||||
availableContentRating = cursor.getStringOrNull(COLUMN_CONTENT_RATING)
|
||||
?.split(',')
|
||||
?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) {
|
||||
ContentRating.entries.find(it)
|
||||
}.orEmpty(),
|
||||
isMultipleTagsSupported = cursor.getBooleanOrDefault(COLUMN_MULTIPLE_TAGS_SUPPORTED, true),
|
||||
isTagsExclusionSupported = cursor.getBooleanOrDefault(COLUMN_TAGS_EXCLUSION_SUPPORTED, false),
|
||||
isSearchSupported = cursor.getBooleanOrDefault(COLUMN_SEARCH_SUPPORTED, true),
|
||||
contentType = cursor.getStringOrNull(COLUMN_CONTENT_TYPE)?.let {
|
||||
ContentType.entries.find(it)
|
||||
} ?: ContentType.OTHER,
|
||||
defaultSortOrder = cursor.getStringOrNull(COLUMN_DEFAULT_SORT_ORDER)?.let {
|
||||
SortOrder.entries.find(it)
|
||||
} ?: SortOrder.ALPHABETICAL,
|
||||
sourceLocale = cursor.getStringOrNull(COLUMN_LOCALE)?.toLocale() ?: Locale.ROOT,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryDetails(url: String): Manga {
|
||||
val uri = "content://${source.authority}/manga".toUri()
|
||||
.buildUpon()
|
||||
.appendPath(url)
|
||||
.build()
|
||||
return contentResolver.query(uri, null, null, null, null)
|
||||
.safe()
|
||||
.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
cursor.getManga()
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryChapters(url: String): List<MangaChapter> {
|
||||
val uri = "content://${source.authority}/manga/chapters".toUri()
|
||||
.buildUpon()
|
||||
.appendPath(url)
|
||||
.build()
|
||||
return contentResolver.query(uri, null, null, null, null)
|
||||
.safe()
|
||||
.use { cursor ->
|
||||
val result = ArrayList<MangaChapter>(cursor.count)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
result += MangaChapter(
|
||||
id = cursor.getLong(COLUMN_ID),
|
||||
name = cursor.getString(COLUMN_NAME),
|
||||
number = cursor.getFloatOrDefault(COLUMN_NUMBER, 0f),
|
||||
volume = cursor.getIntOrDefault(COLUMN_VOLUME, 0),
|
||||
url = cursor.getString(COLUMN_URL),
|
||||
scanlator = cursor.getStringOrNull(COLUMN_SCANLATOR),
|
||||
uploadDate = cursor.getLongOrDefault(COLUMN_UPLOAD_DATE, 0L),
|
||||
branch = cursor.getStringOrNull(COLUMN_BRANCH),
|
||||
source = source,
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
private fun SafeCursor.getManga() = Manga(
|
||||
id = getLong(COLUMN_ID),
|
||||
title = getString(COLUMN_TITLE),
|
||||
altTitle = getStringOrNull(COLUMN_ALT_TITLE),
|
||||
url = getString(COLUMN_URL),
|
||||
publicUrl = getString(COLUMN_PUBLIC_URL),
|
||||
rating = getFloat(COLUMN_RATING),
|
||||
isNsfw = getBooleanOrDefault(COLUMN_IS_NSFW, false),
|
||||
coverUrl = getString(COLUMN_COVER_URL),
|
||||
tags = getStringOrNull(COLUMN_TAGS)?.split(':')?.mapNotNullToSet {
|
||||
val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
|
||||
MangaTag(key = parts.first, title = parts.second, source = source)
|
||||
}.orEmpty(),
|
||||
state = getStringOrNull(COLUMN_STATE)?.let { MangaState.entries.find(it) },
|
||||
author = getStringOrNull(COLUMN_AUTHOR),
|
||||
largeCoverUrl = getStringOrNull(COLUMN_LARGE_COVER_URL),
|
||||
description = getStringOrNull(COLUMN_DESCRIPTION),
|
||||
chapters = emptyList(),
|
||||
source = source,
|
||||
)
|
||||
|
||||
private inline fun <R> runCatchingCompatibility(block: () -> R): R = try {
|
||||
block()
|
||||
} catch (e: NoSuchElementException) { // unknown column name
|
||||
throw IncompatiblePluginException(source.name, e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw IncompatiblePluginException(source.name, e)
|
||||
}
|
||||
|
||||
private fun Cursor?.safe() = SafeCursor(this ?: throw IncompatiblePluginException(source.name, null))
|
||||
|
||||
class MangaSourceCapabilities(
|
||||
val availableSortOrders: Set<SortOrder>,
|
||||
val availableStates: Set<MangaState>,
|
||||
val availableContentRating: Set<ContentRating>,
|
||||
val isMultipleTagsSupported: Boolean,
|
||||
val isTagsExclusionSupported: Boolean,
|
||||
val isSearchSupported: Boolean,
|
||||
val contentType: ContentType,
|
||||
val defaultSortOrder: SortOrder,
|
||||
val sourceLocale: Locale,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
|
||||
const val COLUMN_SORT_ORDERS = "sort_orders"
|
||||
const val COLUMN_STATES = "states"
|
||||
const val COLUMN_CONTENT_RATING = "content_rating"
|
||||
const val COLUMN_MULTIPLE_TAGS_SUPPORTED = "multiple_tags_supported"
|
||||
const val COLUMN_TAGS_EXCLUSION_SUPPORTED = "tags_exclusion_supported"
|
||||
const val COLUMN_SEARCH_SUPPORTED = "search_supported"
|
||||
const val COLUMN_CONTENT_TYPE = "content_type"
|
||||
const val COLUMN_DEFAULT_SORT_ORDER = "default_sort_order"
|
||||
const val COLUMN_LOCALE = "locale"
|
||||
const val COLUMN_ID = "id"
|
||||
const val COLUMN_NAME = "name"
|
||||
const val COLUMN_NUMBER = "number"
|
||||
const val COLUMN_VOLUME = "volume"
|
||||
const val COLUMN_URL = "url"
|
||||
const val COLUMN_SCANLATOR = "scanlator"
|
||||
const val COLUMN_UPLOAD_DATE = "upload_date"
|
||||
const val COLUMN_BRANCH = "branch"
|
||||
const val COLUMN_TITLE = "title"
|
||||
const val COLUMN_ALT_TITLE = "alt_title"
|
||||
const val COLUMN_PUBLIC_URL = "public_url"
|
||||
const val COLUMN_RATING = "rating"
|
||||
const val COLUMN_IS_NSFW = "is_nsfw"
|
||||
const val COLUMN_COVER_URL = "cover_url"
|
||||
const val COLUMN_TAGS = "tags"
|
||||
const val COLUMN_STATE = "state"
|
||||
const val COLUMN_AUTHOR = "author"
|
||||
const val COLUMN_LARGE_COVER_URL = "large_cover_url"
|
||||
const val COLUMN_DESCRIPTION = "description"
|
||||
const val COLUMN_PREVIEW = "preview"
|
||||
const val COLUMN_KEY = "key"
|
||||
}
|
||||
}
|
||||
73
app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/SafeCursor.kt
vendored
Normal file
73
app/src/main/kotlin/org/koitharu/kotatsu/core/parser/external/SafeCursor.kt
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
package org.koitharu.kotatsu.core.parser.external
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.CursorWrapper
|
||||
import org.koitharu.kotatsu.core.util.ext.getBoolean
|
||||
|
||||
class SafeCursor(cursor: Cursor) : CursorWrapper(cursor) {
|
||||
|
||||
fun getString(columnName: String): String {
|
||||
return getString(getColumnIndexOrThrow(columnName))
|
||||
}
|
||||
|
||||
fun getStringOrNull(columnName: String): String? {
|
||||
val columnIndex = getColumnIndex(columnName)
|
||||
return when {
|
||||
columnIndex < 0 -> null
|
||||
isNull(columnIndex) -> null
|
||||
else -> getString(columnIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBoolean(columnName: String): Boolean {
|
||||
return getBoolean(getColumnIndexOrThrow(columnName))
|
||||
}
|
||||
|
||||
fun getBooleanOrDefault(columnName: String, defaultValue: Boolean): Boolean {
|
||||
val columnIndex = getColumnIndex(columnName)
|
||||
return when {
|
||||
columnIndex < 0 -> defaultValue
|
||||
isNull(columnIndex) -> defaultValue
|
||||
else -> getBoolean(columnIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun getInt(columnName: String): Int {
|
||||
return getInt(getColumnIndexOrThrow(columnName))
|
||||
}
|
||||
|
||||
fun getIntOrDefault(columnName: String, defaultValue: Int): Int {
|
||||
val columnIndex = getColumnIndex(columnName)
|
||||
return when {
|
||||
columnIndex < 0 -> defaultValue
|
||||
isNull(columnIndex) -> defaultValue
|
||||
else -> getInt(columnIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLong(columnName: String): Long {
|
||||
return getLong(getColumnIndexOrThrow(columnName))
|
||||
}
|
||||
|
||||
fun getLongOrDefault(columnName: String, defaultValue: Long): Long {
|
||||
val columnIndex = getColumnIndex(columnName)
|
||||
return when {
|
||||
columnIndex < 0 -> defaultValue
|
||||
isNull(columnIndex) -> defaultValue
|
||||
else -> getLong(columnIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFloat(columnName: String): Float {
|
||||
return getFloat(getColumnIndexOrThrow(columnName))
|
||||
}
|
||||
|
||||
fun getFloatOrDefault(columnName: String, defaultValue: Float): Float {
|
||||
val columnIndex = getColumnIndex(columnName)
|
||||
return when {
|
||||
columnIndex < 0 -> defaultValue
|
||||
isNull(columnIndex) -> defaultValue
|
||||
else -> getFloat(columnIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -704,6 +704,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_LOGS_SHARE = "logs_share"
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||
const val PROXY_TEST = "proxy_test"
|
||||
|
||||
// old keys are for migration only
|
||||
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
|
||||
|
||||
@@ -37,3 +37,5 @@ fun JSONObject.toContentValues(): ContentValues {
|
||||
}
|
||||
|
||||
private fun String.escapeName() = "`$this`"
|
||||
|
||||
fun Cursor.getBoolean(columnIndex: Int) = getInt(columnIndex) > 0
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.exceptions.CaughtException
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
|
||||
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
|
||||
import org.koitharu.kotatsu.core.exceptions.SyncApiException
|
||||
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
|
||||
@@ -60,7 +61,7 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
||||
-> resources.getString(R.string.network_error)
|
||||
|
||||
is NoDataReceivedException -> resources.getString(R.string.error_no_data_received)
|
||||
|
||||
is IncompatiblePluginException -> resources.getString(R.string.plugin_incompatible)
|
||||
is WrongPasswordException -> resources.getString(R.string.wrong_password)
|
||||
is NotFoundException -> resources.getString(R.string.not_found_404)
|
||||
is UnsupportedSourceException -> resources.getString(R.string.unsupported_source)
|
||||
|
||||
@@ -61,7 +61,13 @@ class MangaSourcesRepository @Inject constructor(
|
||||
suspend fun getEnabledSources(): List<MangaSource> {
|
||||
assimilateNewSources()
|
||||
val order = settings.sourcesSortOrder
|
||||
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
|
||||
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order).let { enabled ->
|
||||
val external = getExternalSources()
|
||||
val list = ArrayList<MangaSourceInfo>(enabled.size + external.size)
|
||||
external.mapTo(list) { MangaSourceInfo(it, isEnabled = true, isPinned = true) }
|
||||
list.addAll(enabled)
|
||||
list
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPinnedSources(): Set<MangaSource> {
|
||||
@@ -308,8 +314,6 @@ class MangaSourcesRepository @Inject constructor(
|
||||
}
|
||||
|
||||
private fun observeExternalSources(): Flow<List<ExternalMangaSource>> {
|
||||
val intent = Intent("app.kotatsu.parser.PROVIDE_MANGA")
|
||||
val pm = context.packageManager
|
||||
return callbackFlow {
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -333,15 +337,19 @@ class MangaSourcesRepository @Inject constructor(
|
||||
}.onStart {
|
||||
emit(null)
|
||||
}.map {
|
||||
pm.queryIntentContentProviders(intent, 0).map { resolveInfo ->
|
||||
ExternalMangaSource(
|
||||
packageName = resolveInfo.providerInfo.packageName,
|
||||
authority = resolveInfo.providerInfo.authority,
|
||||
)
|
||||
}
|
||||
getExternalSources()
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun getExternalSources() = context.packageManager.queryIntentContentProviders(
|
||||
Intent("app.kotatsu.parser.PROVIDE_MANGA"), 0,
|
||||
).map { resolveInfo ->
|
||||
ExternalMangaSource(
|
||||
packageName = resolveInfo.providerInfo.packageName,
|
||||
authority = resolveInfo.providerInfo.authority,
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<MangaSourceEntity>.toSources(
|
||||
skipNsfwSources: Boolean,
|
||||
sortOrder: SourcesSortOrder?,
|
||||
|
||||
@@ -31,5 +31,7 @@ data class ReadingProgress(
|
||||
CHAPTERS_LEFT -> totalChapters > 0 && percent in 0f..1f
|
||||
}
|
||||
|
||||
fun isComplete() = percent >= 1f - Math.ulp(percent)
|
||||
|
||||
fun isReversed() = mode == PERCENT_LEFT || mode == CHAPTERS_LEFT
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.biometric.BiometricPrompt.AuthenticationCallback
|
||||
import androidx.core.graphics.Insets
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
@@ -25,6 +26,7 @@ import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivityProtectBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ProtectActivity :
|
||||
@@ -34,6 +36,7 @@ class ProtectActivity :
|
||||
View.OnClickListener {
|
||||
|
||||
private val viewModel by viewModels<ProtectViewModel>()
|
||||
private var canUseBiometric = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -61,7 +64,9 @@ class ProtectActivity :
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (!useFingerprint()) {
|
||||
canUseBiometric = useFingerprint()
|
||||
updateEndIcon()
|
||||
if (!canUseBiometric) {
|
||||
viewBinding.editPassword.requestFocus()
|
||||
}
|
||||
}
|
||||
@@ -80,6 +85,7 @@ class ProtectActivity :
|
||||
when (v.id) {
|
||||
R.id.button_next -> viewModel.tryUnlock(viewBinding.editPassword.text?.toString().orEmpty())
|
||||
R.id.button_cancel -> finish()
|
||||
materialR.id.text_input_end_icon -> useFingerprint()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +105,7 @@ class ProtectActivity :
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
viewBinding.layoutPassword.error = null
|
||||
viewBinding.buttonNext.isEnabled = !s.isNullOrEmpty()
|
||||
updateEndIcon()
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
@@ -127,6 +134,24 @@ class ProtectActivity :
|
||||
return true
|
||||
}
|
||||
|
||||
private fun updateEndIcon() = with(viewBinding.layoutPassword) {
|
||||
val isFingerprintIcon = canUseBiometric && viewBinding.editPassword.text.isNullOrEmpty()
|
||||
if (isFingerprintIcon == (endIconMode == TextInputLayout.END_ICON_CUSTOM)) {
|
||||
return@with
|
||||
}
|
||||
if (isFingerprintIcon) {
|
||||
endIconMode = TextInputLayout.END_ICON_CUSTOM
|
||||
setEndIconDrawable(androidx.biometric.R.drawable.fingerprint_dialog_fp_icon)
|
||||
endIconContentDescription = getString(androidx.biometric.R.string.use_biometric_label)
|
||||
setEndIconOnClickListener(this@ProtectActivity)
|
||||
} else {
|
||||
setEndIconOnClickListener(null)
|
||||
setEndIconDrawable(0)
|
||||
endIconContentDescription = null
|
||||
endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE
|
||||
}
|
||||
}
|
||||
|
||||
private inner class BiometricCallback : AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
|
||||
@@ -73,7 +73,7 @@ private const val PREFETCH_LIMIT = 10
|
||||
class ReaderViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
@@ -223,6 +223,7 @@ constructor(
|
||||
fun saveCurrentState(state: ReaderState? = null) {
|
||||
if (state != null) {
|
||||
currentState.value = state
|
||||
savedStateHandle[ReaderActivity.EXTRA_STATE] = state
|
||||
}
|
||||
if (incognitoMode.value) {
|
||||
return
|
||||
|
||||
@@ -13,28 +13,23 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>(), ZoomControl.ZoomControlListener {
|
||||
|
||||
protected val viewModel by activityViewModels<ReaderViewModel>()
|
||||
private var stateToSave: ReaderState? = null
|
||||
|
||||
protected var readerAdapter: BaseReaderAdapter<*>? = null
|
||||
private set
|
||||
|
||||
override fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
var restoredState = savedInstanceState?.getParcelableCompat<ReaderState>(KEY_STATE)
|
||||
readerAdapter = onCreateAdapter()
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner) {
|
||||
var pendingState = restoredState ?: it.state
|
||||
if (pendingState == null && it.pages.isNotEmpty() && readerAdapter?.hasItems != true) {
|
||||
pendingState = viewModel.getCurrentState()
|
||||
if (it.state == null && it.pages.isNotEmpty() && readerAdapter?.hasItems != true) {
|
||||
onPagesChanged(it.pages, viewModel.getCurrentState())
|
||||
} else {
|
||||
onPagesChanged(it.pages, it.state)
|
||||
}
|
||||
onPagesChanged(it.pages, pendingState)
|
||||
restoredState = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,19 +39,11 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>(), ZoomCont
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
stateToSave = getCurrentState()
|
||||
viewModel.saveCurrentState(getCurrentState())
|
||||
readerAdapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
getCurrentState()?.let {
|
||||
stateToSave = it
|
||||
}
|
||||
outState.putParcelable(KEY_STATE, stateToSave)
|
||||
}
|
||||
|
||||
protected fun requireAdapter() = checkNotNull(readerAdapter) {
|
||||
"Adapter was not created or already destroyed"
|
||||
}
|
||||
|
||||
@@ -176,8 +176,10 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
val targetChild = findTargetChild()
|
||||
adjustBounds()
|
||||
targetChild.run {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
if (!scale.isNaN()) {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
translationX = transX
|
||||
translationY = transY
|
||||
if (pendingScroll != 0) {
|
||||
@@ -298,7 +300,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
distanceX: Float,
|
||||
distanceY: Float,
|
||||
): Boolean {
|
||||
if (scale <= 1f) return false
|
||||
if (scale <= 1f || scale.isNaN()) return false
|
||||
transformMatrix.postTranslate(-distanceX, -distanceY)
|
||||
invalidateTarget()
|
||||
return true
|
||||
@@ -323,7 +325,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
velocityX: Float,
|
||||
velocityY: Float,
|
||||
): Boolean {
|
||||
if (scale <= 1) return false
|
||||
if (scale <= 1 || scale.isNaN()) return false
|
||||
|
||||
prevPos.set(transX.toInt(), transY.toInt())
|
||||
overScroller.fling(
|
||||
|
||||
@@ -7,20 +7,40 @@ import android.view.inputmethod.EditorInfo
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.network.BaseHttpClient
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.settings.utils.EditTextBindListener
|
||||
import org.koitharu.kotatsu.settings.utils.PasswordSummaryProvider
|
||||
import org.koitharu.kotatsu.settings.utils.validation.DomainValidator
|
||||
import org.koitharu.kotatsu.settings.utils.validation.PortNumberValidator
|
||||
import java.net.Proxy
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ProxySettingsFragment : BasePreferenceFragment(R.string.proxy),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private var testJob: Job? = null
|
||||
|
||||
@Inject
|
||||
@BaseHttpClient
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_proxy)
|
||||
findPreference<EditTextPreference>(AppSettings.KEY_PROXY_ADDRESS)?.setOnBindEditTextListener(
|
||||
@@ -60,6 +80,15 @@ class ProxySettingsFragment : BasePreferenceFragment(R.string.proxy),
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean = when (preference.key) {
|
||||
AppSettings.PROXY_TEST -> {
|
||||
testConnection()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_PROXY_TYPE -> updateDependencies()
|
||||
@@ -73,5 +102,47 @@ class ProxySettingsFragment : BasePreferenceFragment(R.string.proxy),
|
||||
findPreference<PreferenceCategory>(AppSettings.KEY_PROXY_AUTH)?.isEnabled = isProxyEnabled
|
||||
findPreference<Preference>(AppSettings.KEY_PROXY_LOGIN)?.isEnabled = isProxyEnabled
|
||||
findPreference<Preference>(AppSettings.KEY_PROXY_PASSWORD)?.isEnabled = isProxyEnabled
|
||||
findPreference<Preference>(AppSettings.PROXY_TEST)?.isEnabled = isProxyEnabled && testJob?.isActive != true
|
||||
}
|
||||
|
||||
private fun testConnection() {
|
||||
testJob?.cancel()
|
||||
testJob = viewLifecycleScope.launch {
|
||||
val pref = findPreference<Preference>(AppSettings.PROXY_TEST)
|
||||
pref?.run {
|
||||
setSummary(R.string.loading_)
|
||||
isEnabled = false
|
||||
}
|
||||
try {
|
||||
withContext(Dispatchers.Default) {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("http://neverssl.com")
|
||||
.build()
|
||||
val response = okHttpClient.newCall(request).await()
|
||||
check(response.isSuccessful) { response.message }
|
||||
}
|
||||
showTestResult(null)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
showTestResult(e)
|
||||
} finally {
|
||||
pref?.run {
|
||||
isEnabled = true
|
||||
summary = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showTestResult(error: Throwable?) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.proxy)
|
||||
.setMessage(error?.getDisplayMessage(resources) ?: getString(R.string.connection_ok))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||
}
|
||||
viewBinding.webView.configureForParser(repository.headers[CommonHeaders.USER_AGENT])
|
||||
viewBinding.webView.configureForParser(repository.getRequestHeaders()[CommonHeaders.USER_AGENT])
|
||||
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
|
||||
viewBinding.webView.webViewClient = BrowserClient(this)
|
||||
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
||||
|
||||
@@ -26,6 +26,7 @@ class UpdatesFragment : MangaListFragment() {
|
||||
return when (item.itemId) {
|
||||
R.id.action_remove -> {
|
||||
viewModel.remove(controller.snapshot())
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@
|
||||
<string name="sync_auth_hint">يمكنك تسجيل الدخول إلى حساب موجود أصلا أو إنشاء حساب جديد</string>
|
||||
<string name="paused">متوقف مؤقتاً</string>
|
||||
<string name="downloads_wifi_only">التحميل عبر شبكة الوايفاي فقط</string>
|
||||
<string name="suggestions_notifications_summary">إظهار الإشعارات أحيانًا بالمانغا المقترحة</string>
|
||||
<string name="suggestions_notifications_summary">إظهار الإشعارات أحيانًا بالمانجا المقترحة</string>
|
||||
<string name="mirror_switching_summary">اللغة العربية</string>
|
||||
<string name="suggestions_enable_prompt">هل ترغب في تلقي اقتراحات المانجا الشخصية؟</string>
|
||||
<string name="images_proxy_title">وكيل تحسين الصور</string>
|
||||
@@ -515,7 +515,7 @@
|
||||
<string name="speed_value">س%.1f</string>
|
||||
<string name="skip">تخطى</string>
|
||||
<string name="grayscale">تدرج الرمادي</string>
|
||||
<string name="globally">عالماً</string>
|
||||
<string name="globally">عالمياً</string>
|
||||
<string name="this_manga">هذه المانجا</string>
|
||||
<string name="color_correction_apply_text">يمكن تطبيق هذه الإعدادات عالمياً أو على المانجا الحالية فقط. إذا تم تطبيقه عالمياً، فلن يتم تجاوز الإعدادات الفردية.</string>
|
||||
<string name="apply">طَبِق</string>
|
||||
@@ -602,7 +602,7 @@
|
||||
<string name="show_updated">عرض التحديثات</string>
|
||||
<string name="rating_suggestive">موحية</string>
|
||||
<string name="reader_actions_summary">تهيئة الإجراءات لمناطق الشاشة القابلة للنقر عليها</string>
|
||||
<string name="ask_for_dest_dir_every_time">اطلب دليل الوجهة في كل مرة</string>
|
||||
<string name="ask_for_dest_dir_every_time">اطلب وجهة المجلد في كل مرة</string>
|
||||
<string name="default_page_save_dir">مجلد حفظ الصفحة الافتراضية</string>
|
||||
<string name="preferred_download_format">تنسيق التحميل المُفضل</string>
|
||||
<string name="automatic">تلقائي</string>
|
||||
|
||||
@@ -653,4 +653,9 @@
|
||||
<string name="recent_sources">Нядаўнія крыніцы</string>
|
||||
<string name="sources_pinned">Крыніцы замацаваны</string>
|
||||
<string name="crop_pages">Абрэзаць старонкі</string>
|
||||
<string name="percent_read">Працэнт прачытанага</string>
|
||||
<string name="percent_left">Астатні працэнт</string>
|
||||
<string name="chapters_read">Прачытаныя раздзелы</string>
|
||||
<string name="chapters_left">Астатнія раздзелы</string>
|
||||
<string name="external_source">Знешні/плагін</string>
|
||||
</resources>
|
||||
@@ -658,4 +658,5 @@
|
||||
<string name="chapters_read">Capítulos leídos</string>
|
||||
<string name="chapters_left">Capítulos restantes</string>
|
||||
<string name="external_source">Externo/plugin</string>
|
||||
<string name="plugin_incompatible">Complemento incompatible o error interno. Asegúrate de estar usando la última versión del complemento y de Kotatsu</string>
|
||||
</resources>
|
||||
@@ -653,4 +653,10 @@
|
||||
<string name="source_pinned">स्रोत पिन किया गया</string>
|
||||
<string name="source_unpinned">स्रोत अनपिन किया गया</string>
|
||||
<string name="recent_sources">हालिया स्रोत</string>
|
||||
<string name="percent_read">प्रतिशत पढ़ा</string>
|
||||
<string name="percent_left">प्रतिशत शेष</string>
|
||||
<string name="chapters_read">अध्याय पढ़ा</string>
|
||||
<string name="external_source">बाहरी/प्लगइन</string>
|
||||
<string name="chapters_left">अध्याय शेष</string>
|
||||
<string name="plugin_incompatible">असंगत प्लगइन या आंतरिक त्रुटि। सुनिश्चित करें कि आप प्लगइन और कोटात्सु के नवीनतम संस्करण का उपयोग कर रहे हैं</string>
|
||||
</resources>
|
||||
@@ -644,4 +644,18 @@
|
||||
<string name="crop_pages">Cortar páginas</string>
|
||||
<string name="disable_nsfw_notifications">Desativar notificações NSFW</string>
|
||||
<string name="disable_nsfw_notifications_summary">Não mostrar notificações sobre atualizações de mangás NSFW</string>
|
||||
<string name="percent_read">Porcentagem lido</string>
|
||||
<string name="percent_left">Porcentagem restante</string>
|
||||
<string name="chapters_read">Capítulos lidos</string>
|
||||
<string name="chapters_left">Capítulos restantes</string>
|
||||
<string name="pin">Pin</string>
|
||||
<string name="unpin">Unpin</string>
|
||||
<string name="source_pinned">Fonte destacada</string>
|
||||
<string name="source_unpinned">Fonte não destacada</string>
|
||||
<string name="sources_pinned">Fontes destacadas</string>
|
||||
<string name="recent_sources">Fontes recentes</string>
|
||||
<string name="external_source">Plugin/Externo</string>
|
||||
<string name="sources_unpinned">Fontes não destacadas</string>
|
||||
<string name="tracker_debug_info">Checando por novos logs de capítulos</string>
|
||||
<string name="tracker_debug_info_summary">Informações de Debug sobre a checagem de fundo para novos capítulos</string>
|
||||
</resources>
|
||||
@@ -657,4 +657,5 @@
|
||||
<string name="chapters_read">Глав прочитано</string>
|
||||
<string name="percent_left">Процент оставшегося</string>
|
||||
<string name="chapters_left">Глав осталось</string>
|
||||
<string name="external_source">Внешний/плагин</string>
|
||||
</resources>
|
||||
@@ -2,7 +2,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="local_storage">Локално складиште</string>
|
||||
<string name="error_occurred">Грешка се појавила</string>
|
||||
<string name="favourites">Омиљено</string>
|
||||
<string name="favourites">Омиљене</string>
|
||||
<string name="history">Историја</string>
|
||||
<string name="network_error">Грешка на мрежи</string>
|
||||
<string name="details">Детаљи</string>
|
||||
@@ -117,7 +117,7 @@
|
||||
<string name="show_on_shelf">Прикажи на полици</string>
|
||||
<string name="explore">Истражи</string>
|
||||
<string name="options">Опције</string>
|
||||
<string name="add_to_favourites">Додај у омиљене</string>
|
||||
<string name="add_to_favourites">Додај у Омиљене</string>
|
||||
<string name="text_history_holder_secondary">Пронађи ствари за читање у одељку „Истражи“</string>
|
||||
<string name="light_indicator">Показатељ ЛЕД светла</string>
|
||||
<string name="favourites_categories">Омиљене категорије</string>
|
||||
@@ -151,7 +151,7 @@
|
||||
<string name="cancel_all">Откажи све</string>
|
||||
<string name="sync_host_description">Можеш да користиш послуживач за синхронизацију који се самостално хостује или подразумевани. Не мењај ово ако ниси сигуран шта радиш.</string>
|
||||
<string name="error_corrupted_file">Враћени су неважећи подаци или је датотека оштећена</string>
|
||||
<string name="all_favourites">Сви омиљени</string>
|
||||
<string name="all_favourites">Све омиљене</string>
|
||||
<string name="email_enter_hint">Унесите своју адресу е-поште да бисте наставили</string>
|
||||
<string name="pick_custom_directory">Изабери прилагођени директоријум</string>
|
||||
<string name="no_chapters">Нема поглавља</string>
|
||||
@@ -189,7 +189,7 @@
|
||||
<string name="clear_all_history">Избриши сву историју</string>
|
||||
<string name="data_deletion">Брисање података</string>
|
||||
<string name="history_shortcuts">Прикажи недавне пречице за мангу</string>
|
||||
<string name="downloads_wifi_only_summary">Заустави преузимање када пређеш на мобилну мрежу</string>
|
||||
<string name="downloads_wifi_only_summary">Зауставља преузимање када пређеш на мобилну мрежу</string>
|
||||
<string name="suggest_new_sources">Предложи нове изворе након ажурирања апликације</string>
|
||||
<string name="import_completed">Увоз је завршен</string>
|
||||
<string name="show_reading_indicators">Прикажи показивач током читања</string>
|
||||
@@ -252,7 +252,7 @@
|
||||
<string name="data_not_restored">Подаци нису враћени</string>
|
||||
<string name="manage_sources">Управљај изворима</string>
|
||||
<string name="directories">Директоријуми</string>
|
||||
<string name="local_manga_directories">Локални директорији манги</string>
|
||||
<string name="local_manga_directories">Локални директоријуми Манги</string>
|
||||
<string name="manage_categories">Управљај категоријама</string>
|
||||
<string name="update">Ажурирај</string>
|
||||
<string name="scrobbling_empty_hint">Да бисте пратили напредак читања, изаберите Изборник → Прати на екрану са детаљима манге.</string>
|
||||
@@ -308,7 +308,7 @@
|
||||
<string name="dns_over_https">DNS преко HTTPS-а</string>
|
||||
<string name="show_suspicious_content">Прикажи сумњив садржај</string>
|
||||
<string name="sync_title">Синхронизујте своје податке</string>
|
||||
<string name="appwidget_shelf_description">Манга из ваших омиљених</string>
|
||||
<string name="appwidget_shelf_description">Манга из ваших Омиљених</string>
|
||||
<string name="comics_archive_import_description">Можеш да изабереш једну или више .cbz или .zip датотека, свака датотека ће бити препозната као засебна манга.</string>
|
||||
<string name="downloads_paused">Преузимања су заустављена</string>
|
||||
<string name="too_many_requests_message">Превише захтева. Покушај поново касније</string>
|
||||
@@ -400,7 +400,7 @@
|
||||
<string name="mark_as_current">Означи као тренутно</string>
|
||||
<string name="protect_application_summary">Затражи лозинку при покретању Котатсу-а</string>
|
||||
<string name="right_to_left">Са десна на лево</string>
|
||||
<string name="show_reading_indicators_summary">Прикажи проценат читања у историји и омиљеним</string>
|
||||
<string name="show_reading_indicators_summary">Прикажи проценат читања у Историји и Омиљеним</string>
|
||||
<string name="random">Насумично</string>
|
||||
<string name="mirror_switching">Аутоматски изабери послуживач</string>
|
||||
<string name="use_fingerprint">Користи отисак прста ако је доступан</string>
|
||||
@@ -495,7 +495,7 @@
|
||||
<string name="content_type_other">Остало</string>
|
||||
<string name="suggestion_manga">Предлог: %s</string>
|
||||
<string name="color_black">Црна</string>
|
||||
<string name="removed_from_favourites">Уклоњено из омиљених</string>
|
||||
<string name="removed_from_favourites">Уклоњено из Омиљених</string>
|
||||
<string name="bookmarks">Обележивачи</string>
|
||||
<string name="show_all">Покажи све</string>
|
||||
<string name="this_month">Овог месеца</string>
|
||||
@@ -657,4 +657,5 @@
|
||||
<string name="recent_sources">Недавни извори</string>
|
||||
<string name="image_server">Жељени послуживач слика</string>
|
||||
<string name="crop_pages">Изрежи странице</string>
|
||||
<string name="external_source">Спољни/додатак</string>
|
||||
</resources>
|
||||
@@ -658,4 +658,5 @@
|
||||
<string name="chapters_read">Okunan bölüm</string>
|
||||
<string name="chapters_left">Kalan bölüm</string>
|
||||
<string name="external_source">Harici/eklenti</string>
|
||||
<string name="plugin_incompatible">Uyumsuz eklenti veya dahili hata. Eklentinin ve Kotatsu\'nun en son sürümünü kullandığınızdan emin olun</string>
|
||||
</resources>
|
||||
@@ -657,4 +657,5 @@
|
||||
<string name="percent_left">Tiến trình đọc còn lại</string>
|
||||
<string name="chapters_read">Chương đã đọc</string>
|
||||
<string name="chapters_left">Chương còn lại</string>
|
||||
<string name="external_source">Nguồn / Plugin bên ngoài</string>
|
||||
</resources>
|
||||
@@ -617,7 +617,7 @@
|
||||
<string name="hours_minutes_short">%1$d 时 %2$d 分</string>
|
||||
<string name="fix">修复</string>
|
||||
<string name="missing_storage_permission">无访问外部存储漫画权限</string>
|
||||
<string name="last_used">最近使用</string>
|
||||
<string name="last_used">上次使用</string>
|
||||
<string name="show_updated">显示更新</string>
|
||||
<string name="webtoon_gaps_summary">在条漫模式下添加页与页之间的横向缝隙</string>
|
||||
<string name="webtoon_gaps">缝隙条漫模式</string>
|
||||
@@ -658,4 +658,5 @@
|
||||
<string name="chapters_read">已读章节数</string>
|
||||
<string name="chapters_left">剩余章节数</string>
|
||||
<string name="external_source">外部插件</string>
|
||||
</resources>
|
||||
<string name="plugin_incompatible">插件不兼容或出现了外部错误,请确保你已经将 Kotatsu 以及插件更新至最新版本</string>
|
||||
</resources>
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
<string name="status_on_hold">On hold</string>
|
||||
<string name="status_dropped">Dropped</string>
|
||||
<string name="disable_all">Disable all</string>
|
||||
<string name="use_fingerprint">Use fingerprint if available</string>
|
||||
<string name="use_fingerprint">Use biometric if available</string>
|
||||
<string name="appwidget_shelf_description">Manga from your favourites</string>
|
||||
<string name="appwidget_recent_description">Your recently read manga</string>
|
||||
<string name="report">Report</string>
|
||||
@@ -669,4 +669,6 @@
|
||||
<string name="chapters_read">Chapters read</string>
|
||||
<string name="chapters_left">Chapters left</string>
|
||||
<string name="external_source">External/plugin</string>
|
||||
<string name="plugin_incompatible">Incompatible plugin or internal error. Make sure you are using the latest version of the plugin and Kotatsu</string>
|
||||
<string name="connection_ok">Connection is OK</string>
|
||||
</resources>
|
||||
|
||||
@@ -36,4 +36,10 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<Preference
|
||||
android:key="proxy_test"
|
||||
android:persistent="false"
|
||||
android:title="Test connection"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -4,10 +4,10 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.5.1'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.10-RC'
|
||||
classpath 'com.android.tools.build:gradle:8.5.2'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.10'
|
||||
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
|
||||
classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:2.0.10-RC-1.0.23'
|
||||
classpath 'com.google.devtools.ksp:symbol-processing-gradle-plugin:2.0.10-1.0.24'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user