Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8fa0e33f1 | ||
|
|
97bc638f5f | ||
|
|
064c0ae425 | ||
|
|
ae7aa52177 | ||
|
|
6edda72d61 | ||
|
|
2f58f32bdd | ||
|
|
0b821db046 | ||
|
|
36472998ee | ||
|
|
c2e7325876 | ||
|
|
28a4a3849c | ||
|
|
6e9c934912 | ||
|
|
675ef0e629 | ||
|
|
484914b2dc | ||
|
|
ee85ef50f4 | ||
|
|
dcee5542c5 | ||
|
|
9b3ce4d849 | ||
|
|
5ab7e586f3 | ||
|
|
9f5d4ed52c | ||
|
|
c3ca734005 | ||
|
|
a158a488f2 | ||
|
|
6048cb917e | ||
|
|
81aac0d431 | ||
|
|
dfb50fbddc | ||
|
|
1f03e0a84b | ||
|
|
77e393ae48 |
@@ -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'
|
||||
|
||||
|
||||
@@ -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?> {
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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? ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,6 +13,7 @@ sealed interface SourceConfigItem : ListModel {
|
||||
val isEnabled: Boolean,
|
||||
val isDraggable: Boolean,
|
||||
val isAvailable: Boolean,
|
||||
val isPinned: Boolean,
|
||||
) : SourceConfigItem {
|
||||
|
||||
val isNsfw: Boolean
|
||||
|
||||
12
app/src/main/res/drawable/ic_images.xml
Normal file
12
app/src/main/res/drawable/ic_images.xml
Normal 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>
|
||||
12
app/src/main/res/drawable/ic_pin_small.xml
Normal file
12
app/src/main/res/drawable/ic_pin_small.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
12
app/src/main/res/drawable/ic_shortcut.xml
Normal file
12
app/src/main/res/drawable/ic_shortcut.xml
Normal 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>
|
||||
12
app/src/main/res/drawable/ic_unpin.xml
Normal file
12
app/src/main/res/drawable/ic_unpin.xml
Normal 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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
2
app/src/main/res/values-ab/plurals.xml
Normal file
2
app/src/main/res/values-ab/plurals.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user