Compare commits

...

25 Commits

Author SHA1 Message Date
Koitharu
d8fa0e33f1 Update parsers 2024-07-07 12:29:08 +03:00
Koitharu
97bc638f5f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (658 of 658 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (658 of 658 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Scrambled777
064c0ae425 Translated using Weblate (Hindi)
Currently translated at 100.0% (651 of 651 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Jesús Hernández santillan
ae7aa52177 Added translation using Weblate (Abkhazian)
Co-authored-by: Jesús Hernández santillan <jesusguibel122@gmail.com>
2024-07-07 12:05:34 +03:00
gallegonovato
6edda72d61 Translated using Weblate (Spanish)
Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Akhil Raj
2f58f32bdd Translated using Weblate (Malayalam)
Currently translated at 2.6% (17 of 648 strings)

Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ml/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Eno
0b821db046 Translated using Weblate (Arabic)
Currently translated at 60.4% (392 of 648 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Eno <msa39716@zbock.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-07-07 12:05:34 +03:00
Tawsif
36472998ee Translated using Weblate (Bengali)
Currently translated at 24.5% (159 of 648 strings)

Co-authored-by: Tawsif <tawsif7492@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Abay Emes
c2e7325876 Translated using Weblate (Kazakh)
Currently translated at 84.4% (547 of 648 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Zhafran Aziz
28a4a3849c Translated using Weblate (Indonesian)
Currently translated at 99.2% (643 of 648 strings)

Co-authored-by: Zhafran Aziz <aanmts70@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Biagio Ricci
6e9c934912 Translated using Weblate (Italian)
Currently translated at 94.5% (613 of 648 strings)

Co-authored-by: Biagio Ricci <biagior00@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Макар Разин
675ef0e629 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (648 of 648 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/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-07-07 12:05:34 +03:00
Koitharu
484914b2dc Fix detection webtoon mode for local manga 2024-07-07 11:54:24 +03:00
Koitharu
ee85ef50f4 Use image proxy for downloading #897 2024-07-07 11:50:08 +03:00
Koitharu
dcee5542c5 Add recent sources to search suggestions 2024-07-07 11:38:35 +03:00
Koitharu
9b3ce4d849 Ability to pin manga sources (close #830, close #531) 2024-07-07 11:15:44 +03:00
Koitharu
5ab7e586f3 Option to sort manga sources by last used #947 2024-07-07 10:18:18 +03:00
Koitharu
9f5d4ed52c Refactor details title expansion 2024-07-07 09:38:34 +03:00
Koitharu
c3ca734005 Update reader state on chapter switch 2024-07-07 09:05:01 +03:00
Koitharu
a158a488f2 Fix error if manga has no chapters 2024-07-07 09:05:01 +03:00
Mac135135
6048cb917e Add functionality to expand manga title on click 2024-07-07 09:04:40 +03:00
Koitharu
81aac0d431 Pages crop feature #326 #919 2024-07-06 19:25:08 +03:00
Koitharu
dfb50fbddc Add image server option to reader config sheet 2024-07-06 14:21:46 +03:00
Koitharu
1f03e0a84b Update parsers and add image server option support 2024-07-06 12:47:01 +03:00
Koitharu
77e393ae48 Pages crop proof-of-concept 2024-06-29 15:56:32 +03:00
83 changed files with 1073 additions and 203 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 650
versionName = '7.2.1'
versionCode = 651
versionName = '7.3'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -82,7 +82,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:7ed8c9f787') {
implementation('com.github.KotatsuApp:kotatsu-parsers:74b8aaa94e') {
exclude group: 'org.json', module: 'json'
}
@@ -93,12 +93,12 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.9.0'
implementation 'androidx.fragment:fragment-ktx:1.8.0'
implementation 'androidx.fragment:fragment-ktx:1.8.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.2'
implementation 'androidx.lifecycle:lifecycle-service:2.8.2'
implementation 'androidx.lifecycle:lifecycle-process:2.8.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3'
implementation 'androidx.lifecycle:lifecycle-service:2.8.3'
implementation 'androidx.lifecycle:lifecycle-process:2.8.3'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
@@ -106,7 +106,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.2'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.3'
implementation 'androidx.webkit:webkit:1.11.0'
implementation 'androidx.work:work-runtime:2.9.0'
@@ -136,7 +136,7 @@ dependencies {
implementation 'io.coil-kt:coil-base:2.6.0'
implementation 'io.coil-kt:coil-svg:2.6.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:8cafac256e'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:882bc0620c'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'
@@ -154,10 +154,10 @@ dependencies {
testImplementation 'org.json:json:20240303'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'androidx.test:runner:1.6.1'
androidTestImplementation 'androidx.test:rules:1.6.1'
androidTestImplementation 'androidx.test:core-ktx:1.6.1'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.1'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
class JsonDeserializer(private val json: JSONObject) {
@@ -85,6 +86,8 @@ class JsonDeserializer(private val json: JSONObject) {
isEnabled = json.getBoolean("enabled"),
sortKey = json.getInt("sort_key"),
addedIn = json.getIntOrDefault("added_in", 0),
lastUsedAt = json.getLongOrDefault("used_at", 0L),
isPinned = json.getBooleanOrDefault("pinned", false),
)
fun toMap(): Map<String, Any?> {

View File

@@ -89,6 +89,9 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("source", e.source)
put("enabled", e.isEnabled)
put("sort_key", e.sortKey)
put("added_in", e.addedIn)
put("used_at", e.lastUsedAt)
put("pinned", e.isPinned)
},
)

View File

@@ -34,6 +34,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration18To19
import org.koitharu.kotatsu.core.db.migrations.Migration19To20
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
import org.koitharu.kotatsu.core.db.migrations.Migration21To22
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -59,7 +60,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 21
const val DATABASE_VERSION = 22
@Database(
entities = [
@@ -120,6 +121,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration18To19(),
Migration19To20(),
Migration20To21(),
Migration21To22(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -18,7 +18,7 @@ import org.koitharu.kotatsu.explore.data.SourcesSortOrder
@Dao
abstract class MangaSourcesDao {
@Query("SELECT * FROM sources ORDER BY sort_key")
@Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key")
abstract suspend fun findAll(): List<MangaSourceEntity>
@Query("SELECT source FROM sources WHERE enabled = 1")
@@ -27,7 +27,10 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources WHERE added_in >= :version")
abstract suspend fun findAllFromVersion(version: Int): List<MangaSourceEntity>
@Query("SELECT * FROM sources ORDER BY sort_key")
@Query("SELECT * FROM sources ORDER BY used_at DESC LIMIT :limit")
abstract suspend fun findLastUsed(limit: Int): List<MangaSourceEntity>
@Query("SELECT * FROM sources ORDER BY pinned DESC, sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
@Query("SELECT enabled FROM sources WHERE source = :source")
@@ -42,6 +45,12 @@ abstract class MangaSourcesDao {
@Query("UPDATE sources SET sort_key = :sortKey WHERE source = :source")
abstract suspend fun setSortKey(source: String, sortKey: Int)
@Query("UPDATE sources SET used_at = :value WHERE source = :source")
abstract suspend fun setLastUsed(source: String, value: Long)
@Query("UPDATE sources SET pinned = :isPinned WHERE source = :source")
abstract suspend fun setPinned(source: String, isPinned: Boolean)
@Insert(onConflict = OnConflictStrategy.IGNORE)
@Transaction
abstract suspend fun insertIfAbsent(entries: Collection<MangaSourceEntity>)
@@ -49,11 +58,14 @@ abstract class MangaSourcesDao {
@Upsert
abstract suspend fun upsert(entry: MangaSourceEntity)
@Query("SELECT * FROM sources WHERE pinned = 1")
abstract suspend fun findAllPinned(): List<MangaSourceEntity>
fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
val orderBy = getOrderBy(order)
@Language("RoomSql")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
return observeImpl(query)
}
@@ -61,7 +73,7 @@ abstract class MangaSourcesDao {
val orderBy = getOrderBy(order)
@Language("RoomSql")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
return findAllImpl(query)
}
@@ -73,6 +85,8 @@ abstract class MangaSourcesDao {
isEnabled = isEnabled,
sortKey = getMaxSortKey() + 1,
addedIn = BuildConfig.VERSION_CODE,
lastUsedAt = 0,
isPinned = false,
)
upsert(entity)
}
@@ -91,5 +105,6 @@ abstract class MangaSourcesDao {
SourcesSortOrder.ALPHABETIC -> "source ASC"
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"
SourcesSortOrder.MANUAL -> "sort_key ASC"
SourcesSortOrder.LAST_USED -> "used_at DESC"
}
}

View File

@@ -15,4 +15,6 @@ data class MangaSourceEntity(
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
@ColumnInfo(name = "added_in") val addedIn: Int,
@ColumnInfo(name = "used_at") val lastUsedAt: Long,
@ColumnInfo(name = "pinned") val isPinned: Boolean,
)

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration21To22 : Migration(21, 22) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE sources ADD COLUMN `used_at` INTEGER NOT NULL DEFAULT 0")
db.execSQL("ALTER TABLE sources ADD COLUMN `pinned` INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -33,7 +33,6 @@ import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import java.io.File
import java.net.Proxy
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -485,6 +484,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isAutoLocalChaptersCleanupEnabled: Boolean
get() = prefs.getBoolean(KEY_CHAPTERS_CLEAR_AUTO, false)
fun isPagesCropEnabled(mode: ReaderMode): Boolean {
val rawValue = prefs.getStringSet(KEY_READER_CROP, emptySet())
if (rawValue.isNullOrEmpty()) {
return false
}
val needle = if (mode == ReaderMode.WEBTOON) READER_CROP_WEBTOON else READER_CROP_PAGED
return needle.toString() in rawValue
}
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
}
@@ -597,6 +605,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READER_ANIMATION = "reader_animation2"
const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_READER_CROP = "reader_crop"
const val KEY_APP_PASSWORD = "app_password"
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
const val KEY_PROTECT_APP = "protect_app"
@@ -698,5 +707,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
// old keys are for migration only
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"
// values
private const val READER_CROP_PAGED = 1
private const val READER_CROP_WEBTOON = 2
}
}

View File

@@ -12,5 +12,6 @@ enum class SearchSuggestionType(
QUERIES_SUGGEST(R.string.suggested_queries),
MANGA(R.string.content_type_manga),
SOURCES(R.string.remote_sources),
RECENT_SOURCES(R.string.recent_sources),
AUTHORS(R.string.authors),
}

View File

@@ -38,6 +38,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
is ConfigKey.PreferredImageServer -> prefs.getString(key.key, key.defaultValue)?.takeUnless(String::isEmpty)
} as T
}
@@ -47,6 +48,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean)
is ConfigKey.UserAgent -> putString(key.key, (value as String?)?.sanitizeHeaderValue())
is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean)
is ConfigKey.PreferredImageServer -> putString(key.key, value as String? ?: "")
}
}

View File

@@ -1,15 +1,10 @@
package org.koitharu.kotatsu.core.ui.image
import android.graphics.Bitmap
import androidx.annotation.ColorInt
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.get
import androidx.core.graphics.green
import androidx.core.graphics.red
import coil.size.Size
import coil.transform.Transformation
import kotlin.math.abs
import org.koitharu.kotatsu.reader.domain.EdgeDetector.Companion.isColorTheSame
class TrimTransformation(
private val tolerance: Int = 20,
@@ -28,7 +23,7 @@ class TrimTransformation(
var isColBlank = true
val prevColor = input[x, 0]
for (y in 1 until input.height) {
if (!isColorTheSame(input[x, y], prevColor)) {
if (!isColorTheSame(input[x, y], prevColor, tolerance)) {
isColBlank = false
break
}
@@ -47,7 +42,7 @@ class TrimTransformation(
var isColBlank = true
val prevColor = input[x, 0]
for (y in 1 until input.height) {
if (!isColorTheSame(input[x, y], prevColor)) {
if (!isColorTheSame(input[x, y], prevColor, tolerance)) {
isColBlank = false
break
}
@@ -63,7 +58,7 @@ class TrimTransformation(
var isRowBlank = true
val prevColor = input[0, y]
for (x in 1 until input.width) {
if (!isColorTheSame(input[x, y], prevColor)) {
if (!isColorTheSame(input[x, y], prevColor, tolerance)) {
isRowBlank = false
break
}
@@ -79,7 +74,7 @@ class TrimTransformation(
var isRowBlank = true
val prevColor = input[0, y]
for (x in 1 until input.width) {
if (!isColorTheSame(input[x, y], prevColor)) {
if (!isColorTheSame(input[x, y], prevColor, tolerance)) {
isRowBlank = false
break
}
@@ -98,13 +93,6 @@ class TrimTransformation(
}
}
private fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int): Boolean {
return abs(a.red - b.red) <= tolerance &&
abs(a.green - b.green) <= tolerance &&
abs(a.blue - b.blue) <= tolerance &&
abs(a.alpha - b.alpha) <= tolerance
}
override fun equals(other: Any?): Boolean {
return this === other || (other is TrimTransformation && other.tolerance == tolerance)
}

View File

@@ -23,11 +23,16 @@ class SelectableTextView @JvmOverloads constructor(
private fun fixSelectionRange() {
if (selectionStart < 0 || selectionEnd < 0) {
val spannableText = text as? Spannable ?: return
Selection.setSelection(spannableText, text.length)
Selection.setSelection(spannableText, spannableText.length)
}
}
override fun scrollTo(x: Int, y: Int) {
super.scrollTo(0, 0)
}
fun selectAll() {
val spannableText = text as? Spannable ?: return
Selection.selectAll(spannableText)
}
}

View File

@@ -69,4 +69,11 @@ fun <T> Iterable<T>.sortedWithSafe(comparator: Comparator<in T>): List<T> = try
}
}
fun Collection<*>?.sizeOrZero() = if (this == null) 0 else size
fun Collection<*>?.sizeOrZero() = this?.size ?: 0
@Suppress("UNCHECKED_CAST")
inline fun <T, reified R> Collection<T>.mapToArray(transform: (T) -> R): Array<R> {
val result = arrayOfNulls<R>(size)
forEachIndexed { index, t -> result[index] = transform(t) }
return result as Array<R>
}

View File

@@ -12,12 +12,16 @@ import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.plus
import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.parsers.util.cancelAll
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -90,3 +94,10 @@ fun <T> Deferred<T>.peek(): T? = if (isCompleted) {
} else {
null
}
@Suppress("SuspendFunctionOnCoroutineScope")
suspend fun CoroutineScope.cancelChildrenAndJoin(cause: CancellationException? = null) {
val jobs = coroutineContext[Job]?.children?.toList() ?: return
jobs.cancelAll(cause)
jobs.joinAll()
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.util.ext
import android.graphics.Bitmap
import android.graphics.Rect
import kotlin.math.roundToInt
@@ -11,3 +12,9 @@ fun Rect.scale(factor: Double) {
(height() - newHeight) / 2,
)
}
inline fun <R> Bitmap.use(block: (Bitmap) -> R) = try {
block(this)
} finally {
recycle()
}

View File

@@ -123,7 +123,6 @@ class DetailsActivity :
lateinit var tagHighlighter: ListExtraProvider
private val viewModel: DetailsViewModel by viewModels()
private lateinit var menuProvider: DetailsMenuProvider
override fun onCreate(savedInstanceState: Bundle?) {
@@ -157,6 +156,7 @@ class DetailsActivity :
viewBinding.containerBottomSheet?.let { sheet ->
onBackPressedDispatcher.addCallback(BottomSheetCollapseCallback(sheet))
}
TitleExpandListener(viewBinding.textViewTitle).attach()
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu.details.ui
import android.annotation.SuppressLint
import android.transition.TransitionManager
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewGroup
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
@SuppressLint("ClickableViewAccessibility")
class TitleExpandListener(
private val textView: SelectableTextView,
) : GestureDetector.SimpleOnGestureListener(), OnTouchListener {
private val gestureDetector = GestureDetector(textView.context, this)
private val linesExpanded = textView.resources.getInteger(R.integer.details_description_lines)
private val linesCollapsed = textView.resources.getInteger(R.integer.details_title_lines)
override fun onTouch(v: View?, event: MotionEvent) = gestureDetector.onTouchEvent(event)
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (textView.context.isAnimationsEnabled) {
TransitionManager.beginDelayedTransition(textView.parent as ViewGroup)
}
if (textView.maxLines in 1 until Integer.MAX_VALUE) {
textView.maxLines = Integer.MAX_VALUE
} else {
textView.maxLines = linesCollapsed
}
return true
}
override fun onLongPress(e: MotionEvent) {
textView.maxLines = Integer.MAX_VALUE
textView.selectAll()
}
fun attach() {
textView.setOnTouchListener(this)
}
}

View File

@@ -31,7 +31,11 @@ fun HistoryInfo(
history: MangaHistory?,
isIncognitoMode: Boolean
): HistoryInfo {
val chapters = manga?.chapters?.get(branch)
val chapters = if (manga?.chapters?.isEmpty() == true) {
emptyList()
} else {
manga?.chapters?.get(branch)
}
val currentChapter = if (history != null && !chapters.isNullOrEmpty()) {
chapters.indexOfFirst { it.id == history.chapterId }
} else {

View File

@@ -92,7 +92,7 @@ class MangaPageFetcher(
}
else -> {
val request = PageLoader.createPageRequest(page, pageUrl)
val request = PageLoader.createPageRequest(pageUrl, page.source)
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
if (!response.isSuccessful) {
throw HttpException(response)

View File

@@ -35,16 +35,16 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.internal.closeQuietly
import okio.IOException
import okio.buffer
import okio.sink
import okio.use
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.network.CommonHeaders
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
@@ -55,6 +55,7 @@ import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.deleteWork
import org.koitharu.kotatsu.core.util.ext.deleteWorks
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getWorkInputData
import org.koitharu.kotatsu.core.util.ext.getWorkSpec
@@ -73,9 +74,9 @@ import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.domain.PageLoader
import java.io.File
import java.util.UUID
import java.util.concurrent.TimeUnit
@@ -93,6 +94,7 @@ class DownloadWorker @AssistedInject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val settings: AppSettings,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
private val imageProxyInterceptor: ImageProxyInterceptor,
notificationFactoryFactory: DownloadNotificationFactory.Factory,
) : CoroutineWorker(appContext, params) {
@@ -327,28 +329,24 @@ class DownloadWorker @AssistedInject constructor(
destination: File,
source: MangaSource,
): File {
val request = Request.Builder()
.url(url)
.tag(MangaSource::class.java, source)
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
.cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE)
.get()
.build()
val request = PageLoader.createPageRequest(url, source)
slowdownDispatcher.delay(source)
val call = okHttp.newCall(request)
val file = File(destination, UUID.randomUUID().toString() + ".tmp")
try {
val response = call.clone().await()
checkNotNull(response.body).use { body ->
file.sink(append = false).buffer().use {
it.writeAllCancellable(body.source())
return imageProxyInterceptor.interceptPageRequest(request, okHttp)
.ensureSuccess()
.use { response ->
val file = File(destination, UUID.randomUUID().toString() + ".tmp")
try {
checkNotNull(response.body).use { body ->
file.sink(append = false).buffer().use {
it.writeAllCancellable(body.source())
}
}
} catch (e: CancellationException) {
file.delete()
throw e
}
file
}
} catch (e: CancellationException) {
file.delete()
throw e
}
return file
}
private suspend fun publishState(state: DownloadState) {

View File

@@ -51,6 +51,19 @@ class MangaSourcesRepository @Inject constructor(
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
}
suspend fun getPinnedSources(): Set<MangaSource> {
assimilateNewSources()
val skipNsfw = settings.isNsfwContentDisabled
return dao.findAllPinned().mapNotNullTo(EnumSet.noneOf(MangaSource::class.java)) {
it.source.toMangaSourceOrNull()?.takeUnless { x -> skipNsfw && x.isNsfw() }
}
}
suspend fun getTopSources(limit: Int): List<MangaSource> {
assimilateNewSources()
return dao.findLastUsed(limit).toSources(settings.isNsfwContentDisabled, null)
}
suspend fun getDisabledSources(): Set<MangaSource> {
assimilateNewSources()
val result = EnumSet.copyOf(remoteSources)
@@ -214,6 +227,8 @@ class MangaSourcesRepository @Inject constructor(
isEnabled = false,
sortKey = ++maxSortKey,
addedIn = BuildConfig.VERSION_CODE,
lastUsedAt = 0,
isPinned = false,
)
}
dao.insertIfAbsent(entities)
@@ -224,6 +239,19 @@ class MangaSourcesRepository @Inject constructor(
return settings.sourcesVersion == 0 && dao.findAllEnabledNames().isEmpty()
}
suspend fun setIsPinned(sources: Collection<MangaSource>, isPinned: Boolean): ReversibleHandle {
setSourcesPinnedImpl(sources, isPinned)
return ReversibleHandle {
setSourcesEnabledImpl(sources, !isPinned)
}
}
suspend fun trackUsage(source: MangaSource) {
if (!settings.isIncognitoModeEnabled && !(settings.isHistoryExcludeNsfw && source.isNsfw())) {
dao.setLastUsed(source.name, System.currentTimeMillis())
}
}
private suspend fun setSourcesEnabledImpl(sources: Collection<MangaSource>, isEnabled: Boolean) {
if (sources.size == 1) { // fast path
dao.setEnabled(sources.first().name, isEnabled)
@@ -236,6 +264,18 @@ class MangaSourcesRepository @Inject constructor(
}
}
private suspend fun setSourcesPinnedImpl(sources: Collection<MangaSource>, isPinned: Boolean) {
if (sources.size == 1) { // fast path
dao.setPinned(sources.first().name, isPinned)
return
}
db.withTransaction {
for (source in sources) {
dao.setPinned(source.name, isPinned)
}
}
}
private suspend fun getNewSources(): MutableSet<MangaSource> {
val entities = dao.findAll()
val result = EnumSet.copyOf(remoteSources)
@@ -250,6 +290,7 @@ class MangaSourcesRepository @Inject constructor(
sortOrder: SourcesSortOrder?,
): MutableList<MangaSource> {
val result = ArrayList<MangaSource>(size)
val pinned = EnumSet.noneOf(MangaSource::class.java)
for (entity in this) {
val source = entity.source.toMangaSourceOrNull() ?: continue
if (skipNsfwSources && source.isNsfw()) {
@@ -257,10 +298,13 @@ class MangaSourcesRepository @Inject constructor(
}
if (source in remoteSources) {
result.add(source)
if (entity.isPinned) {
pinned.add(source)
}
}
}
if (sortOrder == SourcesSortOrder.ALPHABETIC) {
result.sortBy { it.title }
result.sortWith(compareBy<MangaSource> { it in pinned }.thenBy { it.title })
}
return result
}

View File

@@ -9,4 +9,5 @@ enum class SourcesSortOrder(
ALPHABETIC(R.string.by_name),
POPULARITY(R.string.popular),
MANUAL(R.string.manual),
LAST_USED(R.string.last_used),
}

View File

@@ -196,6 +196,16 @@ class ExploreFragment :
mode.finish()
}
R.id.action_pin -> {
viewModel.setSourcesPinned(selectedSources, isPinned = true)
mode.finish()
}
R.id.action_unpin -> {
viewModel.setSourcesPinned(selectedSources, isPinned = false)
mode.finish()
}
else -> return false
}
return true

View File

@@ -108,6 +108,18 @@ class ExploreViewModel @Inject constructor(
}
}
fun setSourcesPinned(sources: Set<MangaSource>, isPinned: Boolean) {
launchJob(Dispatchers.Default) {
sourcesRepository.setIsPinned(sources, isPinned)
val message = if (sources.size == 1) {
if (isPinned) R.string.source_pinned else R.string.source_unpinned
} else {
if (isPinned) R.string.sources_pinned else R.string.sources_unpinned
}
onActionDone.call(ReversibleAction(message, null))
}
}
fun respondSuggestionTip(isAccepted: Boolean) {
settings.isSuggestionsEnabled = isAccepted
settings.closeTip(TIP_SUGGESTIONS)

View File

@@ -82,6 +82,13 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
}
}
suspend fun clear() {
val cache = lruCache.get()
runInterruptible(Dispatchers.IO) {
cache.clearCache()
}
}
private suspend fun getAvailableSize(): Long = runCatchingCancellable {
val statFs = StatFs(cacheDir.get().absolutePath)
statFs.availableBytes

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.list.domain.ListExtraProvider
@@ -39,6 +40,7 @@ class LocalListViewModel @Inject constructor(
exploreRepository: ExploreRepository,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
private val localStorageManager: LocalStorageManager,
sourcesRepository: MangaSourcesRepository,
) : RemoteListViewModel(
savedStateHandle,
mangaRepositoryFactory,
@@ -47,6 +49,7 @@ class LocalListViewModel @Inject constructor(
listExtraProvider,
downloadScheduler,
exploreRepository,
sourcesRepository,
), SharedPreferences.OnSharedPreferenceChangeListener {
val onMangaRemoved = MutableEventFlow<Unit>()

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.domain
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size
import androidx.core.net.toFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
@@ -14,6 +15,8 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -61,19 +64,28 @@ class DetectReaderModeUseCase @Inject constructor(
val page = requireNotNull(pages.getOrNull(pageIndex)) { "No pages" }
val url = repository.getPageUrl(page)
val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") {
runInterruptible(Dispatchers.IO) {
val size = when {
uri.isZipUri() -> runInterruptible(Dispatchers.IO) {
val zip = ZipFile(uri.schemeSpecificPart)
val entry = zip.getEntry(uri.fragment)
zip.getInputStream(entry).use {
getBitmapSize(it)
}
}
} else {
val request = PageLoader.createPageRequest(page, url)
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
uri.isFileUri() -> runInterruptible(Dispatchers.IO) {
uri.toFile().inputStream().use {
getBitmapSize(it)
}
}
else -> {
val request = PageLoader.createPageRequest(url, page.source)
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
}
}

View File

@@ -0,0 +1,150 @@
package org.koitharu.kotatsu.reader.domain
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Point
import android.graphics.Rect
import androidx.annotation.ColorInt
import androidx.core.graphics.alpha
import androidx.core.graphics.blue
import androidx.core.graphics.get
import androidx.core.graphics.green
import androidx.core.graphics.red
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.util.ext.use
import kotlin.math.abs
class EdgeDetector(private val context: Context) {
private val mutex = Mutex()
suspend fun getBounds(imageSource: ImageSource): Rect? = mutex.withLock {
withContext(Dispatchers.IO) {
val decoder = SkiaPooledImageRegionDecoder(Bitmap.Config.RGB_565)
try {
val size = runInterruptible {
decoder.init(context, imageSource)
}
val edges = coroutineScope {
listOf(
async { detectLeftRightEdge(decoder, size, isLeft = true) },
async { detectTopBottomEdge(decoder, size, isTop = true) },
async { detectLeftRightEdge(decoder, size, isLeft = false) },
async { detectTopBottomEdge(decoder, size, isTop = false) },
).awaitAll()
}
var hasEdges = false
for (edge in edges) {
if (edge > 0) {
hasEdges = true
} else if (edge < 0) {
return@withContext null
}
}
if (hasEdges) {
Rect(edges[0], edges[1], size.x - edges[2], size.y - edges[3])
} else {
null
}
} finally {
decoder.recycle()
}
}
}
private fun detectLeftRightEdge(decoder: ImageRegionDecoder, size: Point, isLeft: Boolean): Int {
var width = size.x
val rectCount = size.x / BLOCK_SIZE
val maxRect = rectCount / 3
for (i in 0 until rectCount) {
if (i > maxRect) {
return -1
}
var dd = BLOCK_SIZE
for (j in 0 until size.y / BLOCK_SIZE) {
val regionX = if (isLeft) i * BLOCK_SIZE else size.x - (i + 1) * BLOCK_SIZE
decoder.decodeRegion(region(regionX, j * BLOCK_SIZE), 1).use { bitmap ->
for (ii in 0 until minOf(BLOCK_SIZE, dd)) {
for (jj in 0 until BLOCK_SIZE) {
val bi = if (isLeft) ii else BLOCK_SIZE - ii - 1
if (bitmap[bi, jj].isNotWhite()) {
width = minOf(width, BLOCK_SIZE * i + ii)
dd--
break
}
}
}
}
if (dd == 0) {
break
}
}
if (dd < BLOCK_SIZE) {
break // We have already found vertical field or it is not exist
}
}
return width
}
private fun detectTopBottomEdge(decoder: ImageRegionDecoder, size: Point, isTop: Boolean): Int {
var height = size.y
val rectCount = size.y / BLOCK_SIZE
val maxRect = rectCount / 3
for (j in 0 until rectCount) {
if (j > maxRect) {
return -1
}
var dd = BLOCK_SIZE
for (i in 0 until size.x / BLOCK_SIZE) {
val regionY = if (isTop) j * BLOCK_SIZE else size.y - (j + 1) * BLOCK_SIZE
decoder.decodeRegion(region(i * BLOCK_SIZE, regionY), 1).use { bitmap ->
for (jj in 0 until minOf(BLOCK_SIZE, dd)) {
for (ii in 0 until BLOCK_SIZE) {
val bj = if (isTop) jj else BLOCK_SIZE - jj - 1
if (bitmap[ii, bj].isNotWhite()) {
height = minOf(height, BLOCK_SIZE * j + jj)
dd--
break
}
}
}
}
if (dd == 0) {
break
}
}
if (dd < BLOCK_SIZE) {
break // We have already found vertical field or it is not exist
}
}
return height
}
companion object {
private const val BLOCK_SIZE = 100
private const val COLOR_TOLERANCE = 16
fun isColorTheSame(@ColorInt a: Int, @ColorInt b: Int, tolerance: Int): Boolean {
return abs(a.red - b.red) <= tolerance &&
abs(a.green - b.green) <= tolerance &&
abs(a.blue - b.blue) <= tolerance &&
abs(a.alpha - b.alpha) <= tolerance
}
private fun Int.isNotWhite() = !isColorTheSame(this, Color.WHITE, COLOR_TOLERANCE)
private fun region(x: Int, y: Int) = Rect(x, y, x + BLOCK_SIZE, y + BLOCK_SIZE)
}
}

View File

@@ -2,12 +2,14 @@ package org.koitharu.kotatsu.reader.domain
import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Rect
import android.net.Uri
import androidx.annotation.AnyThread
import androidx.collection.LongSparseArray
import androidx.collection.set
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.davemorrissey.labs.subscaleview.ImageSource
import dagger.hilt.android.ActivityRetainedLifecycle
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped
@@ -35,6 +37,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
import org.koitharu.kotatsu.core.util.ext.cancelChildrenAndJoin
import org.koitharu.kotatsu.core.util.ext.compressToPNG
import org.koitharu.kotatsu.core.util.ext.ensureRamAtLeast
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
@@ -44,6 +47,7 @@ import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
import org.koitharu.kotatsu.core.util.ext.isTargetNotEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.use
import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
import org.koitharu.kotatsu.local.data.PagesCache
@@ -51,6 +55,7 @@ import org.koitharu.kotatsu.local.data.isFileUri
import org.koitharu.kotatsu.local.data.isZipUri
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import java.util.LinkedList
import java.util.concurrent.atomic.AtomicInteger
@@ -83,6 +88,7 @@ class PageLoader @Inject constructor(
private val prefetchQueue = LinkedList<MangaPage>()
private val counter = AtomicInteger(0)
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
private val edgeDetector = EdgeDetector(context)
fun isPrefetchApplicable(): Boolean {
return repository is RemoteMangaRepository
@@ -142,22 +148,33 @@ class PageLoader @Inject constructor(
} else {
val file = uri.toFile()
context.ensureRamAtLeast(file.length() * 2)
val image = runInterruptible(Dispatchers.IO) {
runInterruptible(Dispatchers.IO) {
BitmapFactory.decodeFile(file.absolutePath)
}
try {
}.use { image ->
image.compressToPNG(file)
} finally {
image.recycle()
}
uri
}
}
suspend fun getTrimmedBounds(uri: Uri): Rect? = runCatchingCancellable {
edgeDetector.getBounds(ImageSource.Uri(uri))
}.onFailure { error ->
error.printStackTraceDebug()
}.getOrNull()
suspend fun getPageUrl(page: MangaPage): String {
return getRepository(page.source).getPageUrl(page)
}
suspend fun invalidate(clearCache: Boolean) {
tasks.clear()
loaderScope.cancelChildrenAndJoin()
if (clearCache) {
cache.clear()
}
}
private fun onIdle() = loaderScope.launch {
prefetchLock.withLock {
while (prefetchQueue.isNotEmpty()) {
@@ -213,7 +230,7 @@ class PageLoader @Inject constructor(
uri.isFileUri() -> uri
else -> {
val request = createPageRequest(page, pageUrl)
val request = createPageRequest(pageUrl, page.source)
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
val body = checkNotNull(response.body) { "Null response body" }
body.withProgress(progress).use {
@@ -248,12 +265,12 @@ class PageLoader @Inject constructor(
private const val PREFETCH_LIMIT_DEFAULT = 6
private const val PREFETCH_MIN_RAM_MB = 80L
fun createPageRequest(page: MangaPage, pageUrl: String) = Request.Builder()
fun createPageRequest(pageUrl: String, mangaSource: MangaSource) = Request.Builder()
.url(pageUrl)
.get()
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
.cacheControl(CommonHeaders.CACHE_CONTROL_NO_STORE)
.tag(MangaSource::class.java, page.source)
.tag(MangaSource::class.java, mangaSource)
.build()
}
}

View File

@@ -283,7 +283,9 @@ constructor(
prevJob?.cancelAndJoin()
content.value = ReaderContent(emptyList(), null)
chaptersLoader.loadSingleChapter(id)
content.value = ReaderContent(chaptersLoader.snapshot(), ReaderState(id, page, 0))
val newState = ReaderState(id, page, 0)
content.value = ReaderContent(chaptersLoader.snapshot(), newState)
saveCurrentState(newState)
}
}
@@ -291,17 +293,27 @@ constructor(
val prevJob = loadingJob
loadingJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
val currentChapterId = currentState.requireValue().chapterId
val allChapters = checkNotNull(manga).allChapters
var index = allChapters.indexOfFirst { x -> x.id == currentChapterId }
if (index < 0) {
return@launchLoadingJob
val prevState = currentState.requireValue()
val newChapterId = if (delta != 0) {
val allChapters = checkNotNull(manga).allChapters
var index = allChapters.indexOfFirst { x -> x.id == prevState.chapterId }
if (index < 0) {
return@launchLoadingJob
}
index += delta
(allChapters.getOrNull(index) ?: return@launchLoadingJob).id
} else {
prevState.chapterId
}
index += delta
val newChapterId = (allChapters.getOrNull(index) ?: return@launchLoadingJob).id
content.value = ReaderContent(emptyList(), null)
chaptersLoader.loadSingleChapter(newChapterId)
content.value = ReaderContent(chaptersLoader.snapshot(), ReaderState(newChapterId, 0, 0))
val newState = ReaderState(
chapterId = newChapterId,
page = if (delta == 0) prevState.page else 0,
scroll = if (delta == 0) prevState.scroll else 0,
)
content.value = ReaderContent(chaptersLoader.snapshot(), newState)
saveCurrentState(newState)
}
}

View File

@@ -0,0 +1,85 @@
package org.koitharu.kotatsu.reader.ui.config
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.mapToArray
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import kotlin.coroutines.resume
class ImageServerDelegate(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaSource: MangaSource?,
) {
private val repositoryLazy = SuspendLazy {
mangaRepositoryFactory.create(checkNotNull(mangaSource)) as RemoteMangaRepository
}
suspend fun isAvailable() = withContext(Dispatchers.Default) {
repositoryLazy.tryGet().map { repository ->
repository.getConfigKeys().any { it is ConfigKey.PreferredImageServer }
}.getOrDefault(false)
}
suspend fun getValue(): String? = withContext(Dispatchers.Default) {
repositoryLazy.tryGet().map { repository ->
val key = repository.getConfigKeys().firstNotNullOfOrNull { it as? ConfigKey.PreferredImageServer }
if (key != null) {
key.presetValues[repository.getConfig()[key]]
} else {
null
}
}.getOrNull()
}
suspend fun showDialog(context: Context): Boolean {
val repository = withContext(Dispatchers.Default) {
repositoryLazy.tryGet().getOrNull()
} ?: return false
val key = repository.getConfigKeys().firstNotNullOfOrNull {
it as? ConfigKey.PreferredImageServer
} ?: return false
val entries = key.presetValues.values.mapToArray {
it ?: context.getString(R.string.automatic)
}
val entryValues = key.presetValues.keys.toTypedArray()
val config = repository.getConfig()
val initialValue = config[key]
var currentValue = initialValue
val changed = suspendCancellableCoroutine { cont ->
val dialog = MaterialAlertDialogBuilder(context)
.setTitle(R.string.image_server)
.setCancelable(true)
.setSingleChoiceItems(entries, entryValues.indexOf(initialValue)) { _, i ->
currentValue = entryValues[i]
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.cancel()
}.setPositiveButton(android.R.string.ok) { _, _ ->
if (currentValue != initialValue) {
config[key] = currentValue
cont.resume(true)
} else {
cont.resume(false)
}
}.setOnCancelListener {
cont.resume(false)
}.create()
dialog.show()
cont.invokeOnCancellation {
dialog.cancel()
}
}
if (changed) {
repository.invalidateCache()
}
return changed
}
}

View File

@@ -16,8 +16,10 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
@@ -29,6 +31,7 @@ import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetReaderConfigBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
import org.koitharu.kotatsu.settings.SettingsActivity
@@ -47,7 +50,14 @@ class ReaderConfigSheet :
@Inject
lateinit var orientationHelper: ScreenOrientationHelper
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
@Inject
lateinit var pageLoader: PageLoader
private lateinit var mode: ReaderMode
private lateinit var imageServerDelegate: ImageServerDelegate
@Inject
lateinit var settings: AppSettings
@@ -57,6 +67,10 @@ class ReaderConfigSheet :
mode = arguments?.getInt(ARG_MODE)
?.let { ReaderMode.valueOf(it) }
?: ReaderMode.STANDARD
imageServerDelegate = ImageServerDelegate(
mangaRepositoryFactory = mangaRepositoryFactory,
mangaSource = viewModel.manga?.toManga()?.source,
)
}
override fun onCreateViewBinding(
@@ -83,11 +97,20 @@ class ReaderConfigSheet :
binding.buttonSavePage.setOnClickListener(this)
binding.buttonScreenRotate.setOnClickListener(this)
binding.buttonSettings.setOnClickListener(this)
binding.buttonImageServer.setOnClickListener(this)
binding.buttonColorFilter.setOnClickListener(this)
binding.sliderTimer.addOnChangeListener(this)
binding.switchScrollTimer.setOnCheckedChangeListener(this)
binding.switchDoubleReader.setOnCheckedChangeListener(this)
viewLifecycleScope.launch {
val isAvailable = imageServerDelegate.isAvailable()
if (isAvailable) {
bindImageServerTitle()
}
binding.buttonImageServer.isVisible = isAvailable
}
settings.observeAsStateFlow(
scope = lifecycleScope + Dispatchers.Default,
key = AppSettings.KEY_READER_AUTOSCROLL_SPEED,
@@ -124,6 +147,14 @@ class ReaderConfigSheet :
val manga = viewModel.manga?.toManga() ?: return
startActivity(ColorFilterConfigActivity.newIntent(v.context, manga, page))
}
R.id.button_image_server -> viewLifecycleScope.launch {
if (imageServerDelegate.showDialog(v.context)) {
bindImageServerTitle()
pageLoader.invalidate(clearCache = true)
viewModel.switchChapterBy(0)
}
}
}
}
@@ -194,6 +225,14 @@ class ReaderConfigSheet :
switch.setOnCheckedChangeListener(this)
}
private suspend fun bindImageServerTitle() {
viewBinding?.buttonImageServer?.text = getString(
R.string.inline_preference_pattern,
getString(R.string.image_server),
imageServerDelegate.getValue() ?: getString(R.string.automatic),
)
}
interface Callback {
var isAutoScrollEnabled: Boolean

View File

@@ -18,6 +18,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
@@ -54,6 +55,10 @@ class ReaderSettings(
view.background = bg.resolve(view.context)
}
fun isPagesCropEnabled(isWebtoon: Boolean) = settings.isPagesCropEnabled(
if (isWebtoon) ReaderMode.WEBTOON else ReaderMode.STANDARD,
)
@CheckResult
fun applyBitmapConfig(ssiv: SubsamplingScaleImageView): Boolean {
val config = bitmapConfig

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.PageHolderDelegate.State
import org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonHolder
abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B,
@@ -24,7 +25,14 @@ abstract class BasePageHolder<B : ViewBinding>(
) : LifecycleAwareViewHolder(binding.root, lifecycleOwner), PageHolderDelegate.Callback {
@Suppress("LeakingThis")
protected val delegate = PageHolderDelegate(loader, settings, this, networkState, exceptionResolver)
protected val delegate = PageHolderDelegate(
loader = loader,
readerSettings = settings,
callback = this,
networkState = networkState,
exceptionResolver = exceptionResolver,
isWebtoon = this is WebtoonHolder,
)
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
val context: Context
@@ -70,7 +78,7 @@ abstract class BasePageHolder<B : ViewBinding>(
delegate.onRecycle()
}
protected fun SubsamplingScaleImageView.applyDownsampling(isForeground: Boolean) {
protected fun SubsamplingScaleImageView.applyDownSampling(isForeground: Boolean) {
downSampling = when {
isForeground || !settings.isReaderOptimizationEnabled -> 1
context.isLowRamDevice() -> 8

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.ui.pager
import android.graphics.Rect
import android.net.Uri
import androidx.lifecycle.Observer
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
@@ -32,6 +33,7 @@ class PageHolderDelegate(
private val callback: Callback,
private val networkState: NetworkState,
private val exceptionResolver: ExceptionResolver,
private val isWebtoon: Boolean,
) : DefaultOnImageEventListener, Observer<ReaderSettings> {
private val scope = loader.loaderScope + Dispatchers.Main.immediate
@@ -39,6 +41,7 @@ class PageHolderDelegate(
private set
private var job: Job? = null
private var uri: Uri? = null
private var cachedBounds: Rect? = null
private var error: Throwable? = null
init {
@@ -88,6 +91,7 @@ class PageHolderDelegate(
fun onRecycle() {
state = State.EMPTY
uri = null
cachedBounds = null
error = null
job?.cancel()
}
@@ -95,7 +99,7 @@ class PageHolderDelegate(
fun reload() {
if (state == State.SHOWN) {
uri?.let {
callback.onImageReady(it)
callback.onImageReady(it, cachedBounds)
}
}
}
@@ -138,8 +142,13 @@ class PageHolderDelegate(
state = State.CONVERTING
try {
val newUri = loader.convertBimap(uri)
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
loader.getTrimmedBounds(newUri)
} else {
null
}
state = State.CONVERTED
callback.onImageReady(newUri)
callback.onImageReady(newUri, cachedBounds)
} catch (ce: CancellationException) {
throw ce
} catch (e2: Throwable) {
@@ -166,7 +175,12 @@ class PageHolderDelegate(
file
}
state = State.LOADED
callback.onImageReady(checkNotNull(uri))
cachedBounds = if (readerSettings.isPagesCropEnabled(isWebtoon)) {
loader.getTrimmedBounds(checkNotNull(uri))
} else {
null
}
callback.onImageReady(checkNotNull(uri), cachedBounds)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
@@ -196,7 +210,7 @@ class PageHolderDelegate(
fun onError(e: Throwable)
fun onImageReady(uri: Uri)
fun onImageReady(uri: Uri, bounds: Rect?)
fun onImageShowing(settings: ReaderSettings)

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.annotation.SuppressLint
import android.graphics.PointF
import android.graphics.Rect
import android.net.Uri
import android.view.View
import android.view.animation.DecelerateInterpolator
@@ -46,12 +47,12 @@ open class PageHolder(
override fun onResume() {
super.onResume()
binding.ssiv.applyDownsampling(isForeground = true)
binding.ssiv.applyDownSampling(isForeground = true)
}
override fun onPause() {
super.onPause()
binding.ssiv.applyDownsampling(isForeground = false)
binding.ssiv.applyDownSampling(isForeground = false)
}
override fun onConfigChanged() {
@@ -59,7 +60,7 @@ open class PageHolder(
if (settings.applyBitmapConfig(binding.ssiv)) {
delegate.reload()
}
binding.ssiv.applyDownsampling(isResumed())
binding.ssiv.applyDownSampling(isResumed())
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
}
@@ -89,8 +90,12 @@ open class PageHolder(
}
}
override fun onImageReady(uri: Uri) {
binding.ssiv.setImage(ImageSource.Uri(uri))
override fun onImageReady(uri: Uri, bounds: Rect?) {
val source = ImageSource.Uri(uri)
if (bounds != null) {
source.region(bounds)
}
binding.ssiv.setImage(source)
}
override fun onImageShowing(settings: ReaderSettings) {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.graphics.Rect
import android.net.Uri
import android.view.View
import androidx.core.view.isVisible
@@ -39,12 +40,12 @@ class WebtoonHolder(
override fun onResume() {
super.onResume()
binding.ssiv.applyDownsampling(isForeground = true)
binding.ssiv.applyDownSampling(isForeground = true)
}
override fun onPause() {
super.onPause()
binding.ssiv.applyDownsampling(isForeground = false)
binding.ssiv.applyDownSampling(isForeground = false)
}
override fun onConfigChanged() {
@@ -52,7 +53,7 @@ class WebtoonHolder(
if (settings.applyBitmapConfig(binding.ssiv)) {
delegate.reload()
}
binding.ssiv.applyDownsampling(isResumed())
binding.ssiv.applyDownSampling(isResumed())
}
override fun onBind(data: ReaderPage) {
@@ -89,8 +90,12 @@ class WebtoonHolder(
}
}
override fun onImageReady(uri: Uri) {
binding.ssiv.setImage(ImageSource.Uri(uri))
override fun onImageReady(uri: Uri, bounds: Rect?) {
val source = ImageSource.Uri(uri)
if (bounds != null) {
source.region(bounds)
}
binding.ssiv.setImage(source)
}
override fun onImageShowing(settings: ReaderSettings) {

View File

@@ -28,6 +28,7 @@ import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sizeOrZero
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.MangaFilter
@@ -45,7 +46,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.concatUrl
import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L
@@ -59,6 +59,7 @@ open class RemoteListViewModel @Inject constructor(
listExtraProvider: ListExtraProvider,
downloadScheduler: DownloadWorker.Scheduler,
private val exploreRepository: ExploreRepository,
sourcesRepository: MangaSourcesRepository,
) : MangaListViewModel(settings, downloadScheduler), MangaFilter by filter {
val source = savedStateHandle.require<MangaSource>(RemoteListFragment.ARG_SOURCE)
@@ -117,6 +118,10 @@ open class RemoteListViewModel @Inject constructor(
}.catch { error ->
listError.value = error
}.launchIn(viewModelScope)
launchJob(Dispatchers.Default) {
sourcesRepository.trackUsage(source)
}
}
override fun onRefresh() {

View File

@@ -125,6 +125,8 @@ class MangaSearchRepository @Inject constructor(
return db.getTagsDao().findRareTags(source.name, limit).toMangaTagsList()
}
suspend fun getSourcesSuggestion(limit: Int): List<MangaSource> = sourcesRepository.getTopSources(limit)
fun getSourcesSuggestion(query: String, limit: Int): List<MangaSource> {
if (query.length < 3) {
return emptyList()

View File

@@ -37,6 +37,7 @@ private const val MAX_HINTS_ITEMS = 3
private const val MAX_AUTHORS_ITEMS = 2
private const val MAX_TAGS_ITEMS = 8
private const val MAX_SOURCES_ITEMS = 6
private const val MAX_SOURCES_TIPS_ITEMS = 2
@HiltViewModel
class SearchSuggestionViewModel @Inject constructor(
@@ -149,12 +150,18 @@ class SearchSuggestionViewModel @Inject constructor(
} else {
null
}
val sourcesTipsDeferred = if (searchQuery.isEmpty() && SearchSuggestionType.RECENT_SOURCES in types) {
async { repository.getSourcesSuggestion(MAX_SOURCES_TIPS_ITEMS) }
} else {
null
}
val tags = tagsDeferred?.await()
val mangaList = mangaDeferred?.await()
val queries = queriesDeferred?.await()
val hints = hintsDeferred?.await()
val authors = authorsDeferred?.await()
val sourcesTips = sourcesTipsDeferred?.await()
buildList(queries.sizeOrZero() + sources.sizeOrZero() + authors.sizeOrZero() + hints.sizeOrZero() + 2) {
if (!tags.isNullOrEmpty()) {
@@ -167,6 +174,7 @@ class SearchSuggestionViewModel @Inject constructor(
queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
authors?.mapTo(this) { SearchSuggestionItem.Author(it) }
hints?.mapTo(this) { SearchSuggestionItem.Hint(it) }
sourcesTips?.mapTo(this) { SearchSuggestionItem.SourceTip(it) }
}
}

View File

@@ -18,6 +18,7 @@ class SearchSuggestionAdapter(
delegatesManager
.addDelegate(SEARCH_SUGGESTION_ITEM_TYPE_QUERY, searchSuggestionQueryAD(listener))
.addDelegate(searchSuggestionSourceAD(coil, lifecycleOwner, listener))
.addDelegate(searchSuggestionSourceTipAD(coil, lifecycleOwner, listener))
.addDelegate(searchSuggestionTagsAD(listener))
.addDelegate(searchSuggestionMangaListAD(coil, lifecycleOwner, listener))
.addDelegate(searchSuggestionQueryHintAD(listener))

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceTipBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionSourceTipAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener,
) =
adapterDelegateViewBinding<SearchSuggestionItem.SourceTip, SearchSuggestionItem, ItemSearchSuggestionSourceTipBinding>(
{ inflater, parent -> ItemSearchSuggestionSourceTipBinding.inflate(inflater, parent, false) },
) {
binding.root.setOnClickListener {
listener.onSourceClick(item.source)
}
bind {
binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)
placeholder(fallbackIcon)
error(fallbackIcon)
source(item.source)
enqueueWith(coil)
}
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.search.ui.suggestion.model
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -69,6 +70,18 @@ sealed interface SearchSuggestionItem : ListModel {
}
}
data class SourceTip(
val source: MangaSource,
) : SearchSuggestionItem {
val isNsfw: Boolean
get() = source.isNsfw()
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Source && other.source == source
}
}
data class Tags(
val tags: List<ChipsView.ChipModel>,
) : SearchSuggestionItem {

View File

@@ -5,6 +5,7 @@ import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
@@ -17,6 +18,7 @@ import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
import org.koitharu.kotatsu.settings.utils.PercentSummaryProvider
import org.koitharu.kotatsu.settings.utils.SliderPreference
@@ -48,6 +50,9 @@ class ReaderSettingsFragment :
entryValues = ZoomMode.entries.names()
setDefaultValueCompat(ZoomMode.FIT_CENTER.name)
}
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_CROP)?.run {
summaryProvider = MultiSummaryProvider(R.string.disabled)
}
findPreference<SliderPreference>(AppSettings.KEY_WEBTOON_ZOOM_OUT)?.summaryProvider = PercentSummaryProvider()
updateReaderModeDependency()
}

View File

@@ -2,11 +2,13 @@ package org.koitharu.kotatsu.settings.sources
import android.view.inputmethod.EditorInfo
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.mapToArray
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.settings.utils.AutoCompleteTextViewPreference
@@ -23,9 +25,9 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
is ConfigKey.Domain -> {
val presetValues = key.presetValues
if (presetValues.size <= 1) {
EditTextPreference(requireContext())
EditTextPreference(screen.context)
} else {
AutoCompleteTextViewPreference(requireContext()).apply {
AutoCompleteTextViewPreference(screen.context).apply {
entries = presetValues.toStringArray()
}
}.apply {
@@ -43,7 +45,7 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
}
is ConfigKey.UserAgent -> {
AutoCompleteTextViewPreference(requireContext()).apply {
AutoCompleteTextViewPreference(screen.context).apply {
entries = arrayOf(
UserAgents.FIREFOX_MOBILE,
UserAgents.CHROME_MOBILE,
@@ -64,19 +66,32 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
}
is ConfigKey.ShowSuspiciousContent -> {
SwitchPreferenceCompat(requireContext()).apply {
SwitchPreferenceCompat(screen.context).apply {
setDefaultValue(key.defaultValue)
setTitle(R.string.show_suspicious_content)
}
}
is ConfigKey.SplitByTranslations -> {
SwitchPreferenceCompat(requireContext()).apply {
SwitchPreferenceCompat(screen.context).apply {
setDefaultValue(key.defaultValue)
setTitle(R.string.split_by_translations)
setSummary(R.string.split_by_translations_summary)
}
}
is ConfigKey.PreferredImageServer -> {
ListPreference(screen.context).apply {
entries = key.presetValues.values.mapToArray {
it ?: context.getString(R.string.automatic)
}
entryValues = key.presetValues.keys.mapToArray { it.orEmpty() }
setDefaultValue(key.defaultValue.orEmpty())
setTitle(R.string.image_server)
setDialogTitle(R.string.image_server)
summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
}
}
}
preference.isIconSpaceReserved = false
preference.key = key.key

View File

@@ -36,7 +36,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0), Preference.OnPreferenc
addPreferencesFromRepository(viewModel.repository)
findPreference<SwitchPreferenceCompat>(KEY_ENABLE)?.run {
setOnPreferenceChangeListener(this@SourceSettingsFragment)
onPreferenceChangeListener = this@SourceSettingsFragment
}
findPreference<Preference>(KEY_AUTH)?.run {
val authProvider = viewModel.repository.getAuthProvider()

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.settings.sources.adapter
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
@@ -16,49 +17,14 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding
import org.koitharu.kotatsu.databinding.ItemTipBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
fun sourceConfigItemCheckableDelegate(
listener: SourceConfigListener,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
{ layoutInflater, parent ->
ItemSourceConfigCheckableBinding.inflate(
layoutInflater,
parent,
false,
)
},
) {
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
listener.onItemEnabledChanged(item, isChecked)
}
bind {
binding.textViewTitle.text = item.source.getTitle(context)
binding.switchToggle.isChecked = item.isEnabled
binding.switchToggle.isEnabled = item.isAvailable
binding.textViewDescription.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
crossfade(context)
error(fallbackIcon)
placeholder(fallbackIcon)
fallback(fallbackIcon)
source(item.source)
enqueueWith(coil)
}
}
}
fun sourceConfigItemDelegate2(
listener: SourceConfigListener,
coil: ImageLoader,
@@ -73,6 +39,7 @@ fun sourceConfigItemDelegate2(
},
) {
val iconPinned = ContextCompat.getDrawable(context, R.drawable.ic_pin_small)
val eventListener = View.OnClickListener { v ->
when (v.id) {
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
@@ -89,6 +56,7 @@ fun sourceConfigItemDelegate2(
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
binding.imageViewRemove.isVisible = item.isEnabled
binding.imageViewMenu.isVisible = item.isEnabled
binding.textViewTitle.drawableStart = if (item.isPinned) iconPinned else null
binding.textViewDescription.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
@@ -132,12 +100,15 @@ private fun showSourceMenu(
menu.inflate(R.menu.popup_source_config)
menu.menu.findItem(R.id.action_shortcut)
?.isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(anchor.context)
menu.menu.findItem(R.id.action_pin)?.isVisible = item.isEnabled
menu.menu.findItem(R.id.action_pin)?.isChecked = item.isPinned
menu.menu.findItem(R.id.action_lift)?.isVisible = item.isDraggable
menu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_settings -> listener.onItemSettingsClick(item)
R.id.action_lift -> listener.onItemLiftClick(item)
R.id.action_shortcut -> listener.onItemShortcutClick(item)
R.id.action_pin -> listener.onItemPinClick(item)
}
true
}

View File

@@ -11,5 +11,7 @@ interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {
fun onItemShortcutClick(item: SourceConfigItem.SourceItem)
fun onItemPinClick(item: SourceConfigItem.SourceItem)
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
}

View File

@@ -61,6 +61,7 @@ class SourcesListProducer @Inject constructor(
private suspend fun buildList(): List<SourceConfigItem> {
val enabledSources = repository.getEnabledSources()
val pinned = repository.getPinnedSources()
val isNsfwDisabled = settings.isNsfwContentDisabled
val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL
val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER)
@@ -75,6 +76,7 @@ class SourcesListProducer @Inject constructor(
isEnabled = it in enabledSet,
isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(),
isPinned = it in pinned,
)
}.ifEmpty {
listOf(SourceConfigItem.EmptySearchResult)
@@ -95,6 +97,7 @@ class SourcesListProducer @Inject constructor(
isEnabled = true,
isDraggable = isReorderAvailable,
isAvailable = false,
isPinned = it in pinned,
)
}
}

View File

@@ -120,6 +120,10 @@ class SourcesManageFragment :
}
}
override fun onItemPinClick(item: SourceConfigItem.SourceItem) {
viewModel.setPinned(item.source, !item.isPinned)
}
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
viewModel.setEnabled(item.source, isEnabled)
}

View File

@@ -58,8 +58,9 @@ class SourcesManageViewModel @Inject constructor(
fun canReorder(oldPos: Int, newPos: Int): Boolean {
val snapshot = content.value
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isEnabled != true) return false
return (snapshot[newPos] as? SourceConfigItem.SourceItem)?.isEnabled == true
val oldPosItem = snapshot.getOrNull(oldPos) as? SourceConfigItem.SourceItem ?: return false
val newPosItem = snapshot.getOrNull(newPos) as? SourceConfigItem.SourceItem ?: return false
return oldPosItem.isEnabled && newPosItem.isEnabled && oldPosItem.isPinned == newPosItem.isPinned
}
fun setEnabled(source: MangaSource, isEnabled: Boolean) {
@@ -71,6 +72,14 @@ class SourcesManageViewModel @Inject constructor(
}
}
fun setPinned(source: MangaSource, isPinned: Boolean) {
launchJob(Dispatchers.Default) {
val rollback = repository.setIsPinned(setOf(source), isPinned)
val message = if (isPinned) R.string.source_pinned else R.string.source_unpinned
onActionDone.call(ReversibleAction(message, rollback))
}
}
fun bringToTop(source: MangaSource) {
val snapshot = content.value
launchJob(Dispatchers.Default) {

View File

@@ -13,6 +13,7 @@ sealed interface SourceConfigItem : ListModel {
val isEnabled: Boolean,
val isDraggable: Boolean,
val isAvailable: Boolean,
val isPinned: Boolean,
) : SourceConfigItem {
val isNsfw: Boolean

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3M15.96,10.29L13.21,13.83L11.25,11.47L8.5,15H19.5L15.96,10.29Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z" />
</vector>

View File

@@ -6,6 +6,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:fillColor="#000000"
android:pathData="M19.43 12.98c0.04-0.32 0.07-0.64 0.07-0.98 0-0.34-0.03-0.66-0.07-0.98l2.11-1.65c0.19-0.15 0.24-0.42 0.12-0.64l-2-3.46c-0.09-0.16-0.26-0.25-0.44-0.25-0.06 0-0.12 0.01-0.17 0.03l-2.49 1c-0.52-0.4-1.08-0.73-1.69-0.98l-0.38-2.65C14.46 2.18 14.25 2 14 2h-4C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05l-2.49-1C4.89 5.03 4.83 5.02 4.77 5.02c-0.17 0-0.34 0.09-0.43 0.25l-2 3.46C2.21 8.95 2.27 9.22 2.46 9.37l2.11 1.65C4.53 11.34 4.5 11.67 4.5 12c0 0.33 0.03 0.66 0.07 0.98l-2.11 1.65c-0.19 0.15-0.24 0.42-0.12 0.64l2 3.46c0.09 0.16 0.26 0.25 0.44 0.25 0.06 0 0.12-0.01 0.17-0.03l2.49-1c0.52 0.4 1.08 0.73 1.69 0.98l0.38 2.65C9.54 21.82 9.75 22 10 22h4c0.25 0 0.46-0.18 0.49-0.42l0.38-2.65c0.61-0.25 1.17-0.59 1.69-0.98l2.49 1c0.06 0.02 0.12 0.03 0.18 0.03 0.17 0 0.34-0.09 0.43-0.25l2-3.46c0.12-0.22 0.07-0.49-0.12-0.64l-2.11-1.65zm-1.98-1.71c0.04 0.31 0.05 0.52 0.05 0.73 0 0.21-0.02 0.43-0.05 0.73l-0.14 1.13 0.89 0.7 1.08 0.84-0.7 1.21-1.27-0.51-1.04-0.42-0.9 0.68c-0.43 0.32-0.84 0.56-1.25 0.73l-1.06 0.43-0.16 1.13L12.7 20h-1.4l-0.19-1.35-0.16-1.13-1.06-0.43c-0.43-0.18-0.83-0.41-1.23-0.71l-0.91-0.7-1.06 0.43-1.27 0.51-0.7-1.21 1.08-0.84 0.89-0.7-0.14-1.13C6.52 12.43 6.5 12.2 6.5 12s0.02-0.43 0.05-0.73l0.14-1.13-0.89-0.7L4.72 8.6l0.7-1.21L6.69 7.9l1.04 0.42 0.9-0.68c0.43-0.32 0.84-0.56 1.25-0.73l1.06-0.43 0.16-1.13L11.3 4h1.39l0.19 1.35 0.16 1.13 1.06 0.43c0.43 0.18 0.83 0.41 1.23 0.71l0.91 0.7 1.06-0.43 1.27-0.51 0.7 1.21-1.07 0.85-0.89 0.7 0.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M14,14.5V12H10V15H8V11A1,1 0 0,1 9,10H14V7.5L17.5,11M21.71,11.29L12.71,2.29H12.7C12.31,1.9 11.68,1.9 11.29,2.29L2.29,11.29C1.9,11.68 1.9,12.32 2.29,12.71L11.29,21.71C11.68,22.09 12.31,22.1 12.71,21.71L21.71,12.71C22.1,12.32 22.1,11.68 21.71,11.29Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M8,6.2V4H7V2H17V4H16V12L18,14V16H17.8L14,12.2V4H10V8.2L8,6.2M20,20.7L18.7,22L12.8,16.1V22H11.2V16H6V14L8,12V11.3L2,5.3L3.3,4L20,20.7M8.8,14H10.6L9.7,13.1L8.8,14Z" />
</vector>

View File

@@ -84,7 +84,7 @@
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="5"
android:maxLines="@integer/details_title_lines"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingVertical="@dimen/margin_small">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:scaleType="centerCrop"
app:shapeAppearance="?shapeAppearanceCornerSmall"
tools:src="@tools:sample/backgrounds/scenic" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="?listPreferredItemPaddingEnd"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[2]" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
tools:text="@tools:sample/lorem[2]" />
</LinearLayout>
</LinearLayout>

View File

@@ -35,9 +35,11 @@
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:drawableStart="@drawable/ic_pin_small"
tools:text="@tools:sample/lorem[15]" />
<TextView

View File

@@ -210,6 +210,19 @@
android:textAppearance="?attr/textAppearanceButton"
app:drawableStartCompat="@drawable/ic_appearance" />
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
android:id="@+id/button_image_server"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/image_server"
android:textAppearance="?attr/textAppearanceButton"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_images"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
android:id="@+id/button_settings"
android:layout_width="match_parent"

View File

@@ -11,10 +11,22 @@
<item
android:id="@+id/action_shortcut"
android:icon="@drawable/ic_pin"
android:icon="@drawable/ic_shortcut"
android:title="@string/create_shortcut"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_pin"
android:icon="@drawable/ic_pin"
android:title="@string/pin"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_unpin"
android:icon="@drawable/ic_unpin"
android:title="@string/unpin"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"

View File

@@ -6,6 +6,11 @@
android:id="@+id/action_lift"
android:title="@string/to_top" />
<item
android:id="@+id/action_pin"
android:checkable="true"
android:title="@string/pin" />
<item
android:id="@+id/action_shortcut"
android:title="@string/create_shortcut" />

View File

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

View File

@@ -64,4 +64,12 @@
<item quantity="many">إحدى عشر دقيقة</item>
<item quantity="other">مئة دقيقة</item>
</plurals>
<plurals name="hours">
<item quantity="zero">العربية</item>
<item quantity="one"></item>
<item quantity="two"></item>
<item quantity="few"></item>
<item quantity="many"></item>
<item quantity="other"></item>
</plurals>
</resources>

View File

@@ -383,4 +383,6 @@
<string name="paused">متوقف مؤقتاً</string>
<string name="downloads_wifi_only">التحميل عبر شبكة الوايفاي فقط</string>
<string name="suggestions_notifications_summary">إظهار الإشعارات أحيانًا بالمانغا المقترحة</string>
<string name="mirror_switching_summary">اللغة العربية</string>
<string name="suggestions_enable_prompt"></string>
</resources>

View File

@@ -22,7 +22,7 @@
<string name="history_is_empty">Гісторыя пустая</string>
<string name="read">Чытаць</string>
<string name="you_have_not_favourites_yet">Дадайце цікавую для вас мангу ў абранае, каб не страціць яе</string>
<string name="add_to_favourites">Дадаць ў абраныя</string>
<string name="add_to_favourites">Дадаць у абраныя</string>
<string name="add_new_category">Стварыць катэгорыю</string>
<string name="add">Дадаць</string>
<string name="save">Захаваць</string>
@@ -45,7 +45,7 @@
<string name="theme">Тэма</string>
<string name="light">Светлая</string>
<string name="dark">Цёмная</string>
<string name="follow_system">Як ў сістэме</string>
<string name="follow_system">Як у сістэме</string>
<string name="pages">Старонкi</string>
<string name="clear">Ачысціць</string>
<string name="remove">Выдаліць</string>
@@ -78,7 +78,7 @@
<string name="external_storage">Знешняе сховішча</string>
<string name="domain">Дамен</string>
<string name="app_update_available">Даступна абнаўленне праграмы</string>
<string name="open_in_browser">Адкрыць ў браўзеры</string>
<string name="open_in_browser">Адкрыць у браўзеры</string>
<string name="notifications">Паведамленні</string>
<string name="enabled_d_of_d">Ўключана %1$d з %2$d</string>
<string name="new_chapters">Новыя раздзелы</string>
@@ -125,7 +125,7 @@
<string name="right_to_left">Справа налева</string>
<string name="create_category">Стварыць катэгорыю</string>
<string name="scale_mode">Маштабаванне</string>
<string name="zoom_mode_fit_center">Ўмясціць ў экран</string>
<string name="zoom_mode_fit_center">Умясціць у экран</string>
<string name="zoom_mode_fit_height">Падагнаць па вышыні</string>
<string name="zoom_mode_fit_width">Падагнаць па шырыні</string>
<string name="zoom_mode_keep_start">Зыходны памер</string>
@@ -137,7 +137,7 @@
<string name="data_restored">Данныя адноўлены</string>
<string name="preparing_">Падрыхтоўка…</string>
<string name="file_not_found">Файл не знойдзены</string>
<string name="data_restored_success">Ўсе данныя паспяхова адноўлены</string>
<string name="data_restored_success">Усе данныя паспяхова адноўлены</string>
<string name="data_restored_with_errors">Данныя адноўлены, але ўзніклі некаторыя памылкі</string>
<string name="backup_information">Вы можаце стварыць рэзервовую копію абранага і гісторыі і потым аднавіць іх</string>
<string name="just_now">Толькі што</string>
@@ -153,7 +153,7 @@
<string name="clear_cookies">Ачысціць кукi</string>
<string name="cookies_cleared">Ўсе кукi выдалены</string>
<string name="clear_feed">Ачысціць стужку</string>
<string name="text_clear_updates_feed_prompt">Ўся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя?</string>
<string name="text_clear_updates_feed_prompt">Уся гісторыя абнаўленняў будзе ачышчана і яе нельга будзе вярнуць. Вы ўпэўненыя?</string>
<string name="check_for_new_chapters">Праверка новых глаў</string>
<string name="reverse">Ў адваротным парадку</string>
<string name="sign_in">Ўвайсці</string>
@@ -163,14 +163,14 @@
<string name="protect_application_subtitle">Калі ласка, увядзіце пароль, які спатрэбіцца пры запуску праграмы</string>
<string name="confirm">Пацвердзіць</string>
<string name="password_length_hint">Пароль павінен змяшчаць не менш за 4 сімвалы</string>
<string name="text_clear_search_history_prompt">Вы сапраўды хочаце выдаліць ўсе апошнія пошукавыя запыты?</string>
<string name="text_clear_search_history_prompt">Вы сапраўды хочаце выдаліць усе апошнія пошукавыя запыты?</string>
<string name="read_more">Падрабязна</string>
<string name="tracker_warning">Некаторыя вытворцы могуць змяняць паводзіны сістэмы, што можа парушаць выкананне фонавых задач.</string>
<string name="backup_saved">Рэзервовая копія паспяхова захавана</string>
<string name="welcome">Вітаю</string>
<string name="text_local_holder_secondary">Захавайце што-небудзь з інтэрнэт-каталога або імпартуйце гэта з файла.</string>
<string name="text_local_holder_primary">Спачатку захавайце што-небудзь</string>
<string name="text_history_holder_secondary">Знайдзіце, што пачытаць, ў раздзеле «Агляд»</string>
<string name="text_history_holder_secondary">Знайдзіце, што пачытаць, у раздзеле «Агляд»</string>
<string name="text_history_holder_primary">Тут будзе паказана манга, якую вы чытаеце</string>
<string name="text_search_holder_secondary">Паспрабуйце перафармуляваць запыт.</string>
<string name="text_empty_holder_primary">Неяк тут пуста…</string>
@@ -178,7 +178,7 @@
<string name="queued">Ў чарзе</string>
<string name="about_app_translation_summary">Дапамагчы з перакладам праграмы</string>
<string name="about_app_translation">Пераклад</string>
<string name="text_clear_cookies_prompt">Вы выйдзеце з усіх крыніц, ў якіх вы аўтарызаваны</string>
<string name="text_clear_cookies_prompt">Вы выйдзеце з усіх крыніц, у якіх вы аўтарызаваны</string>
<string name="auth_not_supported_by">Аўтарызацыя на %s не падтрымліваецца</string>
<string name="auth_complete">Аўтарызацыя выканана</string>
<string name="genres">Жанры</string>
@@ -196,7 +196,7 @@
<string name="enabled">Ўключаны</string>
<string name="exclude_nsfw_from_suggestions">Ня прапаноўваць NSFW мангу</string>
<string name="text_suggestion_holder">Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы</string>
<string name="suggestions_info">Ўсе даныя аналізуюцца толькі лакальна на гэтай прыладзе і нікуды не адпраўляюцца.</string>
<string name="suggestions_info">Усе даныя аналізуюцца толькі лакальна на гэтай прыладзе і нікуды не адпраўляюцца.</string>
<string name="suggestions_summary">Прапануеце мангу, заснаваную на вашых перавагах</string>
<string name="suggestions_enable">Ўключыць прапановы</string>
<string name="suggestions">Прапанова</string>
@@ -211,10 +211,10 @@
<string name="various_languages">Розныя мовы</string>
<string name="search_chapters">Знайсці главу</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="chapters_empty">Ў гэтай манге няма раздзелаў</string>
<string name="chapters_empty">У гэтай манге няма раздзелаў</string>
<string name="hide">Схаваць</string>
<string name="appearance">Знешні выгляд</string>
<string name="disable_all">Выключыць ўсё</string>
<string name="disable_all">Адключыць усе</string>
<string name="use_fingerprint">Выкарыстоўваць адбітак пальца, калі даступна</string>
<string name="appwidget_shelf_description">Манга з абраных</string>
<string name="appwidget_recent_description">Манга, якую вы нядаўна чыталі</string>
@@ -235,7 +235,7 @@
<string name="new_sources_text">Даступныя новыя крыніцы мангі</string>
<string name="download_slowdown">Запавольванне спампоўкі</string>
<string name="suggestions_excluded_genres">Выключыць жанры</string>
<string name="suggestions_excluded_genres_summary">Укажыце жанры, якія вы не хочаце бачыць ў рэкамендацыях</string>
<string name="suggestions_excluded_genres_summary">Укажыце жанры, якія вы не хочаце бачыць у рэкамендацыях</string>
<string name="text_delete_local_manga_batch">Выдаліць выбраныя элементы з прылады назаўжды\?</string>
<string name="removal_completed">Выдаленне завершана</string>
<string name="download_slowdown_summary">Дапамагае пазбегнуць блакіроўкі па IP-адрасе</string>
@@ -263,20 +263,20 @@
<string name="logout">Выйсці</string>
<string name="show_reading_indicators_summary">Паказваць працэнт прачытанага ў гісторыі і абраных</string>
<string name="data_deletion">Выдаленне даных</string>
<string name="show_all">Паказаць ўсе</string>
<string name="show_all">Паказаць усе</string>
<string name="exclude_nsfw_from_history_summary">Манга, пазначаная як NSFW, не будзе дададзеная ў гісторыю і ваш прагрэс не будзе захаваны</string>
<string name="clear_cookies_summary">Можа дапамагчы з некаторымі праблемам. Ўсе аўтарызацыі будуць ануляваныя</string>
<string name="clear_cookies_summary">Можа дапамагчы з некаторымі праблемам. Усе аўтарызацыі будуць ануляваныя</string>
<string name="not_found_404">Змесціва не знойдзена ці выдалена</string>
<string name="status_re_reading">Перачытваю</string>
<string name="select_range">Выберыце дыяпазон</string>
<string name="nothing_here">Тут нічога няма</string>
<string name="services">Службы</string>
<string name="theme_name_kanade">Канадзе</string>
<string name="clear_all_history">Ачысціць ўсю гісторыю</string>
<string name="clear_all_history">Ачысціць усю гісторыю</string>
<string name="history_cleared">Гісторыя ачышчана</string>
<string name="incognito_mode">Рэжым інкогніта</string>
<string name="categories_delete_confirm">Вы ўпэўнены, што хочаце выдаліць выбраныя абраныя катэгорыі?
\nЎся манга ў ім будзе страчана, і гэта нельга будзе адрабіць.</string>
\nУся манга ў ім будзе страчана, і гэта нельга будзе адрабіць.</string>
<string name="no_bookmarks_summary">Вы можаце стварыць закладку падчас чытання мангі</string>
<string name="saved_manga">Захаваная манга</string>
<string name="theme_name_mamimi">Мамімі</string>
@@ -288,7 +288,7 @@
<string name="mark_as_current">Пазначыць як бягучы</string>
<string name="error_no_space_left">На прыладзе не засталося месца</string>
<string name="network_unavailable">Сетка недаступная</string>
<string name="network_unavailable_hint">Каб чытаць мангу онлайн, ўключыце Wi-Fi або мабільную сетку</string>
<string name="network_unavailable_hint">Каб чытаць мангу онлайн, уключыце Wi-Fi або мабільную сетку</string>
<string name="webtoon_zoom">Маштабаванне ў рэжыме манхвы</string>
<string name="theme_name_dynamic">Дынамічны</string>
<string name="color_theme">Каляровая гама</string>
@@ -304,7 +304,7 @@
<string name="history_shortcuts">Паказаць апошнія ярлыкі мангі</string>
<string name="history_shortcuts_summary">Зрабіце нядаўнюю мангу даступнай, доўга націскаючы на значок праграмы</string>
<string name="reader_control_ltr_summary">Націск на правы край або націск правай клавішы заўсёды перамыкае на наступную старонку.</string>
<string name="reader_control_ltr">Эрганамічны упраўленне чытаннем</string>
<string name="reader_control_ltr">Эрганамічнае кіраванне рэжымам чытання</string>
<string name="color_correction">Карэкцыя колеру</string>
<string name="brightness">Яркасць</string>
<string name="contrast">Кантраст</string>
@@ -321,7 +321,7 @@
<string name="feed">Стужка</string>
<string name="reader_slider">Паказаць паўзунок пераключэння старонак</string>
<string name="source_disabled">Крыніца адключана</string>
<string name="show_in_grid_view">Паказаць ў выглядзе сеткі</string>
<string name="show_in_grid_view">Паказаць у выглядзе сеткі</string>
<string name="theme_name_miku">Міку</string>
<string name="theme_name_asuka">Аска</string>
<string name="theme_name_mion">Міён</string>
@@ -330,7 +330,7 @@
<string name="no_chapters">Няма раздзелаў</string>
<string name="automatic_scroll">Аўтаматычная пракрутка</string>
<string name="reader_info_pattern">Разд. %1$d/%2$d Стар. %3$d/%4$d</string>
<string name="reader_info_bar">Паказаць інфармацыйную панэль ў праграме чытання</string>
<string name="reader_info_bar">Паказаць інфармацыйную панэль у праграме чытання</string>
<string name="comics_archive">Архіў коміксаў</string>
<string name="folder_with_images">Тэчка з малюнкамі</string>
<string name="importing_manga">Імпарт мангі</string>
@@ -339,7 +339,7 @@
<string name="no_bookmarks_yet">Закладак пакуль няма</string>
<string name="bookmarks_removed">Закладкі выдалены</string>
<string name="no_manga_sources">Няма крыніц мангі</string>
<string name="no_manga_sources_text">Каб чытаць мангу онлайн, ўключыце крыніцы мангі</string>
<string name="no_manga_sources_text">Каб чытаць мангу онлайн, уключыце крыніцы мангі</string>
<string name="random">Выпадковы</string>
<string name="reorder">Змяніць парадак</string>
<string name="empty">Пуста</string>
@@ -373,7 +373,7 @@
<string name="ignore_ssl_errors">Ігнараваць памылкі SSL</string>
<string name="resume">Аднавіць</string>
<string name="paused">Прыпынена</string>
<string name="cancel_all">Адмяніць ўсё</string>
<string name="cancel_all">Адмяніць усе</string>
<string name="downloads_wifi_only">Спампаваць толькі праз Wi-Fi</string>
<string name="sync_host_description">Вы можаце выкарыстоўваць уласны сервер сінхранізацыі або сервер па змаўчанні. Не змяняйце гэта, калі вы не ўпэўненыя, што робіце.</string>
<string name="pause">Паўза</string>
@@ -382,7 +382,7 @@
<string name="suggestions_notifications_summary">Часам паказваць апавяшчэнні з прапанаванай мангай</string>
<string name="more">Больш</string>
<string name="enable">Ўключыць</string>
<string name="cancel_all_downloads_confirm">Ўсе актыўныя спампоўкі будуць адменены, часткова спампаваныя даныя будуць страчаны</string>
<string name="cancel_all_downloads_confirm">Усе актыўныя спампоўкі будуць адменены, часткова спампаваныя даныя будуць страчаны</string>
<string name="suggestions_enable_prompt">Хочаце атрымліваць персаналізаваныя прапановы мангі\?</string>
<string name="suggestion_manga">Прапанова: %s</string>
<string name="no_thanks">Не, дзякуй</string>
@@ -407,13 +407,13 @@
<string name="images_procy_description">Выкарыстоўвайце службу wsrv.nl, каб паменшыць выкарыстанне трафіку і паскорыць загрузку малюнкаў, калі гэта магчыма</string>
<string name="password">Пароль</string>
<string name="invert_colors">Інвертаваць колеры</string>
<string name="show_pages_numbers_summary">Паказаць нумары старонак ў ніжнім куце</string>
<string name="show_pages_numbers_summary">Паказаць нумары старонак у ніжнім куце</string>
<string name="network">Сетка</string>
<string name="data_and_privacy">Дадзеныя і канфідэнцыяльнасць</string>
<string name="webtoon_zoom_summary">Дазволіць жэсты маштабавання ў рэжыме манхвы</string>
<string name="restore_summary">Аднавіць раней створаную рэзервовую копію</string>
<string name="reader_info_bar_summary">Паказаць бягучы час і ход чытання ў верхняй частцы экрана</string>
<string name="clear_source_cookies_summary">Выдаліць файлы cookie толькі для вызначанага дамена. Ў большасці выпадкаў гэта робіць аўтарызацыю несапраўднай</string>
<string name="clear_source_cookies_summary">Выдаліць файлы cookie толькі для вызначанага дамена. У большасці выпадкаў гэта робіць аўтарызацыю несапраўднай</string>
<string name="download_option_whole_manga">Манга цалкам</string>
<string name="local_manga_directories">Лакальныя каталогі мангі</string>
<string name="download_option_all_chapters">Ўсе раздзелы з перакладам %s</string>
@@ -459,10 +459,10 @@
<string name="main_screen_sections">Раздзелы галоўнага экрана</string>
<string name="to_top">Ўверх</string>
<string name="zoom_in">Павялічыць</string>
<string name="reader_zoom_buttons_summary">Ці паказваць кнопкі кіравання маштабаваннем ў правым ніжнім куце</string>
<string name="reader_zoom_buttons_summary">Ці паказваць кнопкі кіравання маштабаваннем у правым ніжнім куце</string>
<string name="reader_zoom_buttons">Паказаць кнопкі маштабавання</string>
<string name="zoom_out">Зменшыць</string>
<string name="keep_screen_on">Трымаць экран ўключаным</string>
<string name="keep_screen_on">Трымаць экран уключаным</string>
<string name="keep_screen_on_summary">Ня выключаць экран падчас чытання мангі</string>
<string name="state_abandoned">Кінута</string>
<string name="categories">Катэгорыі</string>
@@ -480,7 +480,7 @@
<string name="frequency_once_per_week">Раз на тыдзень</string>
<string name="periodic_backups">Перыядычнае рэзервовае капіраванне</string>
<string name="frequency_twice_per_month">Два разы на месяц</string>
<string name="frequency_once_per_month">Адзін раз ў месяц</string>
<string name="frequency_once_per_month">Адзін раз у месяц</string>
<string name="last_successful_backup">Апошняе паспяховае рэзервовае капіраванне: %s</string>
<string name="backups_output_directory">Вывадны каталог рэзервовых копій</string>
<string name="speed_value">x%.1f</string>
@@ -496,7 +496,7 @@
<string name="manual">Ўручную</string>
<string name="source_enabled">Крыніца ўключана</string>
<string name="disable_nsfw_summary">Адключыць крыніцы NSFW і схавайць мангу для дарослых са спісу, калі гэта магчыма</string>
<string name="no_manga_sources_catalog_text">Ў гэтым раздзеле няма даступных крыніц, ці ўсе яны маглі быць ўжо дададзены.
<string name="no_manga_sources_catalog_text">У гэтым раздзеле няма даступных крыніц, ці ўсе яны маглі быць ужо дададзены.
\nСачыце за абнаўленнямі</string>
<string name="available_d">Даступна: %1$d</string>
<string name="content_type_other">Іншае</string>
@@ -517,18 +517,18 @@
<string name="skip">Прапусціць</string>
<string name="color_correction_apply_text">Гэтыя налады могуць прымяняцца глабальна або толькі да бягучай мангі. Пры глабальным прымяненні індывідуальныя налады не будуць перавызначаны.</string>
<string name="grayscale">Адценні шэрага</string>
<string name="disable_battery_optimization_summary_downloads">Можа дапамагчы з пачаткам загрузкі, калі у вас ўзнікаюць з ёй праблемы</string>
<string name="disable_battery_optimization_summary_downloads">Можа дапамагчы з пачаткам загрузкі, калі у вас узнікаюць з ёй праблемы</string>
<string name="welcome_text">Выберыце, якія крыніцы змесціва вы хочаце ўключыць. Гэта таксама можна наладзіць пазней ў наладах</string>
<string name="restore">Аднавіць</string>
<string name="backup_date_">Дата стварэння рэзервовай копіі: %s</string>
<string name="sync_auth">Ўвайдзіце, каб сінхранізаваць ўліковы запіс</string>
<string name="sync_auth">Увайсці ў акаўнт сінхранізацыі</string>
<string name="by_name_reverse">Імя (зваротнае)</string>
<string name="content_rating">Рэйтынг кантэнту</string>
<string name="genres_exclude">Выключыць жанры</string>
<string name="rating_safe">Бяспечны</string>
<string name="rating_suggestive">З падказкамі</string>
<string name="rating_adult">Дарослы</string>
<string name="default_tab">Ўкладка па змаўчанні</string>
<string name="default_tab">Укладка па змаўчанні</string>
<string name="state_upcoming">Чакаецца</string>
<string name="volume_">Том %d</string>
<string name="volume_unknown">Невядомы том</string>
@@ -554,7 +554,7 @@
\nУвага: бягучы ход чытання будзе страчаны.</string>
<string name="reader_actions_summary">Налада дзеянняў для сэнсарных абласцей экрана</string>
<string name="remaining_time_pattern">%1$s %2$s</string>
<string name="email_password_enter_hint">Каб працягнуць, ўвядзіце адрас электроннай пошты і пароль</string>
<string name="email_password_enter_hint">Каб працягнуць, увядзіце адрас электроннай пошты і пароль</string>
<string name="config_reset_confirm">Скінуць налады да значэнняў па змаўчанні? Гэта дзеянне нельга адмяніць.</string>
<string name="use_two_pages_landscape">Выкарыстоўвайце двухстаронкавы макет у альбомнай арыентацыі (бэта)</string>
<string name="default_webtoon_zoom_out">Аддаленне ў рэжыме манхвы</string>
@@ -589,7 +589,7 @@
<string name="single_cbz_file">Адзін файл CBZ</string>
<string name="multiple_cbz_files">Некалькі файлаў CBZ</string>
<string name="alternatives">Альтэрнатывы</string>
<string name="migrate_confirmation">Манга «%1$s» з «%2$s» будзе заменена на «%3$s» з «%4$s» ў вашай гісторыі і ў абраных (калі ёсць)</string>
<string name="migrate_confirmation">Манга «%1$s» з «%2$s» будзе заменена на «%3$s» з «%4$s» у вашай гісторыі і ў абраных (калі ёсць)</string>
<string name="migration_completed">Перанос завершаны</string>
<string name="manga_migration">Перанос мангі</string>
<string name="migrate">Перанесці</string>
@@ -609,7 +609,7 @@
<string name="enable_source">Ўключыць крыніцу</string>
<string name="unsupported_source">Гэтая крыніца мангі не падтрымліваецца</string>
<string name="show_pages_thumbs">Паказаць мініяцюры старонак</string>
<string name="show_pages_thumbs_summary">Ўключыце ўкладку «Старонкі» на экране звестак</string>
<string name="show_pages_thumbs_summary">Уключыце ўкладку «Старонкі» на экране звестак</string>
<string name="error_no_data_received">Ніякія дадзеныя не былі атрыманы з сервера</string>
<string name="unsupported_backup_message">Абярыце правільны файл рэзервовай копіі Kotatsu</string>
<string name="last_used">Апошні раз выкарыстоўваўся</string>
@@ -642,4 +642,6 @@
<string name="disable">Адкл.</string>
<string name="sources_disabled">Крыніцы адключаны</string>
<string name="_new">Новае</string>
<string name="all_languages">Усе мовы</string>
<string name="screenshots_block_incognito">Блакіраваць у рэжыме інкогніта</string>
</resources>

View File

@@ -11,7 +11,7 @@
<string name="domain">ডোমেইন</string>
<string name="app_update_available">অ্যাপের নতুন ভার্সন পাওয়া গেছে</string>
<string name="open_in_browser">ব্রাউজারে খুলুন</string>
<string name="error_occurred">কিছু একটা গন্ডগোল হয়েছে</string>
<string name="error_occurred">কিছু একটা সমস্যা হয়েছে</string>
<string name="details">খুঁটিনাটি</string>
<string name="chapters">পর্ব সমূহ</string>
<string name="list">তালিকা</string>
@@ -102,7 +102,7 @@
<string name="manga_save_location">ডাউনলোডের জন্য ফোল্ডার</string>
<string name="suggestions_notifications_summary">কখনও কখনও প্রস্তাবিত মাঙ্গা সহ বিজ্ঞপ্তিগুলি দেখান৷</string>
<string name="updates_feed_cleared">সাফ করা হয়েছে</string>
<string name="list_mode">তালিকা মোড</string>
<string name="list_mode">তালিকার ধরন</string>
<string name="download_complete">ডাউনলোড করা হয়েছে</string>
<string name="update">হালনাগাদ</string>
<string name="feed_will_update_soon">ফিড আপডেট শীঘ্রই শুরু হবে</string>
@@ -151,4 +151,4 @@
<string name="suggestion_manga">পরামর্শ: %s</string>
<string name="text_empty_holder_primary">এখানে খালি…</string>
<string name="done">সম্পন্ন</string>
</resources>
</resources>

View File

@@ -644,4 +644,6 @@
<string name="_new">Nuevos</string>
<string name="all_languages">Todos los idiomas</string>
<string name="screenshots_block_incognito">Bloquear en modo incógnito</string>
<string name="image_server">Servidor de imágenes preferido</string>
<string name="crop_pages">Páginas de recortes</string>
</resources>

View File

@@ -644,4 +644,6 @@
<string name="_new">नया</string>
<string name="all_languages">सभी भाषाएं</string>
<string name="screenshots_block_incognito">गुप्त मोड में ब्लॉक करें</string>
<string name="image_server">पसंदीदा छवि सर्वर</string>
<string name="crop_pages">पृष्ठ काटें</string>
</resources>

View File

@@ -641,4 +641,6 @@
<string name="blocked_by_server_message">Anda diblokir oleh server. Coba gunakan koneksi jaringan yang berbeda (VPN, Proxy, dll.)</string>
<string name="less_frequently">Lebih jarang</string>
<string name="pin_navigation_ui_summary">Jangan sembunyikan bilah navigasi dan tampilan pencarian saat menggulir</string>
<string name="chapters_deleted_pattern">Dihapus, dibersihkan</string>
<string name="suggested_queries">Pertanyaan yang disarankan</string>
</resources>

View File

@@ -593,4 +593,20 @@
<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>
<string name="_new">Nuovi</string>
<string name="automatic">Automatico</string>
<string name="all_languages">Tutte le lingue</string>
<string name="delete_read_chapters">Cancella capitoli letti</string>
<string name="manga_migration">Migrazione manga</string>
<string name="migration_completed">Migrazione completata</string>
<string name="no_chapters_deleted">Nessun capitolo è stato cancellato</string>
<string name="day">Giorno</string>
<string name="three_months">Tre mesi</string>
<string name="empty_stats_text">Non ci sono statistiche per il periodo selezionato</string>
<string name="ask_for_dest_dir_every_time">Chiedi per la cartella di destinazione ogni volta</string>
<string name="pages_saving">Salva pagine</string>
<string name="preferred_download_format">Formato download preferito</string>
<string name="single_cbz_file">File CBZ singolo</string>
<string name="multiple_cbz_files">Molti file CBZ</string>
<string name="reading_stats">Lettura statistiche</string>
</resources>

View File

@@ -17,8 +17,8 @@
<string name="chapters">Тарау</string>
<string name="detailed_list">Егжей-тегжейлі тізім</string>
<string name="list_mode">Тізім түрі</string>
<string name="remote_sources">Маңга қайнары</string>
<string name="loading_">Жүктеу</string>
<string name="remote_sources">Маңга дереккөзі</string>
<string name="loading_">Жүктеліп жатыр</string>
<string name="computing_">Есептеу…</string>
<string name="favourites">Таңдаулы</string>
<string name="network_error">Желі қатесі</string>
@@ -34,7 +34,7 @@
<string name="create_shortcut">Таңбаша жасау…</string>
<string name="share_s">%s бөлісу</string>
<string name="search">Іздеу</string>
<string name="manga_downloading_">Жүктеу</string>
<string name="manga_downloading_">Жүктеліп жатыр</string>
<string name="processing_">Үдерісте…</string>
<string name="download_complete">Жүктелді</string>
<string name="by_name">Атауы</string>
@@ -107,7 +107,7 @@
<string name="history_is_empty">Әзірге тарих жоқ</string>
<string name="search_manga">Маңга іздеу</string>
<string name="add_to_favourites">Таңдаулыға</string>
<string name="downloads">Жүктелгендер</string>
<string name="downloads">Жүктелген</string>
<string name="operation_not_supported">Қолжетімсіз амал</string>
<string name="delete">Жою</string>
<string name="text_file_not_supported">CBZ не ZIP пішімде таңдаңыз.</string>
@@ -272,7 +272,7 @@
<string name="never">Ешқашан</string>
<string name="disable_battery_optimization">Қуат оңтайлығын өшіру</string>
<string name="status_planned">Жоспарланған</string>
<string name="bookmarks">Бетбелгілер</string>
<string name="bookmarks">Бетбелгі</string>
<string name="show_all">Бәрін көрсету</string>
<string name="empty_favourite_categories">Таңдаулы санатыңыз жоқ</string>
<string name="invalid_domain_message">Қате дәмейін</string>
@@ -303,7 +303,7 @@
<string name="password">Құпиясөз</string>
<string name="download_option_whole_manga">Маңганы толықтай</string>
<string name="settings_apply_restart_required">Өзгерту іске қосылуы үшін қолданбаны өшіріп қосыңыз</string>
<string name="source_disabled">Дереккөз сөніп жатыр</string>
<string name="source_disabled">Дереккөз өшіп жатыр</string>
<string name="backup_frequency">Сақтық көшірмесінің жиілігі</string>
<string name="data_and_privacy">Дерек пен құпиялық</string>
<string name="clear_cookies_summary">Қате болса көмектесе алады. Түгел тіркелгінің күші жойылады</string>
@@ -381,7 +381,7 @@
<string name="downloads_paused">Жүктеп алу тоқтап қалды</string>
<string name="too_many_requests_message">Тым көп сұрату. Біраздан соң қайталап көріңіз</string>
<string name="downloads_wifi_only">Wi-Fi арқылы ғана жүктеу</string>
<string name="cancel_all_downloads_confirm">Белсенді жүктеудің бәрі жойылып, жартылай жүктелгендер жоғалып кетеді</string>
<string name="cancel_all_downloads_confirm">Жүктеліп жатқанның бәрі жойылып, жартылай жүктелгендер жоғалып кетеді</string>
<string name="by_relevance">Өзектілігі</string>
<string name="related_manga">Ұқсас маңга</string>
<string name="discard">Сақтамау</string>
@@ -471,7 +471,7 @@
<string name="exit_confirmation_summary">Шығып кету үшін «Кері» батырмасын екі рет басыңыз</string>
<string name="bookmarks_removed">Бетбелгілер жойылды</string>
<string name="theme_name_mion">Мион</string>
<string name="mirror_switching_summary">Дереккөз дәмейіннің қатесі пайда болып, айнасы қолжетімді болса, өздігінен соған ауыстыру</string>
<string name="mirror_switching_summary">Дереккөз дәмейнінің қатесі пайда болып, айнасы қолжетімді болса, өздігінен соған ауыстыру</string>
<string name="no_bookmarks_yet">Бетбелгі жоқ</string>
<string name="no_thanks">Жоқ, рақмет</string>
<string name="suggest_new_sources_summary">Қолданбаның жаңа нұсқасында пайда болған дереккөзді ұсыну</string>
@@ -514,4 +514,28 @@
<string name="this_manga">Бұл маңга</string>
<string name="error_filter_locale_genre_not_supported">Бұл дереккөзде жанр бойынша және локал файлдар бойынша сүзуге болмайды</string>
<string name="error_multiple_genres_not_supported">Бұл дереккөзде бірнеше жанр бойынша сүзуге болмайды</string>
<string name="genres_exclude">Жанр алып тастау</string>
<string name="rating_safe">Қауіпсіз</string>
<string name="chapters_grid_view">Кесте түрі</string>
<string name="error_filter_states_genre_not_supported">Бұл дереккөзде жанр мен күні бойынша сүзу жоқ</string>
<string name="genres_search_hint">Жанр атауын тере бастаңыз</string>
<string name="disable_battery_optimization_summary_downloads">Жүктеу дұрыс басталмай жүрсе көмектесе алады</string>
<string name="restore">Қалпына келтіру</string>
<string name="backup_date_">Сақтық көшірме күні: %s</string>
<string name="state_upcoming">Жақында</string>
<string name="by_name_reverse">Теріс ат</string>
<string name="content_rating">Контент рейтиңі</string>
<string name="prev_chapter">Алдыңғы тарау</string>
<string name="next_chapter">Келесі тарау</string>
<string name="prev_page">Алдыңғы бет</string>
<string name="switch_pages_volume_buttons">Дыбыс батырмасын қосу</string>
<string name="switch_pages_volume_buttons_summary">Дыбыс батырмасы арқылы парақтау</string>
<string name="tap_action">Басқандағы әрекет</string>
<string name="long_tap_action">Басып тұрғандағы әрекет</string>
<string name="none">Түк</string>
<string name="welcome_text">Қандай дереккөз қосқыңыз келетінін таңдаңыз. Кейін баптап алуға болады</string>
<string name="next_page">Келесі бет</string>
<string name="reader_actions">Оқымадағы әрекет</string>
<string name="sync_auth">Синхрондау тіркелгісіне кіру</string>
<string name="config_reset_confirm">Әдепкі баптауға қайтайық па? Әрекетті қайтаруға болмайды.</string>
</resources>

View File

@@ -3,4 +3,6 @@
<string name="history">ചരിത്രം</string>
<string name="chapters">അദ്ധ്യായങ്ങൾ</string>
<string name="settings">ക്രമീകരണങ്ങൾ</string>
<string name="favourites">പ്രിയപ്പെട്ടവ</string>
<string name="details">വിശദാംശങ്ങൾ</string>
</resources>

View File

@@ -642,4 +642,15 @@
<string name="disable">Откл.</string>
<string name="sources_disabled">Источники отключены</string>
<string name="_new">Новое</string>
<string name="all_languages">Все языки</string>
<string name="screenshots_block_incognito">Блокировать в режиме инкогнито</string>
<string name="source_pinned">Источник закреплён</string>
<string name="sources_unpinned">Источники откреплены</string>
<string name="sources_pinned">Источники закреплены</string>
<string name="recent_sources">Недавние источники</string>
<string name="crop_pages">Обрезать страницы</string>
<string name="pin">Закрепить</string>
<string name="unpin">Открепить</string>
<string name="source_unpinned">Источник откреплён</string>
<string name="image_server">Сервер изображений</string>
</resources>

View File

@@ -642,4 +642,15 @@
<string name="disable">Вимкнути</string>
<string name="sources_disabled">Джерела вимкнено</string>
<string name="_new">Нове</string>
<string name="all_languages">Всі мови</string>
<string name="screenshots_block_incognito">Блокувати в режимі інкогніто</string>
<string name="image_server">Сервер зображень</string>
<string name="unpin">Відкріпити</string>
<string name="source_pinned">Джерело закріплено</string>
<string name="sources_pinned">Джерела закріплені</string>
<string name="recent_sources">Нещодавні джерела</string>
<string name="crop_pages">Обрізати сторінки</string>
<string name="sources_unpinned">Джерела відкріплені</string>
<string name="pin">Закріпити</string>
<string name="source_unpinned">Джерело відкріплено</string>
</resources>

View File

@@ -97,4 +97,8 @@
<item>@string/system_default</item>
<item>@string/more_frequently</item>
</string-array>
<string-array name="reader_crop" translatable="false">
<item>@string/pages</item>
<item>@string/webtoon</item>
</string-array>
</resources>

View File

@@ -68,4 +68,8 @@
<item>1</item>
<item>2</item>
</string-array>
<string-array name="values_reader_crop" translatable="false">
<item>1</item>
<item>2</item>
</string-array>
</resources>

View File

@@ -8,6 +8,7 @@
<integer name="manga_badge_max_character_count">3</integer>
<integer name="explore_buttons_columns">2</integer>
<integer name="details_description_lines">4</integer>
<integer name="details_title_lines">4</integer>
<integer name="splash_screen_duration">450</integer>
</resources>

View File

@@ -654,4 +654,14 @@
<string name="_new">New</string>
<string name="all_languages">All languages</string>
<string name="screenshots_block_incognito">Block when incognito mode</string>
<string name="image_server">Preferred image server</string>
<string name="inline_preference_pattern" translatable="false">%1$s: %2$s</string>
<string name="crop_pages">Crop pages</string>
<string name="pin">Pin</string>
<string name="unpin">Unpin</string>
<string name="source_pinned">Source pinned</string>
<string name="source_unpinned">Source unpinned</string>
<string name="sources_unpinned">Sources unpinned</string>
<string name="sources_pinned">Sources pinned</string>
<string name="recent_sources">Recent sources</string>
</resources>

View File

@@ -88,6 +88,12 @@
android:summary="@string/reader_optimize_summary"
android:title="@string/reader_optimize" />
<MultiSelectListPreference
android:entries="@array/reader_crop"
android:entryValues="@array/values_reader_crop"
android:key="reader_crop"
android:title="@string/crop_pages" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="reader_fullscreen"

View File

@@ -23,14 +23,14 @@
android:order="101"
android:persistent="false"
android:summary="@string/clear_source_cookies_summary"
android:title="@string/clear_cookies" />
android:title="@string/clear_cookies"
app:allowDividerAbove="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="slowdown"
android:order="105"
android:summary="@string/download_slowdown_summary"
android:title="@string/download_slowdown"
app:allowDividerAbove="true" />
android:title="@string/download_slowdown" />
</PreferenceScreen>

View File

@@ -4,7 +4,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.4.1'
classpath 'com.android.tools.build:gradle:8.5.0'
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'