Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55851fb22f | ||
|
|
7801456d17 | ||
|
|
38a1fafa26 | ||
|
|
aa02233883 | ||
|
|
5405fdb85a | ||
|
|
38ad7e1fd4 | ||
|
|
06372083fd | ||
|
|
d5d3154074 | ||
|
|
1a279966d9 | ||
|
|
3222c2128e | ||
|
|
872c859efe | ||
|
|
b79c00f8df | ||
|
|
e7d3d9811d | ||
|
|
4fdfc75833 | ||
|
|
9754ebf1bb | ||
|
|
fee35cceab | ||
|
|
b928c4123c | ||
|
|
b093a885c9 | ||
|
|
dd898579c9 | ||
|
|
73143d2f94 | ||
|
|
563752f6a4 | ||
|
|
7135902100 | ||
|
|
969947ef71 | ||
|
|
806e4eade6 | ||
|
|
063cfbe6b9 | ||
|
|
7cb94a3baa | ||
|
|
894c584c78 | ||
|
|
2f65e7776a | ||
|
|
76c56c9119 | ||
|
|
e0a803399c | ||
|
|
7803f42486 | ||
|
|
39713b3cf6 | ||
|
|
8ebf5cea62 | ||
|
|
663dabe218 | ||
|
|
3a5d0120bf | ||
|
|
a773f932d4 | ||
|
|
2a5812735f | ||
|
|
06ec145802 | ||
|
|
6624778f7f | ||
|
|
1af1f071ad | ||
|
|
f87db4e6d3 | ||
|
|
07bd66fb39 |
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 593
|
||||
versionName = '6.2.5'
|
||||
versionCode = 597
|
||||
versionName = '6.3.0'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
||||
ksp {
|
||||
@@ -33,7 +33,6 @@ android {
|
||||
applicationIdSuffix = '.debug'
|
||||
}
|
||||
release {
|
||||
multiDexEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
@@ -48,11 +47,12 @@ android {
|
||||
main.java.srcDirs += 'src/main/kotlin/'
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
freeCompilerArgs += [
|
||||
'-opt-in=kotlin.ExperimentalStdlibApi',
|
||||
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
|
||||
@@ -82,17 +82,18 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:4ca3a492b0') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:41eea1c420') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.20'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.8.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.8.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
|
||||
@@ -119,8 +120,8 @@ dependencies {
|
||||
implementation 'androidx.room:room-ktx:2.6.0'
|
||||
ksp 'androidx.room:room-compiler:2.6.0'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
|
||||
implementation 'com.squareup.okio:okio:3.6.0'
|
||||
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||
@@ -128,11 +129,11 @@ dependencies {
|
||||
|
||||
implementation 'com.google.dagger:hilt-android:2.48.1'
|
||||
kapt 'com.google.dagger:hilt-compiler:2.48.1'
|
||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||
implementation 'androidx.hilt:hilt-work:1.1.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.1.0'
|
||||
|
||||
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||
implementation 'io.coil-kt:coil-base:2.5.0'
|
||||
implementation 'io.coil-kt:coil-svg:2.5.0'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:cf089a264d'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
@@ -143,7 +144,7 @@ dependencies {
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20230618'
|
||||
testImplementation 'org.json:json:20231013'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
|
||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -19,3 +19,5 @@
|
||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
||||
-keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; }
|
||||
-keep class org.jsoup.parser.Tag
|
||||
-keep class org.jsoup.internal.StringUtil
|
||||
|
||||
@@ -221,6 +221,9 @@
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity"
|
||||
android:label="@string/sources_catalog" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
|
||||
@@ -48,8 +48,8 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
if (!catchingWebViewUnavailability {
|
||||
setContentView(
|
||||
ActivityBrowserBinding.inflate(
|
||||
layoutInflater
|
||||
)
|
||||
layoutInflater,
|
||||
),
|
||||
)
|
||||
}) {
|
||||
return
|
||||
@@ -82,9 +82,11 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
viewBinding.webView.run {
|
||||
stopLoading()
|
||||
destroy()
|
||||
runCatching {
|
||||
viewBinding.webView
|
||||
}.onSuccess {
|
||||
it.stopLoading()
|
||||
it.destroy()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -4,10 +4,15 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.RawQuery
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
|
||||
@Dao
|
||||
abstract class MangaSourcesDao {
|
||||
@@ -15,11 +20,11 @@ abstract class MangaSourcesDao {
|
||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||
abstract suspend fun findAll(): List<MangaSourceEntity>
|
||||
|
||||
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
|
||||
abstract suspend fun findAllEnabled(): List<MangaSourceEntity>
|
||||
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
|
||||
abstract suspend fun findAllDisabled(): List<MangaSourceEntity>
|
||||
|
||||
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
|
||||
abstract fun observeEnabled(): Flow<List<MangaSourceEntity>>
|
||||
@Query("SELECT * FROM sources WHERE enabled = 0")
|
||||
abstract fun observeDisabled(): Flow<List<MangaSourceEntity>>
|
||||
|
||||
@Query("SELECT * FROM sources ORDER BY sort_key")
|
||||
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
|
||||
@@ -40,6 +45,22 @@ abstract class MangaSourcesDao {
|
||||
@Upsert
|
||||
abstract suspend fun upsert(entry: 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")
|
||||
return observeImpl(query)
|
||||
}
|
||||
|
||||
suspend fun findAllEnabled(order: SourcesSortOrder): List<MangaSourceEntity> {
|
||||
val orderBy = getOrderBy(order)
|
||||
|
||||
@Language("RoomSql")
|
||||
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY $orderBy")
|
||||
return findAllImpl(query)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun setEnabled(source: String, isEnabled: Boolean) {
|
||||
if (updateIsEnabled(source, isEnabled) == 0) {
|
||||
@@ -54,4 +75,16 @@ abstract class MangaSourcesDao {
|
||||
|
||||
@Query("UPDATE sources SET enabled = :isEnabled WHERE source = :source")
|
||||
protected abstract suspend fun updateIsEnabled(source: String, isEnabled: Boolean): Int
|
||||
|
||||
@RawQuery(observedEntities = [MangaSourceEntity::class])
|
||||
protected abstract fun observeImpl(query: SupportSQLiteQuery): Flow<List<MangaSourceEntity>>
|
||||
|
||||
@RawQuery
|
||||
protected abstract suspend fun findAllImpl(query: SupportSQLiteQuery): List<MangaSourceEntity>
|
||||
|
||||
private fun getOrderBy(order: SourcesSortOrder) = when (order) {
|
||||
SourcesSortOrder.ALPHABETIC -> "source ASC"
|
||||
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"
|
||||
SourcesSortOrder.MANUAL -> "sort_key ASC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
@@ -18,3 +21,18 @@ fun MangaSource(name: String): MangaSource {
|
||||
}
|
||||
|
||||
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
||||
|
||||
@get:StringRes
|
||||
val ContentType.titleResId
|
||||
get() = when (this) {
|
||||
ContentType.MANGA -> R.string.content_type_manga
|
||||
ContentType.HENTAI -> R.string.content_type_hentai
|
||||
ContentType.COMICS -> R.string.content_type_comics
|
||||
ContentType.OTHER -> R.string.content_type_other
|
||||
}
|
||||
|
||||
fun MangaSource.getSummary(context: Context): String {
|
||||
val type = context.getString(contentType.titleResId)
|
||||
val locale = getLocaleTitle() ?: context.getString(R.string.various_languages)
|
||||
return context.getString(R.string.source_summary_pattern, type, locale)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ class CloudFlareInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
|
||||
val content = response.body?.source()?.peek()?.use {
|
||||
Jsoup.parse(it.inputStream(), Charsets.UTF_8.name(), response.request.url.toString())
|
||||
val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use {
|
||||
Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString())
|
||||
} ?: return response
|
||||
if (content.getElementById("challenge-error-title") != null) {
|
||||
val request = response.request
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
@@ -77,10 +78,18 @@ class MangaDataRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun storeManga(manga: Manga) {
|
||||
val tags = manga.tags.toEntities()
|
||||
db.withTransaction {
|
||||
db.getTagsDao().upsert(tags)
|
||||
db.getMangaDao().upsert(manga.toEntity(), tags)
|
||||
// avoid storing local manga if remote one is already stored
|
||||
val existing = if (manga.isLocal) {
|
||||
db.getMangaDao().find(manga.id)?.manga
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (existing == null || existing.source == manga.source.name) {
|
||||
val tags = manga.tags.toEntities()
|
||||
db.getTagsDao().upsert(tags)
|
||||
db.getMangaDao().upsert(manga.toEntity(), tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.putEnumValue
|
||||
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
|
||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.find
|
||||
@@ -209,6 +210,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
return policy.isNetworkAllowed(connectivityManager)
|
||||
}
|
||||
|
||||
var sourcesSortOrder: SourcesSortOrder
|
||||
get() = prefs.getEnumValue(KEY_SOURCES_ORDER, SourcesSortOrder.MANUAL)
|
||||
set(value) = prefs.edit { putEnumValue(KEY_SOURCES_ORDER, value) }
|
||||
|
||||
var isSourcesGridMode: Boolean
|
||||
get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
|
||||
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
|
||||
@@ -528,6 +533,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_RELATED_MANGA = "related_manga"
|
||||
const val KEY_NAV_MAIN = "nav_main"
|
||||
const val KEY_32BIT_COLOR = "enhanced_colors"
|
||||
const val KEY_SOURCES_ORDER = "sources_sort_order"
|
||||
const val KEY_SOURCES_CATALOG = "sources_catalog"
|
||||
|
||||
// About
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
|
||||
@@ -4,11 +4,11 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.htmlEncode
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -37,7 +37,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
|
||||
override fun onViewBindingCreated(binding: DialogErrorDetailsBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
with(binding.textViewMessage) {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
text = context.getString(
|
||||
R.string.manga_error_description_pattern,
|
||||
exception.message?.htmlEncode().orEmpty(),
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.ArrayMap
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.slider.Slider
|
||||
import kotlin.math.cbrt
|
||||
import kotlin.math.pow
|
||||
|
||||
class CubicSlider @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : Slider(context, attrs) {
|
||||
|
||||
private val changeListeners = ArrayMap<OnChangeListener, OnChangeListenerMapper>(1)
|
||||
|
||||
override fun setValue(value: Float) {
|
||||
super.setValue(value.unmap())
|
||||
}
|
||||
|
||||
override fun getValue(): Float {
|
||||
return super.getValue().map()
|
||||
}
|
||||
|
||||
override fun getValueFrom(): Float {
|
||||
return super.getValueFrom().map()
|
||||
}
|
||||
|
||||
override fun setValueFrom(valueFrom: Float) {
|
||||
super.setValueFrom(valueFrom.unmap())
|
||||
}
|
||||
|
||||
override fun getValueTo(): Float {
|
||||
return super.getValueTo().map()
|
||||
}
|
||||
|
||||
override fun setValueTo(valueTo: Float) {
|
||||
super.setValueTo(valueTo.unmap())
|
||||
}
|
||||
|
||||
override fun addOnChangeListener(listener: OnChangeListener) {
|
||||
val mapper = OnChangeListenerMapper(listener)
|
||||
super.addOnChangeListener(mapper)
|
||||
changeListeners[listener] = mapper
|
||||
}
|
||||
|
||||
override fun removeOnChangeListener(listener: OnChangeListener) {
|
||||
changeListeners.remove(listener)?.let {
|
||||
super.removeOnChangeListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearOnChangeListeners() {
|
||||
super.clearOnChangeListeners()
|
||||
changeListeners.clear()
|
||||
}
|
||||
|
||||
private fun Float.map(): Float {
|
||||
return this.pow(3)
|
||||
}
|
||||
|
||||
private fun Float.unmap(): Float {
|
||||
return cbrt(this)
|
||||
}
|
||||
|
||||
private inner class OnChangeListenerMapper(
|
||||
private val delegate: OnChangeListener,
|
||||
) : OnChangeListener {
|
||||
|
||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
delegate.onValueChange(slider, value.map(), fromUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class CompositeMutex<T : Any> : Set<T> {
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return state.isEmpty
|
||||
return state.isEmpty()
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
|
||||
@@ -19,7 +19,7 @@ class CompositeMutex2<T : Any> : Set<T> {
|
||||
}
|
||||
|
||||
override fun isEmpty(): Boolean {
|
||||
return delegates.isEmpty
|
||||
return delegates.isEmpty()
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.map
|
||||
import java.util.Locale
|
||||
|
||||
class LocaleComparator : Comparator<Locale?> {
|
||||
|
||||
private val deviceLocales = LocaleListCompat.getAdjustedDefault()//LocaleManagerCompat.getSystemLocales(context)
|
||||
.map { it.language }
|
||||
.distinct()
|
||||
|
||||
override fun compare(a: Locale?, b: Locale?): Int {
|
||||
return if (a === b) {
|
||||
0
|
||||
} else {
|
||||
val indexA = if (a == null) -1 else deviceLocales.indexOf(a.language)
|
||||
val indexB = if (b == null) -1 else deviceLocales.indexOf(b.language)
|
||||
if (indexA < 0 && indexB < 0) {
|
||||
compareValues(a?.language, b?.language)
|
||||
} else {
|
||||
-2 - (indexA - indexB)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,16 @@ import android.content.res.Configuration
|
||||
import android.database.ContentObserver
|
||||
import android.os.Handler
|
||||
import android.provider.Settings
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScreenOrientationHelper(private val activity: Activity) {
|
||||
@ActivityScoped
|
||||
class ScreenOrientationHelper @Inject constructor(private val activity: Activity) {
|
||||
|
||||
val isAutoRotationEnabled: Boolean
|
||||
get() = Settings.System.getInt(
|
||||
@@ -31,9 +34,15 @@ class ScreenOrientationHelper(private val activity: Activity) {
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleOrientation() {
|
||||
isLandscape = !isLandscape
|
||||
}
|
||||
var isLocked: Boolean
|
||||
get() = activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
set(value) {
|
||||
activity.requestedOrientation = if (value) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
fun observeAutoOrientation() = callbackFlow {
|
||||
val observer = object : ContentObserver(Handler(activity.mainLooper)) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.IOException
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.HttpStatusException
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
private val TYPE_JSON = "application/json".toMediaType()
|
||||
@@ -34,9 +35,8 @@ val HttpUrl.isHttpOrHttps: Boolean
|
||||
|
||||
fun Response.ensureSuccess() = apply {
|
||||
if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) {
|
||||
val message = "Invalid response: $code $message at ${request.url}"
|
||||
closeQuietly()
|
||||
throw IllegalStateException(message)
|
||||
throw HttpStatusException(message, code, request.url.toString())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.core.view.children
|
||||
import androidx.core.view.descendants
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||
import com.google.android.material.slider.Slider
|
||||
@@ -68,6 +69,10 @@ inline fun ViewPager2.doOnPageChanged(crossinline callback: (Int) -> Unit) {
|
||||
val ViewPager2.recyclerView: RecyclerView?
|
||||
get() = children.firstNotNullOfOrNull { it as? RecyclerView }
|
||||
|
||||
fun ViewPager2.findCurrentViewHolder(): ViewHolder? {
|
||||
return recyclerView?.findViewHolderForAdapterPosition(currentItem)
|
||||
}
|
||||
|
||||
fun View.resetTransformations() {
|
||||
alpha = 1f
|
||||
translationX = 0f
|
||||
|
||||
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
@@ -32,6 +33,7 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val recoverUseCase: RecoverMangaUseCase,
|
||||
private val imageGetter: Html.ImageGetter,
|
||||
private val networkState: NetworkState,
|
||||
) {
|
||||
|
||||
operator fun invoke(intent: MangaIntent): Flow<MangaDetails> = channelFlow {
|
||||
@@ -46,6 +48,13 @@ class DetailsLoadUseCase @Inject constructor(
|
||||
null
|
||||
}
|
||||
send(MangaDetails(manga, null, null, false))
|
||||
if (!networkState.value) {
|
||||
// try load offline instead
|
||||
local?.await()?.manga?.let { localManga ->
|
||||
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
|
||||
return@channelFlow
|
||||
}
|
||||
}
|
||||
val details = getDetails(manga)
|
||||
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
|
||||
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.transition.TransitionManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -13,6 +12,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.color
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
@@ -107,7 +107,7 @@ class DetailsFragment :
|
||||
binding.infoLayout.textViewSource.setOnClickListener(this)
|
||||
binding.textViewDescription.addOnLayoutChangeListener(this)
|
||||
binding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
binding.recyclerViewRelated.addItemDecoration(
|
||||
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteSheet
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
@@ -63,7 +63,7 @@ class DetailsMenuProvider(
|
||||
|
||||
R.id.action_favourite -> {
|
||||
viewModel.manga.value?.let {
|
||||
FavouriteSheet.show(activity.supportFragmentManager, it)
|
||||
FavoriteSheet.show(activity.supportFragmentManager, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui.scrobbling
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
@@ -12,6 +11,7 @@ import android.widget.RatingBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import coil.ImageLoader
|
||||
@@ -71,7 +71,7 @@ class ScrobblingInfoSheet :
|
||||
binding.ratingBar.onRatingBarChangeListener = this
|
||||
binding.buttonMenu.setOnClickListener(this)
|
||||
binding.imageViewCover.setOnClickListener(this)
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
|
||||
menu = PopupMenu(binding.root.context, binding.buttonMenu).apply {
|
||||
inflate(R.menu.opt_scrobbling)
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.Collections
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
@@ -43,15 +44,44 @@ class MangaSourcesRepository @Inject constructor(
|
||||
get() = Collections.unmodifiableSet(remoteSources)
|
||||
|
||||
suspend fun getEnabledSources(): List<MangaSource> {
|
||||
return dao.findAllEnabled().toSources(settings.isNsfwContentDisabled)
|
||||
val order = settings.sourcesSortOrder
|
||||
return dao.findAllEnabled(order).toSources(settings.isNsfwContentDisabled, order)
|
||||
}
|
||||
|
||||
fun observeEnabledSources(): Flow<List<MangaSource>> = observeIsNsfwDisabled().flatMapLatest { skipNsfw ->
|
||||
dao.observeEnabled().map {
|
||||
it.toSources(skipNsfw)
|
||||
}
|
||||
suspend fun getDisabledSources(): List<MangaSource> {
|
||||
return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled, null)
|
||||
}
|
||||
|
||||
fun observeEnabledSourcesCount(): Flow<Int> {
|
||||
return combine(
|
||||
observeIsNsfwDisabled(),
|
||||
dao.observeEnabled(SourcesSortOrder.MANUAL),
|
||||
) { skipNsfw, sources ->
|
||||
sources.count { skipNsfw || !MangaSource(it.source).isNsfw() }
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeAvailableSourcesCount(): Flow<Int> {
|
||||
return combine(
|
||||
observeIsNsfwDisabled(),
|
||||
dao.observeEnabled(SourcesSortOrder.MANUAL),
|
||||
) { skipNsfw, enabledSources ->
|
||||
val enabled = enabledSources.mapToSet { it.source }
|
||||
allMangaSources.count { x ->
|
||||
x.name !in enabled && (!skipNsfw || !x.isNsfw())
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeEnabledSources(): Flow<List<MangaSource>> = combine(
|
||||
observeIsNsfwDisabled(),
|
||||
observeSortOrder(),
|
||||
) { skipNsfw, order ->
|
||||
dao.observeEnabled(order).map {
|
||||
it.toSources(skipNsfw, order)
|
||||
}
|
||||
}.flatMapLatest { it }
|
||||
|
||||
fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
|
||||
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
|
||||
for (entity in entities) {
|
||||
@@ -146,7 +176,10 @@ class MangaSourcesRepository @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
private fun List<MangaSourceEntity>.toSources(skipNsfwSources: Boolean): List<MangaSource> {
|
||||
private fun List<MangaSourceEntity>.toSources(
|
||||
skipNsfwSources: Boolean,
|
||||
sortOrder: SourcesSortOrder?,
|
||||
): List<MangaSource> {
|
||||
val result = ArrayList<MangaSource>(size)
|
||||
for (entity in this) {
|
||||
val source = MangaSource(entity.source)
|
||||
@@ -157,6 +190,9 @@ class MangaSourcesRepository @Inject constructor(
|
||||
result.add(source)
|
||||
}
|
||||
}
|
||||
if (sortOrder == SourcesSortOrder.ALPHABETIC) {
|
||||
result.sortBy { it.title }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -167,4 +203,8 @@ class MangaSourcesRepository @Inject constructor(
|
||||
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) {
|
||||
isNewSourcesTipEnabled
|
||||
}
|
||||
|
||||
private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) {
|
||||
sourcesSortOrder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.explore.data
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
enum class SourcesSortOrder(
|
||||
@StringRes val titleResId: Int,
|
||||
) {
|
||||
ALPHABETIC(R.string.by_name),
|
||||
POPULARITY(R.string.popular),
|
||||
MANUAL(R.string.manual),
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.explore.ui
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
@@ -46,6 +47,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
|
||||
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -83,7 +85,7 @@ class ExploreFragment :
|
||||
SpanSizeResolver(this, resources.getDimensionPixelSize(R.dimen.explore_grid_width)).attach()
|
||||
addItemDecoration(TypedListSpacingDecoration(context, false))
|
||||
}
|
||||
addMenuProvider(ExploreMenuProvider(binding.root.context, viewModel))
|
||||
addMenuProvider(ExploreMenuProvider(binding.root.context))
|
||||
viewModel.content.observe(viewLifecycleOwner) {
|
||||
exploreAdapter?.items = it
|
||||
}
|
||||
@@ -109,7 +111,7 @@ class ExploreFragment :
|
||||
}
|
||||
|
||||
override fun onListHeaderClick(item: ListHeader, view: View) {
|
||||
startActivity(SettingsActivity.newManageSourcesIntent(view.context))
|
||||
startActivity(Intent(view.context, SourcesCatalogActivity::class.java))
|
||||
}
|
||||
|
||||
override fun onPrimaryButtonClick(tipView: TipView) {
|
||||
@@ -174,7 +176,6 @@ class ExploreFragment :
|
||||
} else {
|
||||
LinearLayoutManager(requireContext())
|
||||
}
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun showSuggestionsTip() {
|
||||
|
||||
@@ -6,10 +6,10 @@ import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
|
||||
class ExploreMenuProvider(
|
||||
private val context: Context,
|
||||
private val viewModel: ExploreViewModel,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
@@ -18,17 +18,12 @@ class ExploreMenuProvider(
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_grid -> {
|
||||
viewModel.setGridMode(!menuItem.isChecked)
|
||||
R.id.action_manage -> {
|
||||
context.startActivity(SettingsActivity.newSourcesSettingsIntent(context))
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
super.onPrepareMenu(menu)
|
||||
menu.findItem(R.id.action_grid)?.isChecked = viewModel.isGrid.value == true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
import org.koitharu.kotatsu.explore.domain.ExploreRepository
|
||||
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
|
||||
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
|
||||
@@ -50,11 +51,13 @@ class ExploreViewModel @Inject constructor(
|
||||
valueProducer = { isSourcesGridMode },
|
||||
)
|
||||
|
||||
val isSuggestionsEnabled = settings.observeAsFlow(
|
||||
private val isSuggestionsEnabled = settings.observeAsFlow(
|
||||
key = AppSettings.KEY_SUGGESTIONS,
|
||||
valueProducer = { isSuggestionsEnabled },
|
||||
)
|
||||
|
||||
val sortOrder = MutableStateFlow(SourcesSortOrder.MANUAL) // TODO
|
||||
|
||||
val onOpenManga = MutableEventFlow<Manga>()
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val onShowSuggestionsTip = MutableEventFlow<Unit>()
|
||||
@@ -104,10 +107,6 @@ class ExploreViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setGridMode(value: Boolean) {
|
||||
settings.isSourcesGridMode = value
|
||||
}
|
||||
|
||||
fun respondSuggestionTip(isAccepted: Boolean) {
|
||||
settings.isSuggestionsEnabled = isAccepted
|
||||
settings.closeTip(TIP_SUGGESTIONS)
|
||||
@@ -137,7 +136,7 @@ class ExploreViewModel @Inject constructor(
|
||||
result += RecommendationsItem(recommendation)
|
||||
}
|
||||
if (sources.isNotEmpty()) {
|
||||
result += ListHeader(R.string.remote_sources, R.string.manage)
|
||||
result += ListHeader(R.string.remote_sources, R.string.catalog)
|
||||
if (newSources.isNotEmpty()) {
|
||||
result += TipModel(
|
||||
key = TIP_NEW_SOURCES,
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
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.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
@@ -48,8 +49,8 @@ fun exploreButtonsAD(
|
||||
icon.setColorSchemeColors(
|
||||
context.getThemeColor(
|
||||
materialR.attr.colorPrimary,
|
||||
Color.DKGRAY
|
||||
)
|
||||
Color.DKGRAY,
|
||||
),
|
||||
)
|
||||
binding.buttonRandom.icon = icon
|
||||
icon.start()
|
||||
@@ -98,7 +99,7 @@ fun exploreSourceListItemAD(
|
||||
ItemExploreSourceListBinding.inflate(
|
||||
layoutInflater,
|
||||
parent,
|
||||
false
|
||||
false,
|
||||
)
|
||||
},
|
||||
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
|
||||
@@ -112,6 +113,7 @@ fun exploreSourceListItemAD(
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.source.title
|
||||
binding.textViewSubtitle.text = item.source.getSummary(context)
|
||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||
fallback(fallbackIcon)
|
||||
@@ -132,7 +134,7 @@ fun exploreSourceGridItemAD(
|
||||
ItemExploreSourceGridBinding.inflate(
|
||||
layoutInflater,
|
||||
parent,
|
||||
false
|
||||
false,
|
||||
)
|
||||
},
|
||||
on = { item, _, _ -> item is MangaSourceItem && item.isGrid },
|
||||
|
||||
@@ -106,6 +106,9 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0")
|
||||
abstract fun observeIds(id: Long): Flow<List<Long>>
|
||||
|
||||
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0")
|
||||
abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long>
|
||||
|
||||
/** INSERT **/
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
@@ -171,6 +174,7 @@ abstract class FavouritesDao {
|
||||
ListSortOrder.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id) DESC"
|
||||
ListSortOrder.UPDATED, // for legacy support
|
||||
ListSortOrder.PROGRESS -> "(SELECT percent FROM history WHERE history.manga_id = manga.manga_id) DESC"
|
||||
|
||||
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,10 @@ class FavouritesRepository @Inject constructor(
|
||||
return db.getFavouriteCategoriesDao().find(id.toInt()).toFavouriteCategory()
|
||||
}
|
||||
|
||||
suspend fun getCategoriesIds(mangaIds: Collection<Long>): Set<Long> {
|
||||
return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet()
|
||||
}
|
||||
|
||||
suspend fun createCategory(
|
||||
title: String,
|
||||
sortOrder: ListSortOrder,
|
||||
|
||||
@@ -19,17 +19,12 @@ import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FavouriteSheet :
|
||||
BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(),
|
||||
OnListItemClickListener<MangaCategoryItem> {
|
||||
class FavoriteSheet : BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(), OnListItemClickListener<MangaCategoryItem> {
|
||||
|
||||
private val viewModel: MangaCategoriesViewModel by viewModels()
|
||||
|
||||
private var adapter: MangaCategoriesAdapter? = null
|
||||
private val viewModel by viewModels<FavoriteSheetViewModel>()
|
||||
|
||||
override fun onCreateViewBinding(
|
||||
inflater: LayoutInflater,
|
||||
@@ -41,44 +36,32 @@ class FavouriteSheet :
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
adapter = MangaCategoriesAdapter(this)
|
||||
val adapter = MangaCategoriesAdapter(this)
|
||||
binding.recyclerViewCategories.adapter = adapter
|
||||
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
||||
viewModel.content.observe(viewLifecycleOwner, adapter)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaCategoryItem, view: View) {
|
||||
viewModel.setChecked(item.category.id, !item.isChecked)
|
||||
}
|
||||
|
||||
private fun onContentChanged(categories: List<ListModel>) {
|
||||
adapter?.items = categories
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FavouriteCategoriesDialog"
|
||||
private const val TAG = "FavoriteSheet"
|
||||
const val KEY_MANGA_LIST = "manga_list"
|
||||
|
||||
fun show(fm: FragmentManager, manga: Manga) = Companion.show(fm, listOf(manga))
|
||||
fun show(fm: FragmentManager, manga: Manga) = show(fm, setOf(manga))
|
||||
|
||||
fun show(fm: FragmentManager, manga: Collection<Manga>) =
|
||||
FavouriteSheet().withArgs(1) {
|
||||
putParcelableArrayList(
|
||||
KEY_MANGA_LIST,
|
||||
manga.mapTo(ArrayList(manga.size)) {
|
||||
ParcelableManga(it)
|
||||
},
|
||||
)
|
||||
}.showDistinct(fm, TAG)
|
||||
fun show(fm: FragmentManager, manga: Collection<Manga>) = FavoriteSheet().withArgs(1) {
|
||||
putParcelableArrayList(
|
||||
KEY_MANGA_LIST,
|
||||
manga.mapTo(ArrayList(manga.size), ::ParcelableManga),
|
||||
)
|
||||
}.showDistinct(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -4,77 +4,70 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.ids
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.firstNotNull
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteSheet.Companion.KEY_MANGA_LIST
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MangaCategoriesViewModel @Inject constructor(
|
||||
class FavoriteSheetViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val manga = savedStateHandle.require<List<ParcelableManga>>(KEY_MANGA_LIST).map { it.manga }
|
||||
private val manga = savedStateHandle.require<List<ParcelableManga>>(FavoriteSheet.KEY_MANGA_LIST).mapToSet {
|
||||
it.manga
|
||||
}
|
||||
private val header = CategoriesHeaderItem()
|
||||
|
||||
val content: StateFlow<List<ListModel>> = combine(
|
||||
private val checkedCategories = MutableStateFlow<Set<Long>?>(null)
|
||||
val content = combine(
|
||||
favouritesRepository.observeCategories(),
|
||||
observeCategoriesIds(),
|
||||
) { all, checked ->
|
||||
buildList(all.size + 1) {
|
||||
checkedCategories.filterNotNull(),
|
||||
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
|
||||
) { categories, checked, tracker ->
|
||||
buildList(categories.size + 1) {
|
||||
add(header)
|
||||
all.mapTo(this) {
|
||||
categories.mapTo(this) { cat ->
|
||||
MangaCategoryItem(
|
||||
category = it,
|
||||
isChecked = it.id in checked,
|
||||
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources,
|
||||
category = cat,
|
||||
isChecked = cat.id in checked,
|
||||
isTrackerEnabled = tracker,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(header))
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
checkedCategories.value = favouritesRepository.getCategoriesIds(manga.ids())
|
||||
}
|
||||
}
|
||||
|
||||
fun setChecked(categoryId: Long, isChecked: Boolean) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val checkedIds = checkedCategories.firstNotNull()
|
||||
if (isChecked) {
|
||||
checkedCategories.value = checkedIds + categoryId
|
||||
favouritesRepository.addToCategory(categoryId, manga)
|
||||
} else {
|
||||
checkedCategories.value = checkedIds - categoryId
|
||||
favouritesRepository.removeFromCategory(categoryId, manga.ids())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeCategoriesIds() = if (manga.size == 1) {
|
||||
// Fast path
|
||||
favouritesRepository.observeCategoriesIds(manga[0].id)
|
||||
} else {
|
||||
combine(
|
||||
manga.map { favouritesRepository.observeCategoriesIds(it.id) },
|
||||
) { array ->
|
||||
val result = HashSet<Long>()
|
||||
var isFirst = true
|
||||
for (ids in array) {
|
||||
if (isFirst) {
|
||||
result.addAll(ids)
|
||||
isFirst = false
|
||||
} else {
|
||||
result.retainAll(ids.toSet())
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.model.findById
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||
@@ -36,6 +37,7 @@ class HistoryRepository @Inject constructor(
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val settings: AppSettings,
|
||||
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
|
||||
private val mangaRepository: MangaDataRepository,
|
||||
) {
|
||||
|
||||
suspend fun getList(offset: Int, limit: Int): List<Manga> {
|
||||
@@ -92,13 +94,8 @@ class HistoryRepository @Inject constructor(
|
||||
if (shouldSkip(manga)) {
|
||||
return
|
||||
}
|
||||
val tags = manga.tags.toEntities()
|
||||
db.withTransaction {
|
||||
val existing = db.getMangaDao().find(manga.id)?.manga
|
||||
if (existing == null || existing.source == manga.source.name) {
|
||||
db.getTagsDao().upsert(tags)
|
||||
db.getMangaDao().upsert(manga.toEntity(), tags)
|
||||
}
|
||||
mangaRepository.storeManga(manga)
|
||||
db.getHistoryDao().upsert(
|
||||
HistoryEntity(
|
||||
mangaId = manga.id,
|
||||
|
||||
@@ -41,7 +41,7 @@ import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteSheet
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||
@@ -284,7 +284,7 @@ abstract class MangaListFragment :
|
||||
}
|
||||
|
||||
R.id.action_favourite -> {
|
||||
FavouriteSheet.show(childFragmentManager, selectedItems)
|
||||
FavoriteSheet.show(childFragmentManager, selectedItems)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.koitharu.kotatsu.list.ui.preview
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import coil.ImageLoader
|
||||
@@ -52,7 +52,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.buttonClose.isVisible = activity is MangaListActivity
|
||||
binding.buttonClose.setOnClickListener(this)
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
binding.textViewAuthor.setOnClickListener(this)
|
||||
binding.imageViewCover.setOnClickListener(this)
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.withResumed
|
||||
import androidx.transition.TransitionManager
|
||||
@@ -229,6 +230,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH)
|
||||
navigationDelegate.primaryFragment?.let {
|
||||
setMaxLifecycle(it, Lifecycle.State.STARTED)
|
||||
}
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
runOnCommit { onSearchOpened() }
|
||||
}
|
||||
@@ -414,16 +418,20 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
private inner class CloseSearchCallback : OnBackPressedCallback(false) {
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH)
|
||||
val fm = supportFragmentManager
|
||||
val fragment = fm.findFragmentByTag(TAG_SEARCH)
|
||||
viewBinding.searchView.clearFocus()
|
||||
if (fragment == null) {
|
||||
// this should not happen but who knows
|
||||
isEnabled = false
|
||||
return
|
||||
}
|
||||
supportFragmentManager.commit {
|
||||
fm.commit {
|
||||
setReorderingAllowed(true)
|
||||
remove(fragment)
|
||||
navigationDelegate.primaryFragment?.let {
|
||||
setMaxLifecycle(it, Lifecycle.State.RESUMED)
|
||||
}
|
||||
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
runOnCommit { onSearchClosed() }
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
import org.koitharu.kotatsu.core.util.GridTouchHelper
|
||||
import org.koitharu.kotatsu.core.util.IdlingDetector
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
@@ -73,7 +74,8 @@ class ReaderActivity :
|
||||
ReaderConfigSheet.Callback,
|
||||
ReaderControlDelegate.OnInteractionListener,
|
||||
OnApplyWindowInsetsListener,
|
||||
IdlingDetector.Callback {
|
||||
IdlingDetector.Callback,
|
||||
ZoomControl.ZoomControlListener {
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
@@ -111,6 +113,7 @@ class ReaderActivity :
|
||||
controlDelegate = ReaderControlDelegate(resources, settings, this, this)
|
||||
viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
|
||||
viewBinding.slider.setLabelFormatter(PageLabelFormatter())
|
||||
viewBinding.zoomControl.listener = this
|
||||
ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider)
|
||||
insetsDelegate.interceptingWindowInsetsListener = this
|
||||
idlingDetector.bindToLifecycle(this)
|
||||
@@ -146,6 +149,9 @@ class ReaderActivity :
|
||||
.setAnchorView(viewBinding.appbarBottom)
|
||||
.show()
|
||||
}
|
||||
viewModel.isZoomControlsEnabled.observe(this) {
|
||||
viewBinding.zoomControl.isVisible = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun getParentActivityIntent(): Intent? {
|
||||
@@ -163,6 +169,14 @@ class ReaderActivity :
|
||||
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
|
||||
}
|
||||
|
||||
override fun onZoomIn() {
|
||||
readerManager.currentReader?.onZoomIn()
|
||||
}
|
||||
|
||||
override fun onZoomOut() {
|
||||
readerManager.currentReader?.onZoomOut()
|
||||
}
|
||||
|
||||
private fun onInitReader(mode: ReaderMode?) {
|
||||
if (mode == null) {
|
||||
return
|
||||
@@ -293,15 +307,13 @@ class ReaderActivity :
|
||||
private fun onPageSaved(uri: Uri?) {
|
||||
if (uri != null) {
|
||||
Snackbar.make(viewBinding.container, R.string.page_saved, Snackbar.LENGTH_LONG)
|
||||
.setAnchorView(viewBinding.appbarBottom)
|
||||
.setAction(R.string.share) {
|
||||
ShareHelper(this).shareImage(uri)
|
||||
}.show()
|
||||
}
|
||||
} else {
|
||||
Snackbar.make(viewBinding.container, R.string.error_occurred, Snackbar.LENGTH_SHORT)
|
||||
.setAnchorView(viewBinding.appbarBottom)
|
||||
.show()
|
||||
}
|
||||
}.setAnchorView(viewBinding.appbarBottom)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun setWindowSecure(isSecure: Boolean) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
@@ -118,17 +119,13 @@ class ReaderViewModel @Inject constructor(
|
||||
valueProducer = { isReaderKeepScreenOn },
|
||||
)
|
||||
|
||||
val isWebtoonZoomEnabled = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_WEBTOON_ZOOM,
|
||||
valueProducer = { isWebtoonZoomEnable },
|
||||
)
|
||||
|
||||
val isZoomControlEnabled = settings.observeAsStateFlow(
|
||||
scope = viewModelScope + Dispatchers.Default,
|
||||
key = AppSettings.KEY_READER_ZOOM_BUTTONS,
|
||||
valueProducer = { isReaderZoomButtonsEnabled },
|
||||
)
|
||||
val isZoomControlsEnabled = getObserveIsZoomControlEnabled().flatMapLatest { zoom ->
|
||||
if (zoom) {
|
||||
combine(readerMode, observeIsWebtoonZoomEnabled()) { mode, ze -> ze || mode != ReaderMode.WEBTOON }
|
||||
} else {
|
||||
flowOf(false)
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
|
||||
|
||||
val readerSettings = ReaderSettings(
|
||||
parentScope = viewModelScope,
|
||||
@@ -259,10 +256,13 @@ class ReaderViewModel @Inject constructor(
|
||||
@MainThread
|
||||
fun onCurrentPageChanged(position: Int) {
|
||||
val prevJob = stateChangeJob
|
||||
val pages = content.value.pages // capture immediately
|
||||
stateChangeJob = launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
loadingJob?.join()
|
||||
val pages = content.value.pages
|
||||
if (BuildConfig.DEBUG && pages.size != content.value.pages.size) {
|
||||
throw IllegalStateException("Concurrent pages modification")
|
||||
}
|
||||
pages.getOrNull(position)?.let { page ->
|
||||
currentState.update { cs ->
|
||||
cs?.copy(chapterId = page.chapterId, page = page.index)
|
||||
@@ -402,4 +402,14 @@ class ReaderViewModel @Inject constructor(
|
||||
val ppc = 1f / chaptersCount
|
||||
return ppc * chapterIndex + ppc * pagePercent
|
||||
}
|
||||
|
||||
private fun observeIsWebtoonZoomEnabled() = settings.observeAsFlow(
|
||||
key = AppSettings.KEY_WEBTOON_ZOOM,
|
||||
valueProducer = { isWebtoonZoomEnable },
|
||||
)
|
||||
|
||||
private fun getObserveIsZoomControlEnabled() = settings.observeAsFlow(
|
||||
key = AppSettings.KEY_READER_ZOOM_BUTTONS,
|
||||
valueProducer = { isReaderZoomButtonsEnabled },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,10 @@ class ReaderConfigSheet :
|
||||
|
||||
private val viewModel by activityViewModels<ReaderViewModel>()
|
||||
private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
|
||||
private var orientationHelper: ScreenOrientationHelper? = null
|
||||
|
||||
@Inject
|
||||
lateinit var orientationHelper: ScreenOrientationHelper
|
||||
|
||||
private lateinit var mode: ReaderMode
|
||||
|
||||
@Inject
|
||||
@@ -113,7 +116,7 @@ class ReaderConfigSheet :
|
||||
}
|
||||
|
||||
R.id.button_screen_rotate -> {
|
||||
orientationHelper?.toggleOrientation()
|
||||
orientationHelper.isLandscape = !orientationHelper.isLandscape
|
||||
}
|
||||
|
||||
R.id.button_color_filter -> {
|
||||
@@ -128,9 +131,13 @@ class ReaderConfigSheet :
|
||||
when (buttonView.id) {
|
||||
R.id.switch_scroll_timer -> {
|
||||
findCallback()?.isAutoScrollEnabled = isChecked
|
||||
requireViewBinding().labelTimer.isVisible = isChecked
|
||||
requireViewBinding().layoutTimer.isVisible = isChecked
|
||||
requireViewBinding().sliderTimer.isVisible = isChecked
|
||||
}
|
||||
|
||||
R.id.switch_screen_lock_rotation -> {
|
||||
orientationHelper.isLocked = isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +166,7 @@ class ReaderConfigSheet :
|
||||
if (fromUser) {
|
||||
settings.readerAutoscrollSpeed = value
|
||||
}
|
||||
(viewBinding ?: return).labelTimerValue.text = getString(R.string.speed_value, value * 10f)
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri?) {
|
||||
@@ -167,14 +175,23 @@ class ReaderConfigSheet :
|
||||
}
|
||||
|
||||
private fun observeScreenOrientation() {
|
||||
val helper = ScreenOrientationHelper(requireActivity())
|
||||
orientationHelper = helper
|
||||
helper.observeAutoOrientation()
|
||||
orientationHelper.observeAutoOrientation()
|
||||
.onEach {
|
||||
requireViewBinding().buttonScreenRotate.isGone = it
|
||||
with(requireViewBinding()) {
|
||||
buttonScreenRotate.isGone = it
|
||||
switchScreenLockRotation.isVisible = it
|
||||
updateOrientationLockSwitch()
|
||||
}
|
||||
}.launchIn(viewLifecycleScope)
|
||||
}
|
||||
|
||||
private fun updateOrientationLockSwitch() {
|
||||
val switch = viewBinding?.switchScreenLockRotation ?: return
|
||||
switch.setOnCheckedChangeListener(null)
|
||||
switch.isChecked = orientationHelper.isLocked
|
||||
switch.setOnCheckedChangeListener(this)
|
||||
}
|
||||
|
||||
private fun findCallback(): Callback? {
|
||||
return (parentFragment as? Callback) ?: (activity as? Callback)
|
||||
}
|
||||
|
||||
@@ -46,9 +46,6 @@ class ReaderSettings(
|
||||
val isPagesNumbersEnabled: Boolean
|
||||
get() = settings.isPagesNumbersEnabled
|
||||
|
||||
val isZoomControlsEnabled: Boolean
|
||||
get() = settings.isReaderZoomButtonsEnabled
|
||||
|
||||
fun applyBackground(view: View) {
|
||||
val bg = settings.readerBackground
|
||||
view.background = bg.resolve(view.context)
|
||||
@@ -106,8 +103,6 @@ class ReaderSettings(
|
||||
if (
|
||||
key == AppSettings.KEY_ZOOM_MODE ||
|
||||
key == AppSettings.KEY_PAGES_NUMBERS ||
|
||||
key == AppSettings.KEY_WEBTOON_ZOOM ||
|
||||
key == AppSettings.KEY_READER_ZOOM_BUTTONS ||
|
||||
key == AppSettings.KEY_READER_BACKGROUND ||
|
||||
key == AppSettings.KEY_32BIT_COLOR
|
||||
) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderAnimation
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
@@ -14,7 +15,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderViewModel
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>() {
|
||||
abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>(), ZoomControl.ZoomControlListener {
|
||||
|
||||
protected val viewModel by activityViewModels<ReaderViewModel>()
|
||||
private var stateToSave: ReaderState? = null
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderAnimation
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.findCurrentViewHolder
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.recyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
||||
@@ -28,6 +29,7 @@ import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderFragment
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.NoAnimPageTransformer
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerEventSupplier
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
|
||||
import javax.inject.Inject
|
||||
@@ -104,6 +106,15 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
|
||||
override fun onZoomIn() {
|
||||
(viewBinding?.pager?.findCurrentViewHolder() as? PageHolder)?.onZoomIn()
|
||||
}
|
||||
|
||||
override fun onZoomOut() {
|
||||
(viewBinding?.pager?.findCurrentViewHolder() as? PageHolder)?.onZoomOut()
|
||||
}
|
||||
|
||||
|
||||
override fun switchPageBy(delta: Int) {
|
||||
with(requireViewBinding().pager) {
|
||||
setCurrentItem(currentItem - delta, isAnimationEnabled())
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.graphics.PointF
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
@@ -12,6 +13,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.ifZero
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
@@ -29,7 +31,8 @@ open class PageHolder(
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, networkState, exceptionResolver),
|
||||
View.OnClickListener {
|
||||
View.OnClickListener,
|
||||
ZoomControl.ZoomControlListener {
|
||||
|
||||
init {
|
||||
binding.ssiv.bindToLifecycle(owner)
|
||||
@@ -40,12 +43,10 @@ open class PageHolder(
|
||||
@Suppress("LeakingThis")
|
||||
bindingInfo.buttonErrorDetails.setOnClickListener(this)
|
||||
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
|
||||
binding.zoomControl.listener = SsivZoomListener(binding.ssiv)
|
||||
}
|
||||
|
||||
override fun onConfigChanged() {
|
||||
super.onConfigChanged()
|
||||
binding.zoomControl.isVisible = settings.isZoomControlsEnabled
|
||||
@Suppress("SENSELESS_COMPARISON")
|
||||
if (settings.applyBitmapConfig(binding.ssiv) && delegate != null) {
|
||||
delegate.reload()
|
||||
@@ -141,4 +142,23 @@ open class PageHolder(
|
||||
bindingInfo.layoutError.isVisible = true
|
||||
bindingInfo.progressBar.hide()
|
||||
}
|
||||
|
||||
override fun onZoomIn() {
|
||||
scaleBy(1.2f)
|
||||
}
|
||||
|
||||
override fun onZoomOut() {
|
||||
scaleBy(0.8f)
|
||||
}
|
||||
|
||||
private fun scaleBy(factor: Float) {
|
||||
val ssiv = binding.ssiv
|
||||
val center = ssiv.getCenter() ?: return
|
||||
val newScale = ssiv.scale * factor
|
||||
ssiv.animateScaleAndCenter(newScale, center)?.apply {
|
||||
withDuration(ssiv.resources.getInteger(android.R.integer.config_shortAnimTime).toLong())
|
||||
withInterpolator(DecelerateInterpolator())
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderAnimation
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.findCurrentViewHolder
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.recyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.resetTransformations
|
||||
@@ -82,6 +83,14 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onZoomIn() {
|
||||
(viewBinding?.pager?.findCurrentViewHolder() as? PageHolder)?.onZoomIn()
|
||||
}
|
||||
|
||||
override fun onZoomOut() {
|
||||
(viewBinding?.pager?.findCurrentViewHolder() as? PageHolder)?.onZoomOut()
|
||||
}
|
||||
|
||||
override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
|
||||
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
|
||||
if (event.actionMasked == MotionEvent.ACTION_SCROLL) {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
|
||||
class SsivZoomListener(
|
||||
private val ssiv: SubsamplingScaleImageView,
|
||||
) : ZoomControl.ZoomControlListener {
|
||||
|
||||
override fun onZoomIn() {
|
||||
scaleBy(1.2f)
|
||||
}
|
||||
|
||||
override fun onZoomOut() {
|
||||
scaleBy(0.8f)
|
||||
}
|
||||
|
||||
private fun scaleBy(factor: Float) {
|
||||
val center = ssiv.getCenter() ?: return
|
||||
val newScale = ssiv.scale * factor
|
||||
ssiv.animateScaleAndCenter(newScale, center)?.apply {
|
||||
withDuration(ssiv.resources.getInteger(android.R.integer.config_shortAnimTime).toLong())
|
||||
withInterpolator(DecelerateInterpolator())
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,15 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.yield
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
|
||||
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
@@ -47,15 +44,6 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
adapter = readerAdapter
|
||||
addOnPageScrollListener(PageScrollListener())
|
||||
}
|
||||
binding.zoomControl.listener = binding.frame
|
||||
|
||||
viewModel.isWebtoonZoomEnabled.observe(viewLifecycleOwner) {
|
||||
binding.frame.isZoomEnable = it
|
||||
}
|
||||
combine(viewModel.isWebtoonZoomEnabled, viewModel.isZoomControlEnabled, Boolean::and)
|
||||
.observe(viewLifecycleOwner) {
|
||||
binding.zoomControl.isVisible = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -111,6 +99,14 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onZoomIn() {
|
||||
viewBinding?.frame?.onZoomIn()
|
||||
}
|
||||
|
||||
override fun onZoomOut() {
|
||||
viewBinding?.frame?.onZoomOut()
|
||||
}
|
||||
|
||||
private fun notifyPageChanged(page: Int) {
|
||||
viewModel.onCurrentPageChanged(page)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
@@ -26,7 +25,7 @@ import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteSheet
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||
@@ -159,7 +158,7 @@ class MultiSearchActivity :
|
||||
}
|
||||
|
||||
R.id.action_favourite -> {
|
||||
FavouriteSheet.show(supportFragmentManager, collectSelectedItems())
|
||||
FavoriteSheet.show(supportFragmentManager, collectSelectedItems())
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
@@ -40,6 +41,7 @@ fun searchSuggestionSourceAD(
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
binding.textViewSubtitle.text = item.source.getSummary(context)
|
||||
binding.switchLocal.isChecked = item.isEnabled
|
||||
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
|
||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
@@ -9,7 +8,6 @@ import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.app.LocaleManagerCompat
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -18,8 +16,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.getLocalesConfig
|
||||
import org.koitharu.kotatsu.core.util.ext.map
|
||||
import org.koitharu.kotatsu.core.util.ext.postDelayed
|
||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
@@ -27,7 +25,6 @@ import org.koitharu.kotatsu.parsers.util.names
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
|
||||
import org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -110,7 +107,7 @@ class AppearanceSettingsFragment :
|
||||
private fun initLocalePicker(preference: ListPreference) {
|
||||
val locales = preference.context.getLocalesConfig()
|
||||
.toList()
|
||||
.sortedWith(LocaleComparator(preference.context))
|
||||
.sortedWith(LocaleComparator())
|
||||
preference.entries = Array(locales.size + 1) { i ->
|
||||
if (i == 0) {
|
||||
getString(R.string.automatic)
|
||||
@@ -134,24 +131,4 @@ class AppearanceSettingsFragment :
|
||||
getString(it.title)
|
||||
}
|
||||
}
|
||||
|
||||
private class LocaleComparator(context: Context) : Comparator<Locale> {
|
||||
|
||||
private val deviceLocales = LocaleManagerCompat.getSystemLocales(context)
|
||||
.map { it.language }
|
||||
|
||||
override fun compare(a: Locale, b: Locale): Int {
|
||||
return if (a === b) {
|
||||
0
|
||||
} else {
|
||||
val indexA = deviceLocales.indexOf(a.language)
|
||||
val indexB = deviceLocales.indexOf(b.language)
|
||||
if (indexA == -1 && indexB == -1) {
|
||||
compareValues(a.language, b.language)
|
||||
} else {
|
||||
-2 - (indexA - indexB)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
@@ -18,7 +17,6 @@ class RootSettingsViewModel @Inject constructor(
|
||||
|
||||
val totalSourcesCount = sourcesRepository.allMangaSources.size
|
||||
|
||||
val enabledSourcesCount = sourcesRepository.observeEnabledSources()
|
||||
.map { it.size }
|
||||
val enabledSourcesCount = sourcesRepository.observeEnabledSourcesCount()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.settings.about.AboutSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.about.AppUpdateDialog
|
||||
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesManageFragment
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.manage.SourcesManageFragment
|
||||
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment
|
||||
|
||||
@@ -153,6 +154,7 @@ class SettingsActivity :
|
||||
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
|
||||
ACTION_HISTORY -> UserDataSettingsFragment()
|
||||
ACTION_TRACKER -> TrackerSettingsFragment()
|
||||
ACTION_SOURCES -> SourcesSettingsFragment()
|
||||
ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment()
|
||||
ACTION_SOURCE -> SourceSettingsFragment.newInstance(
|
||||
intent.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL,
|
||||
@@ -182,6 +184,7 @@ class SettingsActivity :
|
||||
private const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
|
||||
private const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY"
|
||||
private const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
|
||||
private const val ACTION_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES"
|
||||
private const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
|
||||
private const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS"
|
||||
private const val EXTRA_SOURCE = "source"
|
||||
@@ -206,6 +209,10 @@ class SettingsActivity :
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_HISTORY)
|
||||
|
||||
fun newSourcesSettingsIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_SOURCES)
|
||||
|
||||
fun newManageSourcesIntent(context: Context) =
|
||||
Intent(context, SettingsActivity::class.java)
|
||||
.setAction(ACTION_MANAGE_SOURCES)
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.databinding.DialogProgressBinding
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -28,7 +29,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
|
||||
private var backup: File? = null
|
||||
private val saveFileContract = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("*/*"),
|
||||
ActivityResultContracts.CreateDocument("application/zip"),
|
||||
) { uri ->
|
||||
val file = backup
|
||||
if (uri != null && file != null) {
|
||||
@@ -81,7 +82,10 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
|
||||
private fun onBackupDone(file: File) {
|
||||
this.backup = file
|
||||
saveFileContract.launch(file.name)
|
||||
if (!saveFileContract.tryLaunch(file.name)) {
|
||||
Toast.makeText(requireContext(), R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveBackup(file: File, output: Uri) {
|
||||
@@ -91,7 +95,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
|
||||
it.write(file.readBytes())
|
||||
}
|
||||
}
|
||||
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_SHORT).show()
|
||||
dismiss()
|
||||
} catch (e: InterruptedException) {
|
||||
throw e
|
||||
|
||||
@@ -16,15 +16,11 @@ import androidx.work.workDataOf
|
||||
import dagger.Reusable
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
|
||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -52,12 +48,10 @@ class PeriodicalBackupWorker @AssistedInject constructor(
|
||||
}
|
||||
val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData)
|
||||
val target = DocumentFile.fromTreeUri(applicationContext, dirUri)
|
||||
?.createFile("application/zip", file.name)
|
||||
?.createFile("application/zip", file.nameWithoutExtension)
|
||||
?.uri ?: return Result.failure()
|
||||
applicationContext.contentResolver.openOutputStream(target)?.use { output ->
|
||||
file.source().use { input ->
|
||||
output.sink().buffer().writeAllCancellable(input)
|
||||
}
|
||||
applicationContext.contentResolver.openOutputStream(target, "wt")?.use { output ->
|
||||
file.inputStream().copyTo(output)
|
||||
} ?: return Result.failure()
|
||||
file.deleteAwait()
|
||||
return Result.success(resultData)
|
||||
@@ -79,6 +73,7 @@ class PeriodicalBackupWorker @AssistedInject constructor(
|
||||
settings.periodicalBackupFrequency,
|
||||
TimeUnit.DAYS,
|
||||
).setConstraints(constraints.build())
|
||||
.keepResultsForAtLeast(20, TimeUnit.DAYS)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
workManager
|
||||
|
||||
@@ -65,8 +65,6 @@ class NewSourcesDialogFragment :
|
||||
viewModel.onItemEnabledChanged(item, isEnabled)
|
||||
}
|
||||
|
||||
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit
|
||||
|
||||
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
@@ -35,7 +34,6 @@ class NewSourcesViewModel @Inject constructor(
|
||||
SourceConfigItem.SourceItem(
|
||||
source = source,
|
||||
isEnabled = enabled,
|
||||
summary = source.getLocaleTitle(),
|
||||
isDraggable = false,
|
||||
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.names
|
||||
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SourcesSettingsFragment : BasePreferenceFragment(R.string.remote_sources) {
|
||||
|
||||
private val viewModel by viewModels<SourcesSettingsViewModel>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_sources)
|
||||
findPreference<ListPreference>(AppSettings.KEY_SOURCES_ORDER)?.run {
|
||||
entryValues = SourcesSortOrder.entries.names()
|
||||
entries = SourcesSortOrder.entries.map { context.getString(it.titleResId) }.toTypedArray()
|
||||
setDefaultValueCompat(SourcesSortOrder.MANUAL.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_REMOTE_SOURCES)?.let { pref ->
|
||||
viewModel.enabledSourcesCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it >= 0) {
|
||||
resources.getQuantityString(R.plurals.items, it, it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_SOURCES_CATALOG)?.let { pref ->
|
||||
viewModel.availableSourcesCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it >= 0) {
|
||||
getString(R.string.available_d, it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean = when (preference.key) {
|
||||
AppSettings.KEY_SOURCES_CATALOG -> {
|
||||
startActivity(Intent(preference.context, SourcesCatalogActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SourcesSettingsViewModel @Inject constructor(
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val totalSourcesCount = sourcesRepository.allMangaSources.size
|
||||
|
||||
val enabledSourcesCount = sourcesRepository.observeEnabledSourcesCount()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
|
||||
|
||||
val availableSourcesCount = sourcesRepository.observeAvailableSourcesCount()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
|
||||
}
|
||||
@@ -13,8 +13,6 @@ class SourceConfigAdapter(
|
||||
|
||||
init {
|
||||
with(delegatesManager) {
|
||||
addDelegate(sourceConfigHeaderDelegate())
|
||||
addDelegate(sourceConfigGroupDelegate(listener))
|
||||
addDelegate(sourceConfigItemDelegate2(listener, coil, lifecycleOwner))
|
||||
addDelegate(sourceConfigEmptySearchDelegate())
|
||||
addDelegate(sourceConfigTipDelegate(listener))
|
||||
|
||||
@@ -18,6 +18,7 @@ import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getSummary
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
|
||||
@@ -26,144 +27,104 @@ import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemExpandableBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
|
||||
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 sourceConfigHeaderDelegate() =
|
||||
adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
|
||||
{ layoutInflater, parent ->
|
||||
ItemFilterHeaderBinding.inflate(
|
||||
layoutInflater,
|
||||
parent,
|
||||
false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.setText(item.titleResId)
|
||||
}
|
||||
}
|
||||
|
||||
fun sourceConfigGroupDelegate(
|
||||
listener: SourceConfigListener,
|
||||
) =
|
||||
adapterDelegateViewBinding<SourceConfigItem.LocaleGroup, SourceConfigItem, ItemExpandableBinding>(
|
||||
{ layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.root.setOnClickListener {
|
||||
listener.onHeaderClick(item)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.root.text = item.title ?: getString(R.string.various_languages)
|
||||
binding.root.isChecked = item.isExpanded
|
||||
}
|
||||
}
|
||||
|
||||
fun sourceConfigItemCheckableDelegate(
|
||||
listener: SourceConfigListener,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) =
|
||||
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
|
||||
{ layoutInflater, parent ->
|
||||
ItemSourceConfigCheckableBinding.inflate(
|
||||
layoutInflater,
|
||||
parent,
|
||||
false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
|
||||
{ layoutInflater, parent ->
|
||||
ItemSourceConfigCheckableBinding.inflate(
|
||||
layoutInflater,
|
||||
parent,
|
||||
false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
|
||||
listener.onItemEnabledChanged(item, isChecked)
|
||||
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
|
||||
listener.onItemEnabledChanged(item, isChecked)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.isNsfw) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.isNsfw) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
binding.switchToggle.isChecked = item.isEnabled
|
||||
binding.switchToggle.isEnabled = item.isAvailable
|
||||
binding.textViewDescription.textAndVisible = item.summary
|
||||
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)
|
||||
}
|
||||
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,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) =
|
||||
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
|
||||
{ layoutInflater, parent ->
|
||||
ItemSourceConfigBinding.inflate(
|
||||
layoutInflater,
|
||||
parent,
|
||||
false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
|
||||
{ layoutInflater, parent ->
|
||||
ItemSourceConfigBinding.inflate(
|
||||
layoutInflater,
|
||||
parent,
|
||||
false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
val eventListener = View.OnClickListener { v ->
|
||||
when (v.id) {
|
||||
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
|
||||
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
|
||||
R.id.imageView_menu -> showSourceMenu(v, item, listener)
|
||||
}
|
||||
}
|
||||
binding.imageViewRemove.setOnClickListener(eventListener)
|
||||
binding.imageViewAdd.setOnClickListener(eventListener)
|
||||
binding.imageViewMenu.setOnClickListener(eventListener)
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.isNsfw) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
|
||||
binding.imageViewRemove.isVisible = item.isEnabled
|
||||
binding.imageViewMenu.isVisible = item.isEnabled
|
||||
binding.textViewDescription.textAndVisible = item.summary
|
||||
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)
|
||||
}
|
||||
val eventListener = View.OnClickListener { v ->
|
||||
when (v.id) {
|
||||
R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
|
||||
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
|
||||
R.id.imageView_menu -> showSourceMenu(v, item, listener)
|
||||
}
|
||||
}
|
||||
binding.imageViewRemove.setOnClickListener(eventListener)
|
||||
binding.imageViewAdd.setOnClickListener(eventListener)
|
||||
binding.imageViewMenu.setOnClickListener(eventListener)
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.isNsfw) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
|
||||
binding.imageViewRemove.isVisible = item.isEnabled
|
||||
binding.imageViewMenu.isVisible = item.isEnabled
|
||||
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 sourceConfigTipDelegate(
|
||||
listener: OnTipCloseListener<SourceConfigItem.Tip>,
|
||||
@@ -208,6 +169,7 @@ 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_lift)?.isVisible = item.isDraggable
|
||||
menu.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.action_settings -> listener.onItemSettingsClick(item)
|
||||
|
||||
@@ -12,6 +12,4 @@ interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {
|
||||
fun onItemShortcutClick(item: SourceConfigItem.SourceItem)
|
||||
|
||||
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
|
||||
|
||||
fun onHeaderClick(header: SourceConfigItem.LocaleGroup)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
sealed interface SourceCatalogItem : ListModel {
|
||||
|
||||
data class Source(
|
||||
val source: MangaSource
|
||||
) : SourceCatalogItem {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is Source && other.source == source
|
||||
}
|
||||
}
|
||||
|
||||
data class Hint(
|
||||
@DrawableRes val icon: Int,
|
||||
@StringRes val title: Int,
|
||||
@StringRes val text: Int,
|
||||
) : SourceCatalogItem {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is Hint && other.title == title
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
|
||||
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.appendNsfwLabel
|
||||
|
||||
fun sourceCatalogItemSourceAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
listener: OnListItemClickListener<SourceCatalogItem.Source>
|
||||
) = adapterDelegateViewBinding<SourceCatalogItem.Source, SourceCatalogItem, ItemSourceCatalogBinding>(
|
||||
{ layoutInflater, parent ->
|
||||
ItemSourceCatalogBinding.inflate(layoutInflater, parent, false)
|
||||
},
|
||||
) {
|
||||
|
||||
binding.imageViewAdd.setOnClickListener { v ->
|
||||
listener.onItemClick(item, v)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = if (item.source.isNsfw()) {
|
||||
buildSpannedString {
|
||||
append(item.source.title)
|
||||
append(' ')
|
||||
appendNsfwLabel(context)
|
||||
}
|
||||
} else {
|
||||
item.source.title
|
||||
}
|
||||
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 sourceCatalogItemHintAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) = adapterDelegateViewBinding<SourceCatalogItem.Hint, SourceCatalogItem, ItemEmptyHintBinding>(
|
||||
{ inflater, parent -> ItemEmptyHintBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.buttonRetry.isVisible = false
|
||||
|
||||
bind {
|
||||
binding.icon.newImageRequest(lifecycleOwner, item.icon)?.enqueueWith(coil)
|
||||
binding.textPrimary.setText(item.title)
|
||||
binding.textSecondary.setTextAndVisible(item.text)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivitySourcesCatalogBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
|
||||
TabLayout.OnTabSelectedListener,
|
||||
OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
AppBarOwner {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
override val appBar: AppBarLayout
|
||||
get() = viewBinding.appbar
|
||||
|
||||
private val viewModel by viewModels<SourcesCatalogViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivitySourcesCatalogBinding.inflate(layoutInflater))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
initTabs()
|
||||
val sourcesAdapter = SourcesCatalogAdapter(this, coil, this)
|
||||
with(viewBinding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = sourcesAdapter
|
||||
}
|
||||
viewModel.content.observe(this, sourcesAdapter)
|
||||
viewModel.onActionDone.observeEvent(
|
||||
this,
|
||||
ReversibleActionObserver(viewBinding.recyclerView),
|
||||
)
|
||||
viewModel.locale.observe(this) {
|
||||
supportActionBar?.subtitle = it.getLocaleDisplayName()
|
||||
}
|
||||
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel))
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
viewBinding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: SourceCatalogItem.Source, view: View) {
|
||||
viewModel.addSource(item.source)
|
||||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
viewModel.setContentType(tab.tag as ContentType)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
viewBinding.recyclerView.firstVisibleItemPosition = 0
|
||||
}
|
||||
|
||||
private fun initTabs() {
|
||||
val tabs = viewBinding.tabs
|
||||
for (type in ContentType.entries) {
|
||||
if (viewModel.isNsfwDisabled && type == ContentType.HENTAI) {
|
||||
continue
|
||||
}
|
||||
val tab = tabs.newTab()
|
||||
tab.setText(type.titleResId)
|
||||
tab.tag = type
|
||||
tabs.addTab(tab)
|
||||
}
|
||||
tabs.addOnTabSelectedListener(this)
|
||||
}
|
||||
|
||||
private fun String?.getLocaleDisplayName(): String {
|
||||
if (this == null) {
|
||||
return getString(R.string.various_languages)
|
||||
}
|
||||
val lc = Locale(this)
|
||||
return lc.getDisplayLanguage(lc).toTitleCase(lc)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||
|
||||
class SourcesCatalogAdapter(
|
||||
listener: OnListItemClickListener<SourceCatalogItem.Source>,
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
) : BaseListAdapter<SourceCatalogItem>(), FastScroller.SectionIndexer {
|
||||
|
||||
init {
|
||||
addDelegate(ListItemType.CHAPTER, sourceCatalogItemSourceAD(coil, lifecycleOwner, listener))
|
||||
addDelegate(ListItemType.HINT_EMPTY, sourceCatalogItemHintAD(coil, lifecycleOwner))
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
return (items.getOrNull(position) as? SourceCatalogItem.Source)?.source?.title?.take(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.room.InvalidationTracker
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.ViewModelLifecycle
|
||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||
import org.koitharu.kotatsu.core.db.removeObserverAsync
|
||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
|
||||
class SourcesCatalogListProducer @AssistedInject constructor(
|
||||
@Assisted private val locale: String?,
|
||||
@Assisted private val contentType: ContentType,
|
||||
@Assisted lifecycle: ViewModelLifecycle,
|
||||
private val repository: MangaSourcesRepository,
|
||||
private val database: MangaDatabase,
|
||||
) : InvalidationTracker.Observer(TABLE_SOURCES), RetainedLifecycle.OnClearedListener {
|
||||
|
||||
private val scope = lifecycle.lifecycleScope
|
||||
private var query: String = ""
|
||||
val list = MutableStateFlow(emptyList<SourceCatalogItem>())
|
||||
|
||||
private var job = scope.launch(Dispatchers.Default) {
|
||||
list.value = buildList()
|
||||
}
|
||||
|
||||
init {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
database.invalidationTracker.addObserver(this@SourcesCatalogListProducer)
|
||||
}
|
||||
lifecycle.addOnClearedListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
database.invalidationTracker.removeObserverAsync(this)
|
||||
}
|
||||
|
||||
override fun onInvalidated(tables: Set<String>) {
|
||||
val prevJob = job
|
||||
job = scope.launch(Dispatchers.Default) {
|
||||
prevJob.cancelAndJoin()
|
||||
list.update { buildList() }
|
||||
}
|
||||
}
|
||||
|
||||
fun setQuery(value: String) {
|
||||
this.query = value
|
||||
onInvalidated(emptySet())
|
||||
}
|
||||
|
||||
private suspend fun buildList(): List<SourceCatalogItem> {
|
||||
val sources = repository.getDisabledSources().toMutableList()
|
||||
sources.retainAll { it.contentType == contentType && it.locale == locale }
|
||||
if (query.isNotEmpty()) {
|
||||
sources.retainAll { it.title.contains(query, ignoreCase = true) }
|
||||
}
|
||||
return if (sources.isEmpty()) {
|
||||
listOf(
|
||||
if (query.isEmpty()) {
|
||||
SourceCatalogItem.Hint(
|
||||
icon = R.drawable.ic_empty_feed,
|
||||
title = R.string.no_manga_sources,
|
||||
text = R.string.no_manga_sources_catalog_text,
|
||||
)
|
||||
} else {
|
||||
SourceCatalogItem.Hint(
|
||||
icon = R.drawable.ic_empty_feed,
|
||||
title = R.string.nothing_found,
|
||||
text = R.string.no_manga_sources_found,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
sources.sortBy { it.title }
|
||||
sources.map {
|
||||
SourceCatalogItem.Source(
|
||||
source = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
locale: String?,
|
||||
contentType: ContentType,
|
||||
lifecycle: ViewModelLifecycle,
|
||||
): SourcesCatalogListProducer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
|
||||
class SourcesCatalogMenuProvider(
|
||||
private val activity: Activity,
|
||||
private val viewModel: SourcesCatalogViewModel,
|
||||
) : MenuProvider,
|
||||
MenuItem.OnActionExpandListener,
|
||||
SearchView.OnQueryTextListener {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_sources_catalog, menu)
|
||||
val searchMenuItem = menu.findItem(R.id.action_search)
|
||||
searchMenuItem.setOnActionExpandListener(this)
|
||||
val searchView = searchMenuItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(this)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_locales -> {
|
||||
showLocalesMenu()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
(activity as? AppBarOwner)?.appBar?.setExpanded(false, true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
(item.actionView as SearchView).setQuery("", false)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
viewModel.performSearch(newText.orEmpty())
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showLocalesMenu() {
|
||||
val locales = viewModel.locales
|
||||
val anchor: View = (activity as AppBarOwner).appBar.let {
|
||||
it.findViewById<View?>(R.id.toolbar) ?: it
|
||||
}
|
||||
val menu = PopupMenu(activity, anchor)
|
||||
for ((i, lc) in locales.withIndex()) {
|
||||
val title = lc?.getDisplayLanguage(lc)?.toTitleCase(lc) ?: activity.getString(R.string.various_languages)
|
||||
menu.menu.add(Menu.NONE, Menu.NONE, i, title)
|
||||
}
|
||||
menu.setOnMenuItemClickListener {
|
||||
viewModel.setLocale(locales.getOrNull(it.order)?.language)
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.koitharu.kotatsu.settings.sources.catalog
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SourcesCatalogViewModel @Inject constructor(
|
||||
private val repository: MangaSourcesRepository,
|
||||
private val listProducerFactory: SourcesCatalogListProducer.Factory,
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val lifecycle = RetainedLifecycleImpl()
|
||||
private var searchQuery: String = ""
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val contentType = MutableStateFlow(ContentType.entries.first())
|
||||
val locales = getLocalesImpl()
|
||||
val locale = MutableStateFlow(locales.firstOrNull()?.language)
|
||||
|
||||
val isNsfwDisabled = settings.isNsfwContentDisabled
|
||||
|
||||
private val listProducer: StateFlow<SourcesCatalogListProducer?> = combine(
|
||||
locale,
|
||||
contentType,
|
||||
) { lc, type ->
|
||||
listProducerFactory.create(lc, type, lifecycle).also {
|
||||
it.setQuery(searchQuery)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
val content = listProducer.flatMapLatest {
|
||||
it?.list ?: emptyFlow()
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
lifecycle.dispatchOnCleared()
|
||||
}
|
||||
|
||||
fun performSearch(query: String) {
|
||||
searchQuery = query
|
||||
listProducer.value?.setQuery(query)
|
||||
}
|
||||
|
||||
fun setLocale(value: String?) {
|
||||
locale.value = value
|
||||
}
|
||||
|
||||
fun setContentType(value: ContentType) {
|
||||
contentType.value = value
|
||||
}
|
||||
|
||||
fun addSource(source: MangaSource) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val rollback = repository.setSourceEnabled(source, true)
|
||||
onActionDone.call(ReversibleAction(R.string.source_enabled, rollback))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLocalesImpl(): List<Locale?> {
|
||||
return repository.allMangaSources
|
||||
.mapToSet { it.locale?.let(::Locale) }
|
||||
.sortedWith(LocaleComparator())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
package org.koitharu.kotatsu.settings.sources.manage
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.room.InvalidationTracker
|
||||
import dagger.hilt.android.ViewModelLifecycle
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
@@ -15,19 +14,13 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.db.TABLE_SOURCES
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
import org.koitharu.kotatsu.core.model.isNsfw
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.map
|
||||
import org.koitharu.kotatsu.core.util.ext.toEnumSet
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import java.util.Locale
|
||||
import java.util.TreeMap
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
@@ -39,7 +32,6 @@ class SourcesListProducer @Inject constructor(
|
||||
|
||||
private val scope = lifecycle.lifecycleScope
|
||||
private var query: String = ""
|
||||
private val expanded = HashSet<String?>()
|
||||
val list = MutableStateFlow(emptyList<SourceConfigItem>())
|
||||
|
||||
private var job = scope.launch(Dispatchers.Default) {
|
||||
@@ -67,27 +59,19 @@ class SourcesListProducer @Inject constructor(
|
||||
onInvalidated(emptySet())
|
||||
}
|
||||
|
||||
fun expandCollapse(group: String?) {
|
||||
if (!expanded.remove(group)) {
|
||||
expanded.add(group)
|
||||
}
|
||||
onInvalidated(emptySet())
|
||||
}
|
||||
|
||||
private suspend fun buildList(): List<SourceConfigItem> {
|
||||
val allSources = repository.allMangaSources
|
||||
val enabledSources = repository.getEnabledSources()
|
||||
val isNsfwDisabled = settings.isNsfwContentDisabled
|
||||
val withTip = settings.isTipEnabled(TIP_REORDER)
|
||||
val isReorderAvailable = settings.sourcesSortOrder == SourcesSortOrder.MANUAL
|
||||
val withTip = isReorderAvailable && settings.isTipEnabled(TIP_REORDER)
|
||||
val enabledSet = enabledSources.toEnumSet()
|
||||
if (query.isNotEmpty()) {
|
||||
return allSources.mapNotNull {
|
||||
return enabledSources.mapNotNull {
|
||||
if (!it.title.contains(query, ignoreCase = true)) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = it.getLocaleTitle(),
|
||||
isEnabled = it in enabledSet,
|
||||
isDraggable = false,
|
||||
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
||||
@@ -96,17 +80,8 @@ class SourcesListProducer @Inject constructor(
|
||||
listOf(SourceConfigItem.EmptySearchResult)
|
||||
}
|
||||
}
|
||||
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) {
|
||||
if (it in enabledSet) {
|
||||
KEY_ENABLED
|
||||
} else {
|
||||
it.locale
|
||||
}
|
||||
}
|
||||
map.remove(KEY_ENABLED)
|
||||
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
|
||||
val result = ArrayList<SourceConfigItem>(enabledSources.size + 1)
|
||||
if (enabledSources.isNotEmpty()) {
|
||||
result += SourceConfigItem.Header(R.string.enabled_sources)
|
||||
if (withTip) {
|
||||
result += SourceConfigItem.Tip(
|
||||
TIP_REORDER,
|
||||
@@ -117,70 +92,17 @@ class SourcesListProducer @Inject constructor(
|
||||
enabledSources.mapTo(result) {
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = it.getLocaleTitle(),
|
||||
isEnabled = true,
|
||||
isDraggable = true,
|
||||
isDraggable = isReorderAvailable,
|
||||
isAvailable = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (enabledSources.size != allSources.size) {
|
||||
result += SourceConfigItem.Header(R.string.available_sources)
|
||||
val comparator = compareBy<MangaSource, String>(AlphanumComparator()) { it.name }
|
||||
for ((key, list) in map) {
|
||||
list.sortWith(comparator)
|
||||
val isExpanded = key in expanded
|
||||
result += SourceConfigItem.LocaleGroup(
|
||||
localeId = key,
|
||||
title = getLocaleTitle(key),
|
||||
isExpanded = isExpanded,
|
||||
)
|
||||
if (isExpanded) {
|
||||
list.mapTo(result) {
|
||||
SourceConfigItem.SourceItem(
|
||||
source = it,
|
||||
summary = null,
|
||||
isEnabled = false,
|
||||
isDraggable = false,
|
||||
isAvailable = !isNsfwDisabled || !it.isNsfw(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private class LocaleKeyComparator : Comparator<String?> {
|
||||
|
||||
private val deviceLocales = LocaleListCompat.getAdjustedDefault()
|
||||
.map { it.language }
|
||||
|
||||
override fun compare(a: String?, b: String?): Int {
|
||||
when {
|
||||
a == b -> return 0
|
||||
a == null -> return 1
|
||||
b == null -> return -1
|
||||
}
|
||||
val ai = deviceLocales.indexOf(a!!)
|
||||
val bi = deviceLocales.indexOf(b!!)
|
||||
return when {
|
||||
ai < 0 && bi < 0 -> a.compareTo(b)
|
||||
ai < 0 -> 1
|
||||
bi < 0 -> -1
|
||||
else -> ai.compareTo(bi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private fun getLocaleTitle(localeKey: String?): String? {
|
||||
val locale = Locale(localeKey ?: return null)
|
||||
return locale.getDisplayLanguage(locale).toTitleCase(locale)
|
||||
}
|
||||
|
||||
private const val KEY_ENABLED = "!"
|
||||
const val TIP_REORDER = "src_reorder"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
package org.koitharu.kotatsu.settings.sources.manage
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@@ -30,8 +31,10 @@ import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
|
||||
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
|
||||
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
|
||||
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -77,14 +80,14 @@ class SourcesManageFragment :
|
||||
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
|
||||
viewModel.onActionDone.observeEvent(
|
||||
viewLifecycleOwner,
|
||||
ReversibleActionObserver(binding.recyclerView)
|
||||
ReversibleActionObserver(binding.recyclerView),
|
||||
)
|
||||
addMenuProvider(SourcesMenuProvider())
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
activity?.setTitle(R.string.remote_sources)
|
||||
activity?.setTitle(R.string.manage_sources)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -119,10 +122,6 @@ class SourcesManageFragment :
|
||||
viewModel.setEnabled(item.source, isEnabled)
|
||||
}
|
||||
|
||||
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) {
|
||||
viewModel.expandOrCollapse(header.localeId)
|
||||
}
|
||||
|
||||
override fun onCloseTip(tip: SourceConfigItem.Tip) {
|
||||
viewModel.onTipClosed(tip)
|
||||
}
|
||||
@@ -143,6 +142,11 @@ class SourcesManageFragment :
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_catalog -> {
|
||||
startActivity(Intent(context, SourcesCatalogActivity::class.java))
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_disable_all -> {
|
||||
viewModel.disableAll()
|
||||
true
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.settings.sources
|
||||
package org.koitharu.kotatsu.settings.sources.manage
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -103,10 +103,6 @@ class SourcesManageViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun expandOrCollapse(headerId: String?) {
|
||||
listProducer.expandCollapse(headerId)
|
||||
}
|
||||
|
||||
fun performSearch(query: String?) {
|
||||
listProducer.setQuery(query?.trim().orEmpty())
|
||||
}
|
||||
@@ -2,45 +2,15 @@ package org.koitharu.kotatsu.settings.sources.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
sealed interface SourceConfigItem : ListModel {
|
||||
|
||||
data class Header(
|
||||
@StringRes val titleResId: Int,
|
||||
) : SourceConfigItem {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is Header && other.titleResId == titleResId
|
||||
}
|
||||
}
|
||||
|
||||
data class LocaleGroup(
|
||||
val localeId: String?,
|
||||
val title: String?,
|
||||
val isExpanded: Boolean,
|
||||
) : SourceConfigItem {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is LocaleGroup && other.localeId == localeId
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return if (previousState is LocaleGroup && previousState.isExpanded != isExpanded) {
|
||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SourceItem(
|
||||
val source: MangaSource,
|
||||
val isEnabled: Boolean,
|
||||
val summary: String?,
|
||||
val isDraggable: Boolean,
|
||||
val isAvailable: Boolean,
|
||||
) : SourceConfigItem {
|
||||
@@ -51,14 +21,6 @@ sealed interface SourceConfigItem : ListModel {
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is SourceItem && other.source == source
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return if (previousState is SourceItem && previousState.isEnabled != isEnabled) {
|
||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Tip(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.koitharu.kotatsu.settings.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.method.LinkMovementMethodCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
|
||||
@@ -13,11 +13,9 @@ class LinksPreference @JvmOverloads constructor(
|
||||
defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle,
|
||||
defStyleRes: Int = 0,
|
||||
) : Preference(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
val summaryView = holder.findViewById(android.R.id.summary) as TextView
|
||||
summaryView.movementMethod = LinkMovementMethod.getInstance()
|
||||
summaryView.movementMethod = LinkMovementMethodCompat.getInstance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
app/src/main/res/drawable/ic_screen_rotation_lock.xml
Normal file
11
app/src/main/res/drawable/ic_screen_rotation_lock.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M16.8,2.5C16.8,1.56 17.56,0.8 18.5,0.8C19.44,0.8 20.2,1.56 20.2,2.5V3H16.8V2.5M16,9H21A1,1 0 0,0 22,8V4A1,1 0 0,0 21,3V2.5A2.5,2.5 0 0,0 18.5,0A2.5,2.5 0 0,0 16,2.5V3A1,1 0 0,0 15,4V8A1,1 0 0,0 16,9M8.47,20.5C5.2,18.94 2.86,15.76 2.5,12H1C1.5,18.16 6.66,23 12.95,23L13.61,22.97L9.8,19.15L8.47,20.5M23.25,12.77L20.68,10.2L19.27,11.61L21.5,13.83L15.83,19.5L4.5,8.17L10.17,2.5L12.27,4.61L13.68,3.2L11.23,0.75C10.64,0.16 9.69,0.16 9.11,0.75L2.75,7.11C2.16,7.7 2.16,8.65 2.75,9.23L14.77,21.25C15.36,21.84 16.31,21.84 16.89,21.25L23.25,14.89C23.84,14.3 23.84,13.35 23.25,12.77Z" />
|
||||
</vector>
|
||||
@@ -22,6 +22,17 @@
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
android:id="@+id/zoomControl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_dodgeInsetEdges="bottom"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_top"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:background="@color/grey" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
android:id="@+id/zoomControl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_dodgeInsetEdges="bottom"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.koitharu.kotatsu.reader.ui.ReaderInfoBarView
|
||||
android:id="@+id/infoBar"
|
||||
android:layout_width="match_parent"
|
||||
@@ -62,9 +73,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="1"
|
||||
android:valueFrom="0"
|
||||
app:trackColorInactive="?attr/m3ColorBackground"
|
||||
app:labelBehavior="floating"
|
||||
app:tickVisible="false" />
|
||||
app:tickVisible="false"
|
||||
app:trackColorInactive="?attr/m3ColorBackground" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
|
||||
47
app/src/main/res/layout/activity_sources_catalog.xml
Normal file
47
app/src/main/res/layout/activity_sources_catalog.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
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="match_parent"
|
||||
tools:context=".settings.sources.catalog.SourcesCatalogActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabGravity="start"
|
||||
app:tabMode="scrollable" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.list.fastscroll.FastScrollRecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_source_config" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -2,7 +2,6 @@
|
||||
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame
|
||||
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:id="@+id/frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -17,14 +16,4 @@
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
android:id="@+id/zoomControl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<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="@drawable/list_selector"
|
||||
android:clipChildren="false">
|
||||
android:baselineAligned="false"
|
||||
android:clipChildren="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_icon"
|
||||
@@ -23,19 +26,32 @@
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
app:layout_constraintBottom_toBottomOf="@id/imageView_icon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_icon"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView_icon"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
android:layout_marginEnd="@dimen/margin_small"
|
||||
android:orientation="vertical">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<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>
|
||||
|
||||
@@ -27,14 +27,4 @@
|
||||
|
||||
<include layout="@layout/layout_page_info" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ZoomControl
|
||||
android:id="@+id/zoomControl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="@dimen/margin_small">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
@@ -18,17 +20,34 @@
|
||||
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="@dimen/margin_small"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
tools:text="@tools:sample/lorem[2]" />
|
||||
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>
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
@@ -42,4 +61,4 @@
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="?listPreferredItemPaddingEnd" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
49
app/src/main/res/layout/item_source_catalog.xml
Normal file
49
app/src/main/res/layout/item_source_catalog.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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="?android:windowBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="@dimen/margin_small"
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?listPreferredItemPaddingEnd">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_icon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="?colorControlHighlight"
|
||||
android:labelFor="@id/textView_title"
|
||||
android:scaleType="fitCenter"
|
||||
app:shapeAppearance="?shapeAppearanceCornerSmall"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="?android:listPreferredItemPaddingStart"
|
||||
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
tools:text="@tools:sample/lorem[15]" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/add"
|
||||
android:padding="@dimen/margin_small"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_add"
|
||||
android:tooltipText="@string/add" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -7,6 +7,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:windowBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="@dimen/margin_small"
|
||||
android:paddingStart="?listPreferredItemPaddingStart"
|
||||
@@ -36,7 +37,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
tools:text="@tools:sample/lorem[15]" />
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -48,6 +48,22 @@
|
||||
app:drawableStartCompat="@drawable/ic_screen_rotation"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_screen_lock_rotation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:ellipsize="end"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:singleLine="true"
|
||||
android:text="@string/lock_screen_rotation"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_screen_rotation_lock"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -119,18 +135,35 @@
|
||||
android:textColor="?colorOnSurfaceVariant"
|
||||
app:drawableStartCompat="@drawable/ic_timer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_timer"
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_timer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:text="@string/speed"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:textAppearance="?textAppearanceTitleSmall"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
<TextView
|
||||
android:id="@+id/label_timer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/speed"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_timer_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_small"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
tools:text="x0.5" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.CubicSlider
|
||||
android:id="@+id/slider_timer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -21,10 +21,16 @@
|
||||
android:title="@string/save"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_favourite"
|
||||
android:icon="@drawable/ic_heart"
|
||||
android:title="@string/categories"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_select_all"
|
||||
android:icon="?actionModeSelectAllDrawable"
|
||||
android:title="@android:string/selectAll"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
</menu>
|
||||
</menu>
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_grid"
|
||||
android:checkable="true"
|
||||
android:title="@string/show_in_grid_view"
|
||||
android:titleCondensed="@string/grid" />
|
||||
android:id="@+id/action_manage"
|
||||
android:title="@string/manage_sources"
|
||||
android:titleCondensed="@string/manage" />
|
||||
|
||||
</menu>
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_catalog"
|
||||
android:icon="@drawable/ic_add"
|
||||
android:title="@string/sources_catalog"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="?actionModeWebSearchDrawable"
|
||||
|
||||
19
app/src/main/res/menu/opt_sources_catalog.xml
Normal file
19
app/src/main/res/menu/opt_sources_catalog.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_locales"
|
||||
android:icon="@drawable/ic_expand_more"
|
||||
android:title="@string/languages"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="?actionModeWebSearchDrawable"
|
||||
android:title="@string/search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="ifRoom|collapseActionView" />
|
||||
|
||||
</menu>
|
||||
@@ -494,4 +494,16 @@
|
||||
<string name="enhanced_colors">32-бітны каляровы рэжым</string>
|
||||
<string name="suggest_new_sources_summary">Прапаноўваць крыніцы мангі, дададзеныя ў апошнім абнаўленні праграмы</string>
|
||||
<string name="online_variant">Анлайн варыянт</string>
|
||||
<string name="frequency_every_day">Кожны дзень</string>
|
||||
<string name="backup_frequency">Частата стварэння рэзервовых копій</string>
|
||||
<string name="periodic_backups_enable">Уключыць перыядычнае рэзервовае капіраванне</string>
|
||||
<string name="frequency_every_2_days">Кожныя 2 дні</string>
|
||||
<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="last_successful_backup">Апошняе паспяховае рэзервовае капіраванне: %s</string>
|
||||
<string name="backups_output_directory">Вывадны каталог рэзервовых копій</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="lock_screen_rotation">Блакаванне павароту экрана</string>
|
||||
</resources>
|
||||
@@ -503,4 +503,21 @@
|
||||
<string name="frequency_twice_per_month">Dos veces al mes</string>
|
||||
<string name="frequency_once_per_month">Una vez al mes</string>
|
||||
<string name="backups_output_directory">Directorio para guardar la copia de seguridad</string>
|
||||
<string name="last_successful_backup">La última copia de seguridad correcta: %s</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="lock_screen_rotation">Rotación de la pantalla de bloqueo</string>
|
||||
<string name="sources_catalog">Catálogo de las fuentes</string>
|
||||
<string name="content_type_manga">Manga</string>
|
||||
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||
<string name="content_type_hentai">Hentai</string>
|
||||
<string name="content_type_comics">Cómic</string>
|
||||
<string name="no_manga_sources_found">No se han encontrado fuentes de manga disponibles en su búsqueda</string>
|
||||
<string name="source_enabled">Fuente habilitada</string>
|
||||
<string name="no_manga_sources_catalog_text">Aún no hay fuentes disponibles en esta sección. Permanezca atento</string>
|
||||
<string name="content_type_other">Otros</string>
|
||||
<string name="catalog">Catálogo</string>
|
||||
<string name="manage_sources">Gestionar las fuentes</string>
|
||||
<string name="manual">Manual</string>
|
||||
<string name="disable_nsfw_summary">Desactivar las fuentes NSFW y ocultar el manga para adultos de la lista si es posible</string>
|
||||
<string name="available_d">Disponible: %1$d</string>
|
||||
</resources>
|
||||
@@ -503,4 +503,21 @@
|
||||
<string name="frequency_twice_per_month">Dalawang beses bawat buwan</string>
|
||||
<string name="frequency_once_per_month">Isang beses bawat buwan</string>
|
||||
<string name="backups_output_directory">Output directory ng mga backup</string>
|
||||
<string name="last_successful_backup">Huling matagumpay na pag-backup: %s</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="sources_catalog">Katalugo ng mga source</string>
|
||||
<string name="content_type_manga">Manga</string>
|
||||
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||
<string name="content_type_hentai">Hentai</string>
|
||||
<string name="content_type_comics">Mga Comic</string>
|
||||
<string name="catalog">Katalogo</string>
|
||||
<string name="manage_sources">Pamahalaan ang mga source</string>
|
||||
<string name="no_manga_sources_found">Walang available na manga source na nahanap base sa iyong query</string>
|
||||
<string name="lock_screen_rotation">Rotation ng lock screen</string>
|
||||
<string name="manual">Manu-mano</string>
|
||||
<string name="source_enabled">Napaganang source</string>
|
||||
<string name="disable_nsfw_summary">Huwag paganahin ang mga source na NSFW at itago ang manga na pang-adulto mula sa listahan kung maaari</string>
|
||||
<string name="no_manga_sources_catalog_text">Walang available na source sa seksyong ito. Antabayanan</string>
|
||||
<string name="available_d">Available: %1$d</string>
|
||||
<string name="content_type_other">Iba pa</string>
|
||||
</resources>
|
||||
@@ -9,13 +9,13 @@
|
||||
<string name="grid">Kisi</string>
|
||||
<string name="list_mode">Mode daftar</string>
|
||||
<string name="settings">Pengaturan</string>
|
||||
<string name="remote_sources">Sumber jarak jauh</string>
|
||||
<string name="remote_sources">Sumber manga</string>
|
||||
<string name="loading_">Memuat…</string>
|
||||
<string name="computing_">Menghitung…</string>
|
||||
<string name="chapter_d_of_d">Bab %1$d dari %2$d</string>
|
||||
<string name="close">Tutup</string>
|
||||
<string name="try_again">Coba lagi</string>
|
||||
<string name="nothing_found">Nihil</string>
|
||||
<string name="nothing_found">Hasil tidak ditemukan</string>
|
||||
<string name="history_is_empty">Belum ada riwayat</string>
|
||||
<string name="read">Baca</string>
|
||||
<string name="you_have_not_favourites_yet">Belum ada favorit</string>
|
||||
@@ -37,8 +37,8 @@
|
||||
<string name="updated">Diperbarui</string>
|
||||
<string name="newest">Terbaru</string>
|
||||
<string name="by_rating">Peringkat</string>
|
||||
<string name="sort_order">Urutan penyortiran</string>
|
||||
<string name="filter">Saring</string>
|
||||
<string name="sort_order">Urutkan berdasarkan</string>
|
||||
<string name="filter">Filter</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="light">Terang</string>
|
||||
<string name="dark">Gelap</string>
|
||||
@@ -478,4 +478,31 @@
|
||||
<string name="directories">Direktori</string>
|
||||
<string name="main_screen_sections">Bagian layar utama</string>
|
||||
<string name="to_top">Ke atas</string>
|
||||
<string name="zoom_in">Perbesar</string>
|
||||
<string name="frequency_every_day">Setiap Hari</string>
|
||||
<string name="categories">Kategori</string>
|
||||
<string name="frequency_every_2_days">Setiap 2 Hari</string>
|
||||
<string name="online_variant">Variasi Online</string>
|
||||
<string name="keep_screen_on">Biarkan Layar Menyala</string>
|
||||
<string name="zoom_out">Perkecil</string>
|
||||
<string name="keep_screen_on_summary">Jangan Matikan Layar Saat Membaca Komik</string>
|
||||
<string name="list_options">Opsi daftar</string>
|
||||
<string name="reader_zoom_buttons_summary">Apakah menampilkan tombol kontrol zoom di sudut kanan bawah</string>
|
||||
<string name="backup_frequency">Frekuensi pembuatan cadangan</string>
|
||||
<string name="suggest_new_sources">Sumber baru yang disarankan setelah pembaruan aplikasi</string>
|
||||
<string name="periodic_backups_enable">Aktifkan pencadangan berkala</string>
|
||||
<string name="moved_to_top">Bergerak ke atas</string>
|
||||
<string name="enhanced_colors_summary">Mengurangi banding, tetapi dapat mempengaruhi kinerja</string>
|
||||
<string name="frequency_once_per_week">Sekali dalam seminggu</string>
|
||||
<string name="periodic_backups">Pencadangan berkala</string>
|
||||
<string name="reader_zoom_buttons">Tampilkan tombol zoom</string>
|
||||
<string name="frequency_twice_per_month">Dua kali sebulan</string>
|
||||
<string name="by_relevance">Relevansi</string>
|
||||
<string name="state_abandoned">Istirahat</string>
|
||||
<string name="frequency_once_per_month">Sebulan sekali</string>
|
||||
<string name="enhanced_colors">mode warna 32-bit</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="last_successful_backup">Cadangan sukses terakhir: %s</string>
|
||||
<string name="backups_output_directory">Direktori keluaran cadangan</string>
|
||||
<string name="suggest_new_sources_summary">Prompt untuk mengaktifkan sumber baru yang ditambahkan setelah memperbarui aplikasi</string>
|
||||
</resources>
|
||||
31
app/src/main/res/values-kk/plurals.xml
Normal file
31
app/src/main/res/values-kk/plurals.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="one">%1$d минут бұрын</item>
|
||||
<item quantity="other">%1$d минут бұрын</item>
|
||||
</plurals>
|
||||
<plurals name="items">
|
||||
<item quantity="one">%1$d елемент</item>
|
||||
<item quantity="other">%1$d елемент</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="one">%1$d тарау</item>
|
||||
<item quantity="other">%1$d тарау</item>
|
||||
</plurals>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="one">%1$d жаңа тарау</item>
|
||||
<item quantity="other">%1$d жаңа тарау</item>
|
||||
</plurals>
|
||||
<plurals name="months_ago">
|
||||
<item quantity="one">%1$d ай бұрын</item>
|
||||
<item quantity="other">%1$d ай бұрын</item>
|
||||
</plurals>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="one">%1$d күн бұрын</item>
|
||||
<item quantity="other">%1$d күн бұрын</item>
|
||||
</plurals>
|
||||
<plurals name="hours_ago">
|
||||
<item quantity="one">%1$d сағат бұрын</item>
|
||||
<item quantity="other">%1$d сағат бұрын</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="text_search_holder_secondary">Сұрауыңызды қайталап көріңіз.</string>
|
||||
<string name="text_history_holder_secondary">Шеткі мәзірден оқуға болатынды табыңыз.</string>
|
||||
<string name="text_history_holder_secondary">«Шолу» бөлімінен не оқуға болатынын табыңыз.</string>
|
||||
<string name="text_local_holder_secondary">Файл импорттаңыз не онлайн каталогтан бірдеңе сақтаңыз.</string>
|
||||
<string name="other_storage">Басқа бума</string>
|
||||
<string name="new_version_s">Жаңа нұсқа: %s</string>
|
||||
@@ -15,7 +15,7 @@
|
||||
<string name="new_chapters">Жаңа тараулар</string>
|
||||
<string name="local_storage">Құрылғыда</string>
|
||||
<string name="history">Тарих</string>
|
||||
<string name="chapters">Тараулар</string>
|
||||
<string name="chapters">Тарау</string>
|
||||
<string name="detailed_list">Егжей-тегжейлі тізім</string>
|
||||
<string name="list_mode">Тізім түрі</string>
|
||||
<string name="remote_sources">Маңга қайнары</string>
|
||||
@@ -23,8 +23,8 @@
|
||||
<string name="computing_">Есептеу…</string>
|
||||
<string name="favourites">Таңдаулы</string>
|
||||
<string name="network_error">Желі қатесі</string>
|
||||
<string name="details">Деректер</string>
|
||||
<string name="grid">Тор</string>
|
||||
<string name="details">Дерек</string>
|
||||
<string name="grid">Кесте</string>
|
||||
<string name="try_again">Қайта көру</string>
|
||||
<string name="clear_history">Тарихты тазалау</string>
|
||||
<string name="read">Оқу</string>
|
||||
@@ -61,18 +61,18 @@
|
||||
<string name="text_file_sizes">Б|кБ|МБ|ГБ|ТБ</string>
|
||||
<string name="standard">Стандарт</string>
|
||||
<string name="webtoon">Уебтүн</string>
|
||||
<string name="grid_size">Тор өлшемі</string>
|
||||
<string name="grid_size">Кесте өлшемі</string>
|
||||
<string name="search_on_s">%s-те іздеу</string>
|
||||
<string name="delete_manga">Маңганы жою</string>
|
||||
<string name="reader_settings">Оқыманы баптау</string>
|
||||
<string name="switch_pages">Бет ауыстыру</string>
|
||||
<string name="switch_pages">Парақтау</string>
|
||||
<string name="taps_on_edges">Шет жақты түру</string>
|
||||
<string name="clear_thumbs_cache">Нобай кәшін тазалау</string>
|
||||
<string name="search_history_cleared">Тазаланды</string>
|
||||
<string name="gestures_only">Ым ғана</string>
|
||||
<string name="internal_storage">Ішкі жады</string>
|
||||
<string name="external_storage">Сыртқы жады</string>
|
||||
<string name="domain">Домен</string>
|
||||
<string name="domain">Дәмейін</string>
|
||||
<string name="app_update_available">Жаңа нұсқа қолжетімді</string>
|
||||
<string name="open_in_browser">Уеб браузер арқылы ашу</string>
|
||||
<string name="save_manga">Сақтау</string>
|
||||
@@ -92,7 +92,7 @@
|
||||
<string name="text_local_holder_primary">Алдымен бірдеңе сақтаңыз</string>
|
||||
<string name="manga_shelf">Сөре</string>
|
||||
<string name="recent_manga">Соңғы</string>
|
||||
<string name="pages_animation">Бет анимациясы</string>
|
||||
<string name="pages_animation">Бет анимасасы</string>
|
||||
<string name="manga_save_location">Жүктеу бумасы</string>
|
||||
<string name="not_available">Қолжетімсіз</string>
|
||||
<string name="cannot_find_available_storage">Қолжетімді бума жоқ</string>
|
||||
@@ -208,4 +208,316 @@
|
||||
<string name="protect_application_subtitle">Қолданбаға кіру үшін құпиясөз енгізіңіз</string>
|
||||
<string name="confirm">Растау</string>
|
||||
<string name="password_length_hint">Құпиясөзде 4, не одан көп таңба болу керек</string>
|
||||
<string name="status_re_reading">Қайталап оқып жатырмын</string>
|
||||
<string name="detect_reader_mode">Оқу режімін өздігінен анықтау</string>
|
||||
<string name="tracking">Бақылау</string>
|
||||
<string name="email_enter_hint">Жалғастыру үшін email поштаңызды жазыңыз</string>
|
||||
<string name="disable_all">Бәрін өшіру</string>
|
||||
<string name="clear_feed">Лекті тазалау</string>
|
||||
<string name="chapters_empty">Бұл маңгада тарау жоқ</string>
|
||||
<string name="clear_all_history">Түгел тарихты тазалау</string>
|
||||
<string name="preload_pages">Беттерді алдынала жүктей беру</string>
|
||||
<string name="data_deletion">Деректі жою</string>
|
||||
<string name="show_reading_indicators">Оқу прогрессін көрсету</string>
|
||||
<string name="local_manga_processing">Сақталған маңгаңыз үдерісте</string>
|
||||
<string name="show_notification_new_chapters_off">Мәлімдеме алмайсыз, бірақ жаңа тараулар тізімде көрсетіліп тұрады</string>
|
||||
<string name="show_notification_new_chapters_on">Оқып жүрген маңгаңыздың жаңаруы туралы мәлімдеме алып тұрасыз</string>
|
||||
<string name="status_reading">Оқып жүрмін</string>
|
||||
<string name="various_languages">Түрлі тіл</string>
|
||||
<string name="removal_completed">Жойылды</string>
|
||||
<string name="edit">Өңдеу</string>
|
||||
<string name="filter_load_error">Жанр тізімі жүктеліне алмады</string>
|
||||
<string name="removed_from_history">Тарихтан жойылды</string>
|
||||
<string name="crash_text">Ақау пайда болды. Жөндеу үшін әзірлеушіге шағым жіберіңізші.</string>
|
||||
<string name="detect_reader_mode_summary">Маңга уебтүн бе екенін өздігінен анықтап береді</string>
|
||||
<string name="appwidget_recent_description">Соңғы оқыған маңгаңыз</string>
|
||||
<string name="appearance">Кейіп</string>
|
||||
<string name="bookmark_remove">Бетбелгіні алып тастау</string>
|
||||
<string name="disable_battery_optimization_summary">Аяда жаңарту іздеуді көмектеседі</string>
|
||||
<string name="status_on_hold">Кейінге қалған</string>
|
||||
<string name="last_2_hours">Соңғы 2 сағат</string>
|
||||
<string name="name">Атау</string>
|
||||
<string name="edit_category">Санатты өңдеу</string>
|
||||
<string name="bookmark_removed">Бетбелгі жойылды</string>
|
||||
<string name="select_range">Ауқымын таңдау</string>
|
||||
<string name="suggestions_excluded_genres_summary">Көргіңіз келмейтін жанрды таңдаңыз</string>
|
||||
<string name="only_using_wifi">Тек Wi-Fi арқылы</string>
|
||||
<string name="back">Кері</string>
|
||||
<string name="dns_over_https">HTTPS үстінен DNS</string>
|
||||
<string name="sync_title">Дерегіңізді үйлестіріңіз</string>
|
||||
<string name="appwidget_shelf_description">Таңдаулы маңгаңыз</string>
|
||||
<string name="send">Жіберу</string>
|
||||
<string name="bookmark_add">Бетбелгілеу</string>
|
||||
<string name="new_sources_text">Жаңа маңга дереккөзі қолжетімді</string>
|
||||
<string name="check_new_chapters_title">Жаңа тарау барын тексеріп, сол туралы мәлімдеу</string>
|
||||
<string name="logged_in_as">%s деп тіркелгенсіз</string>
|
||||
<string name="suggestions_info">Деректің бәрі тек осы құрылғы аясында қаралып, ешқайда жіберілмейді.</string>
|
||||
<string name="history_cleared">Тарих тазарды</string>
|
||||
<string name="undo">Қайтару</string>
|
||||
<string name="download_slowdown_summary">IP мекенжайыңыздың бұғатқа түспеуіне көмектеседі</string>
|
||||
<string name="text_delete_local_manga_batch">Таңдалғанды құрылғыдан жоямысыз\?</string>
|
||||
<string name="report">Шағым</string>
|
||||
<string name="download_slowdown">Жүктеп алуды баяулату</string>
|
||||
<string name="bookmark_added">Бетбелгі қойылды</string>
|
||||
<string name="sync">Үйлестіру</string>
|
||||
<string name="search_chapters">Тарау табу</string>
|
||||
<string name="always">Әрқашан</string>
|
||||
<string name="suggestions_excluded_genres">Жанрды шектеу</string>
|
||||
<string name="canceled">Доғарылды</string>
|
||||
<string name="account_already_exists">Бұндай тіркелгі бос емес</string>
|
||||
<string name="hide">Жасыру</string>
|
||||
<string name="exclude_nsfw_from_history_summary">ҰЯТСЫЗ деген маңганы оқығаныңыз тарихыңызда сақталмайды</string>
|
||||
<string name="show_reading_indicators_summary">Таңдаулы мен тарихта оқылған туынды пайызын көрсету</string>
|
||||
<string name="use_fingerprint">Қолжетімді болса саусақ ізін қолдану</string>
|
||||
<string name="onboard_text">Маңганы қай тілде оқығыңыз келетінін таңдаңыз. Кейінірек баптауда өзгертіп ала аласыз.</string>
|
||||
<string name="suggestions_updating">Ұсынысты жаңарту</string>
|
||||
<string name="percent_string_pattern">%%%1$s</string>
|
||||
<string name="chapters_will_removed_background">Тараулар аяда жойылады</string>
|
||||
<string name="default_mode">Әдепкі режім</string>
|
||||
<string name="logout">Шығу</string>
|
||||
<string name="status_completed">Аяқталған</string>
|
||||
<string name="reset_filter">Сүзгіні тазарту</string>
|
||||
<string name="status_dropped">Тастап кеткен</string>
|
||||
<string name="nsfw">18+</string>
|
||||
<string name="notifications_enable">Мәлімдеме қосу</string>
|
||||
<string name="never">Ешқашан</string>
|
||||
<string name="disable_battery_optimization">Қуат оңтайлығын өшіру</string>
|
||||
<string name="status_planned">Жоспарланған</string>
|
||||
<string name="bookmarks">Бетбелгілер</string>
|
||||
<string name="show_all">Бәрін көрсету</string>
|
||||
<string name="empty_favourite_categories">Таңдаулы санатыңыз жоқ</string>
|
||||
<string name="invalid_domain_message">Қате дәмейін</string>
|
||||
<string name="languages">Тілдер</string>
|
||||
<string name="zoom_in">Ұлғайту</string>
|
||||
<string name="captcha_required_summary">%s дұрыс істеуі үшін captcha өтіңіз</string>
|
||||
<string name="download_option_all_unread">Түгел оқылмаған тарау</string>
|
||||
<string name="restore_backup_description">Алдында жасалған жеке деректің сақтық көшірмесін импорттау</string>
|
||||
<string name="frequency_every_day">Күнде</string>
|
||||
<string name="download_started">Жүктеп алу басталды</string>
|
||||
<string name="categories">Санат</string>
|
||||
<string name="progress">Прогресс</string>
|
||||
<string name="cancel_all">Бәрін доғару</string>
|
||||
<string name="sync_host_description">Өзіңіздің үйлестіру сербірін я әдепкісін таңдай аласыз. Не істеп жатқаныңызды түсінбеңіз баспаңыз.</string>
|
||||
<string name="error_corrupted_file">Қайта жарамсыз дерек қосылды яки файыл сынған</string>
|
||||
<string name="pick_custom_directory">Жеке каталогты таңдау</string>
|
||||
<string name="no_chapters">Тарау жоқ</string>
|
||||
<string name="list_options">Тізімді реттеу</string>
|
||||
<string name="related_manga_summary">Байланыс маңга тізімін көрсету. Кейде тізім қате я мүлде болмауы мүмкін</string>
|
||||
<string name="remove_completed_downloads_confirm">Жүктеу тарихыңыз тазарады</string>
|
||||
<string name="theme_name_dynamic">Динамикалық</string>
|
||||
<string name="reader_zoom_buttons_summary">Ұлғайту батырмасын астыңғы оң жақта көрсету я көрсетпеу</string>
|
||||
<string name="pages_cache">Бет кәші</string>
|
||||
<string name="reset">Арылту</string>
|
||||
<string name="tracker_wifi_only_summary">Шектеулі желі қосылымы болса жаңа тарау қолжетімдігін тексермеу</string>
|
||||
<string name="order_added">Қосылды</string>
|
||||
<string name="enable_logging">Логтауды қосу</string>
|
||||
<string name="on_device">Құрылғыда</string>
|
||||
<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="backup_frequency">Сақтық көшірмесінің жиілігі</string>
|
||||
<string name="data_and_privacy">Дерек пен құпиялық</string>
|
||||
<string name="clear_cookies_summary">Қате болса көмектесе алады. Түгел тіркелгінің күші жойылады</string>
|
||||
<string name="enable_logging_summary">Кей әрекетті түзеуге сақтау қою. Не екенін білмесеңіз қоспаңыз</string>
|
||||
<string name="clear_source_cookies_summary">Осы дәмейіннің кукиін тазалау. Көп жағдайда тіркелгіден шығылып кетеді</string>
|
||||
<string name="history_shortcuts">Соңғы маңганың таңбашасын көрсету</string>
|
||||
<string name="downloads_wifi_only_summary">Ұялы желіге көшкенде жүктеп алуды тоқтату</string>
|
||||
<string name="suggest_new_sources">Қолданбаны жаңартқан соң жаңа дереккөз ұсыну</string>
|
||||
<string name="import_completed">Импорт аяқталды</string>
|
||||
<string name="different_languages">Әртүрлі тіл</string>
|
||||
<string name="user_agent">UserAgent басы</string>
|
||||
<string name="ignore_ssl_errors">SSL қатеге мән бермеу</string>
|
||||
<string name="reader_info_bar">Оқымада ақпар тақтасын көрсету</string>
|
||||
<string name="periodic_backups_enable">Сақтық көшірмесін кезең-кезеңімен жасауды қосу</string>
|
||||
<string name="server_address">Сербір адресі</string>
|
||||
<string name="moved_to_top">Үстіге жылжыды</string>
|
||||
<string name="explore">Қарау</string>
|
||||
<string name="find_similar">Ұқсасын табу</string>
|
||||
<string name="storage_usage">Жадты қолдану</string>
|
||||
<string name="data_not_restored_text">Дұрыс сақтық көшірме файылын таңдағаныңызды тексеріңіз</string>
|
||||
<string name="theme_name_sakura">Сакура</string>
|
||||
<string name="view_list">Тізімді көру</string>
|
||||
<string name="unknown">Белгісіз</string>
|
||||
<string name="in_progress">Оқылып жатыр</string>
|
||||
<string name="download_option_manual_selection">Тарауды таңдап шығу</string>
|
||||
<string name="enhanced_colors_summary">Түс ыдырауын азайтады, бірақ өнімділікке әсер ете алады</string>
|
||||
<string name="importing_manga">Маңга импорттау</string>
|
||||
<string name="pause">Үзіліс</string>
|
||||
<string name="clear_new_chapters_counters">Жаңа тарау ақпарын да жою</string>
|
||||
<string name="nothing_here">Мұнда түк жоқ</string>
|
||||
<string name="remove_completed">Дайынын жою</string>
|
||||
<string name="items_limit_exceeded">Елемент сыймайды</string>
|
||||
<string name="frequency_every_2_days">Екі күн сайын</string>
|
||||
<string name="suggestions_notifications_summary">Анда-санда мәлімдеме арқылы маңга ұсынып тұру</string>
|
||||
<string name="invalid_value_message">Жарамсыз өлшем</string>
|
||||
<string name="downloads_cancelled">Жүктеп алу доғарылды</string>
|
||||
<string name="webtoon_zoom">Уебтүнді ұлғайту</string>
|
||||
<string name="theme_name_miku">Мику</string>
|
||||
<string name="data_not_restored">Дерек қалыпқа келмеді</string>
|
||||
<string name="directories">Каталог</string>
|
||||
<string name="local_manga_directories">Жергілікті маңга тізімі</string>
|
||||
<string name="manage_categories">Санатты реттеу</string>
|
||||
<string name="scrobbling_empty_hint">Оқу прогрессін бақылау үшін маңга ақпар экранындағы «Мәзірге» өтіп, «Бақылау» батырмасын басыңыз.</string>
|
||||
<string name="color_light">Жарық</string>
|
||||
<string name="web_view_unavailable">WebView қолжетімсіз: WebView провайдері орнатылғанын тексеріп көріңіз</string>
|
||||
<string name="port">Порт</string>
|
||||
<string name="color_correction_hint">Таңдалған түс баптауы осы маңга үшін сақталып тұрады</string>
|
||||
<string name="not_found_404">Контент табылмады, жойылған-мыс</string>
|
||||
<string name="got_it">Ұқтым</string>
|
||||
<string name="type">Түрі</string>
|
||||
<string name="search_hint">Маңга атын, жанрын я дереккөз атауын жазыңыз</string>
|
||||
<string name="frequency_once_per_week">Апта сайын</string>
|
||||
<string name="description">Сипаттама</string>
|
||||
<string name="periodic_backups">Сақтық көшірмесін кезең-кезеңімен жасау</string>
|
||||
<string name="reader_zoom_buttons">Ұлғайту батырмасын көрсету</string>
|
||||
<string name="sources_reorder_tip">Ретін өзгерту үшін елементті басып тұрыңыз</string>
|
||||
<string name="resume">Жалғастыру</string>
|
||||
<string name="server_error">Сербір қуып тұр (%1$d). Сәлден соң қайталап көріңіз</string>
|
||||
<string name="images_proxy_title">Суретті оңтайлау проксиі</string>
|
||||
<string name="network_unavailable_hint">Маңганы онлайн оқу үшін Wi-Fi-ды я ұялы желіні қосыңыз</string>
|
||||
<string name="no_manga_sources">Маңға дереккөзі жоқ</string>
|
||||
<string name="username">Қолданушы аты</string>
|
||||
<string name="frequency_twice_per_month">Екі апта сайын</string>
|
||||
<string name="prefetch_content">Алдын-ала жүктей беру</string>
|
||||
<string name="main_screen_sections">Басты экран бөлімдері</string>
|
||||
<string name="confirm_exit">Шығу үшін тағы бір рет басыңыз</string>
|
||||
<string name="advanced">Қосымша баптау</string>
|
||||
<string name="sync_settings">Үйлестіру баптауы</string>
|
||||
<string name="online_variant">Онлайн нұсқа</string>
|
||||
<string name="download_option_all_unread_b">Түгел оқылмаған тарау (%s)</string>
|
||||
<string name="authorization_optional">Кіру (міндетті емес)</string>
|
||||
<string name="color_dark">Күңгірт</string>
|
||||
<string name="other_cache">Басқа кәш</string>
|
||||
<string name="show_suspicious_content">Күмәнді контентті көрсету</string>
|
||||
<string name="reader_info_bar_summary">Экранның үстінгі жағында уақыт пен оқу прогрессін көрсету</string>
|
||||
<string name="allow_unstable_updates_summary">Тұрақсыз нұсқа туралы мәлімдеме алу</string>
|
||||
<string name="translations">Аударма</string>
|
||||
<string name="comics_archive_import_description">Бір не одан көп .cbz я .zip файлын таңдай аласыз, әр файыл бөлек маңга болып анықталады.</string>
|
||||
<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="by_relevance">Өзектілігі</string>
|
||||
<string name="related_manga">Ұқсас маңга</string>
|
||||
<string name="discard">Сақтамау</string>
|
||||
<string name="saved_manga">Сақталған маңга</string>
|
||||
<string name="manga_error_description_pattern">Қате мәліметі:<br><tt>%1$s</tt><br><br>1. Дереккөзде бар ма екенін тексеру үшін <a href=%2$s>маңганы уеб браузер арқылы</a> ашып көріңіз<br>2. <a href=kotatsu://about>Kotatsu-ның ең соңғы нұсқасын</a><br>3 қолданып отырсыз ба, тексеріп көріңіз. Мүмкін болса: әзірлеушіге қате туралы шағып жіберіңіз.</string>
|
||||
<string name="state_abandoned">Тасталған</string>
|
||||
<string name="history_shortcuts_summary">Қолданба таңбасын ұзақ басып тұрғанда соңғы маңганы қолжетімді ету</string>
|
||||
<string name="automatic_scroll">Өздігінен парақтау</string>
|
||||
<string name="download_option_first_n_chapters">Бірінші %s</string>
|
||||
<string name="keep_screen_on">Экранды сөндірмеу</string>
|
||||
<string name="paused">Тоқтап тұр</string>
|
||||
<string name="text_downloads_list_holder">Жүктеп алғаныңыз жоқ</string>
|
||||
<string name="color_correction">Түс реттеу</string>
|
||||
<string name="invalid_port_number">Порттың нөмірі қате</string>
|
||||
<string name="suggestions_wifi_only_summary">Шектеулі желі қосылымы болса ұсынысты жаңартпау</string>
|
||||
<string name="webtoon_zoom_summary">Уебтүн режімінде ұлғайту ымына рұқсат ету</string>
|
||||
<string name="categories_delete_confirm">Таңдаулы санатты шынымен жойғыңыз келе ме\?
|
||||
\nІшіндегі бар маңга жойылады, сосын оны қайтарып ала алмайсыз.</string>
|
||||
<string name="frequency_once_per_month">Ай сайын</string>
|
||||
<string name="reader_info_pattern">%1$d/%2$d-тарау %3$d/%4$d-бет</string>
|
||||
<string name="contrast">Көреғарлық</string>
|
||||
<string name="network">Желі</string>
|
||||
<string name="reader_slider">Парақтау жүгірткісін көрсету</string>
|
||||
<string name="no_manga_sources_text">Онлайн оқу үшін маңга дереккөзін қосыңыз</string>
|
||||
<string name="downloaded">Жүктеп алынған</string>
|
||||
<string name="options">Басқа</string>
|
||||
<string name="services">Қызмет</string>
|
||||
<string name="suggestions_enable_prompt">Сізге арналған маңга ұсынысын алғыңыз келе ме\?</string>
|
||||
<string name="exit_confirmation">Шығуды растау</string>
|
||||
<string name="comics_archive">Комикс мұрағаты</string>
|
||||
<string name="custom_directory">Жеке каталог</string>
|
||||
<string name="more">Тағы</string>
|
||||
<string name="theme_name_asuka">Асука</string>
|
||||
<string name="address">Адресі</string>
|
||||
<string name="import_will_start_soon">Импорт қазір басталады</string>
|
||||
<string name="compact">Жинақы</string>
|
||||
<string name="enhanced_colors">32-биттік түс режімі</string>
|
||||
<string name="folder_with_images_import_description">Мұрағат я сурет каталогын таңдай аласыз. Әр мұрағат (яки ішкі каталог) бір тарау деп анықталады.</string>
|
||||
<string name="reorder">Ретін өзгерту</string>
|
||||
<string name="default_section">Әдепкі бөлім</string>
|
||||
<string name="background">Ая</string>
|
||||
<string name="feed">Лек</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="downloads_removed">Жүктеп алғаныңыз жойылды</string>
|
||||
<string name="pages_animation_summary">Парақтау анимасасы</string>
|
||||
<string name="no_access_to_file">Бұл файлға я каталогқа рұқсатыңыз жоқ</string>
|
||||
<string name="mark_as_current">Қазіргі деп белгілеу</string>
|
||||
<string name="random">Кездейсоқ</string>
|
||||
<string name="mirror_switching">Айнаны өздігінен таңдау</string>
|
||||
<string name="restore_summary">Бұған дейін жасалған сақтық көшірмесін қалпына келтіру</string>
|
||||
<string name="show_pages_numbers_summary">Бет нөмірін төменгі жақта көрсету</string>
|
||||
<string name="show_in_grid_view">Кесте қып көрсету</string>
|
||||
<string name="zoom_out">Кішірейту</string>
|
||||
<string name="keep_screen_on_summary">Маңга оқып отырғанда экранды сөндірмеу</string>
|
||||
<string name="download_option_next_unread_n_chapters">Келесі оқылмаған %s</string>
|
||||
<string name="clear_network_cache">Желі кәшін тазалау</string>
|
||||
<string name="voice_search">Дауыс іздеуі</string>
|
||||
<string name="enable">Қосу</string>
|
||||
<string name="import_completed_hint">Орын үнемдеу үшін түпнұсқа файлды құрылғыдан жойып тастай аласыз</string>
|
||||
<string name="theme_name_rikka">Рикка</string>
|
||||
<string name="reader_control_ltr_summary">Оң жақты я оң жақ батырманы басқан сайын бет ауысады</string>
|
||||
<string name="language">Тіл</string>
|
||||
<string name="incognito_mode">Инкогнито режімі</string>
|
||||
<string name="no_bookmarks_summary">Оқып жатқан маңгаға бетбелгі жасай аласыз</string>
|
||||
<string name="images_procy_description">Трафик қолдануды азайтып, сурет жүктеп алуды тездету үшін wsrv.nl қызметін қолданыңыз</string>
|
||||
<string name="theme_name_mamimi">Мамими</string>
|
||||
<string name="manga_branch_title_template">%1$s (%2$s)</string>
|
||||
<string name="manage">Реттеу</string>
|
||||
<string name="manga_list">Маңга тізімі</string>
|
||||
<string name="reader_control_ltr">Оқыманы эргономді басқару</string>
|
||||
<string name="disable_nsfw">ҰЯТСЫЗ-ды өшіру</string>
|
||||
<string name="last_successful_backup">Соңғы сақтық көшірме: %s</string>
|
||||
<string name="available">Қолжетімді</string>
|
||||
<string name="color_white">Ақ</string>
|
||||
<string name="network_unavailable">Желі қолжетімсіз</string>
|
||||
<string name="empty">Бос</string>
|
||||
<string name="downloads_resumed">Жүктеп алу жалғасты</string>
|
||||
<string name="details_button_tip">Қосымша реттеу көру үшін «Оқу» батырмасын басып тұрыңыз</string>
|
||||
<string name="text_unsaved_changes_prompt">Өзгерісті сақтайсыз ба\?</string>
|
||||
<string name="folder_with_images">Суреті бар бума</string>
|
||||
<string name="to_top">Үстіге</string>
|
||||
<string name="show">Көрсету</string>
|
||||
<string name="show_on_shelf">Сөреде көрсету</string>
|
||||
<string name="allow_unstable_updates">Тұрақсыз жаңартуды орнатуға рұқсат беру</string>
|
||||
<string name="sync_auth_hint">Жаңа тіркелгі аша аласыз я жаңасын жасай аласыз</string>
|
||||
<string name="theme_name_kanade">Канаде</string>
|
||||
<string name="backups_output_directory">Сақтық көшірмесінің каталогы</string>
|
||||
<string name="invert_colors">Түстерді терістету</string>
|
||||
<string name="color_theme">Түс схемасы</string>
|
||||
<string name="brightness">Ашықтық</string>
|
||||
<string name="memory_usage_pattern">%s - %s</string>
|
||||
<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="no_bookmarks_yet">Бетбелгі жоқ</string>
|
||||
<string name="no_thanks">Жоқ, рақмет</string>
|
||||
<string name="suggest_new_sources_summary">Қолданбаның жаңа нұсқасында пайда болған дереккөзді ұсыну</string>
|
||||
<string name="share_logs">Логты бөлісу</string>
|
||||
<string name="speed">Жылдамдық</string>
|
||||
<string name="download_option_all_chapters">%s аударған түгел тарау</string>
|
||||
<string name="suggestion_manga">Ұсыныс: %s</string>
|
||||
<string name="color_black">Қара</string>
|
||||
<string name="removed_from_favourites">Таңдаулыдан жойылды</string>
|
||||
<string name="this_month">Осы ай</string>
|
||||
<string name="proxy">Прокси</string>
|
||||
<string name="error_no_space_left">Жадта бос орын қалмады</string>
|
||||
<string name="sources_catalog">Дереккөз каталогы</string>
|
||||
<string name="content_type_manga">Маңга</string>
|
||||
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||
<string name="content_type_hentai">Хентай</string>
|
||||
<string name="content_type_comics">Комикс</string>
|
||||
<string name="catalog">Каталог</string>
|
||||
<string name="manage_sources">Маңга дереккөзі</string>
|
||||
<string name="no_manga_sources_found">Іздеуіңіз бойынша дереккөз табылмады</string>
|
||||
<string name="lock_screen_rotation">Экран бұрылуын бұғаттау</string>
|
||||
<string name="manual">Қолмен реттеу</string>
|
||||
<string name="source_enabled">Дереккөз қосылған-ды</string>
|
||||
<string name="disable_nsfw_summary">ҰЯТСЫЗ маңга дереккөзін өшіріп, тізімнен жасырып тастау</string>
|
||||
<string name="no_manga_sources_catalog_text">Әзірге мына жерде қолжетімді дереккөз жоқ. Жаңарту күтіңіз</string>
|
||||
<string name="available_d">Қолжетімді: %1$d</string>
|
||||
<string name="content_type_other">Басқа</string>
|
||||
</resources>
|
||||
@@ -483,4 +483,25 @@
|
||||
<string name="show">Mostrar</string>
|
||||
<string name="color_black">Preto</string>
|
||||
<string name="this_month">Esse mês</string>
|
||||
<string name="frequency_every_day">Diariamente</string>
|
||||
<string name="categories">Categorias</string>
|
||||
<string name="list_options">Opções da lista</string>
|
||||
<string name="backup_frequency">Frequência de criação de backup</string>
|
||||
<string name="suggest_new_sources">Sugerir novas fontes após a atualização do aplicativo</string>
|
||||
<string name="periodic_backups_enable">Ativar backups periódicos</string>
|
||||
<string name="enhanced_colors_summary">Reduz a formação de faixas, mas pode afetar o desempenho</string>
|
||||
<string name="frequency_every_2_days">A cada 2 dias</string>
|
||||
<string name="frequency_once_per_week">Semanalmente</string>
|
||||
<string name="periodic_backups">Backups periódicos</string>
|
||||
<string name="frequency_twice_per_month">Duas vezes ao mês</string>
|
||||
<string name="online_variant">Variante on-line</string>
|
||||
<string name="by_relevance">Relevância</string>
|
||||
<string name="state_abandoned">Abandonado</string>
|
||||
<string name="keep_screen_on">Manter a tela ligada</string>
|
||||
<string name="frequency_once_per_month">Uma vez por mês</string>
|
||||
<string name="enhanced_colors">Modo de cor de 32 bits</string>
|
||||
<string name="keep_screen_on_summary">Não desligue a tela enquanto estiver lendo mangá</string>
|
||||
<string name="last_successful_backup">Último backup bem-sucedido: %s</string>
|
||||
<string name="backups_output_directory">Local de saída de backups</string>
|
||||
<string name="suggest_new_sources_summary">Solicitação para ativar fontes recém-adicionadas após a atualização do aplicativo</string>
|
||||
</resources>
|
||||
@@ -504,4 +504,19 @@
|
||||
<string name="frequency_once_per_month">Один раз в месяц</string>
|
||||
<string name="backups_output_directory">Каталог для сохранения резервных копий</string>
|
||||
<string name="last_successful_backup">Последняя резервная копия: %s</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="lock_screen_rotation">Блокировка поворота экрана</string>
|
||||
<string name="sources_catalog">Каталог источников</string>
|
||||
<string name="content_type_manga">Манга</string>
|
||||
<string name="content_type_hentai">Хентай</string>
|
||||
<string name="content_type_comics">Комиксы</string>
|
||||
<string name="catalog">Каталог</string>
|
||||
<string name="manage_sources">Управление источниками</string>
|
||||
<string name="no_manga_sources_found">Не найдено доступных источников по вашему запросу</string>
|
||||
<string name="manual">Вручную</string>
|
||||
<string name="source_enabled">Источник включен</string>
|
||||
<string name="disable_nsfw_summary">Отключить NSFW источники и скрывать мангу для взрослых в списках, если это возможно</string>
|
||||
<string name="no_manga_sources_catalog_text">Пока что в данном разделе нет доступных источников. Следите за обновлениями</string>
|
||||
<string name="available_d">Доступно: %1$d</string>
|
||||
<string name="content_type_other">Другое</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="light">สว่าง</string>
|
||||
<string name="automatic">ตั้งค่าตามเครื่อง</string>
|
||||
<string name="text_clear_history_prompt">จะเคลียร์ประวัติการอ่านทั้งหมดแบบถาวรใช่ไหม\?</string>
|
||||
@@ -160,7 +160,7 @@
|
||||
<string name="zoom_mode_fit_center">พอดีตรงกลาง</string>
|
||||
<string name="zoom_mode_fit_height">พอดีกับความสูง</string>
|
||||
<string name="black_dark_theme">ดำ</string>
|
||||
<string name="just_now">เมื่อกี้</string>
|
||||
<string name="just_now">เมื่อเร็วนี้</string>
|
||||
<string name="clear_feed">เคลียร์ฟีด</string>
|
||||
<string name="backup_restore">สำรองและคืนค่า</string>
|
||||
<string name="create_backup">สร้างข้อมูลสำรอง</string>
|
||||
@@ -344,4 +344,29 @@
|
||||
<string name="exit_confirmation_summary">กดย้อนกลับสองครั้งเพื่อออกจากแอป</string>
|
||||
<string name="bookmarks_removed">ลบบุ๊คมาร์กแล้ว</string>
|
||||
<string name="error_no_space_left">ไม่มีพื้นที่เหลือบนอุปกรณ์แล้ว</string>
|
||||
<string name="zoom_in">ซูมเข้า</string>
|
||||
<string name="detect_reader_mode">โหมดตรวจจับเครื่องอ่านอัตโนมัติ</string>
|
||||
<string name="frequency_every_day">ทุกวัน</string>
|
||||
<string name="categories">หมวดหมู่</string>
|
||||
<string name="backup_frequency">ความถี่การสำรองข้อมูล</string>
|
||||
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d of %2$d on</string>
|
||||
<string name="unknown">ไม่ทราบ</string>
|
||||
<string name="frequency_every_2_days">ทุก 2 วัน</string>
|
||||
<string name="frequency_once_per_week">สัปดาห์ละครั้ง</string>
|
||||
<string name="reader_zoom_buttons">โชว์ปุ่มซูม</string>
|
||||
<string name="select_range">เลือกช่วง</string>
|
||||
<string name="no_manga_sources">ไม่พบแหล่งมังงะ</string>
|
||||
<string name="frequency_twice_per_month">สองครั้งต่อเดือน</string>
|
||||
<string name="keep_screen_on">เปิดหน้าจอไว้ตลอด</string>
|
||||
<string name="categories_delete_confirm">คุณแน่ใจหรือไม่ว่าต้องการลบหมวดหมู่รายการโปรดที่เลือก
|
||||
\nมังงะทั้งหมดในนั้นจะหายไปและไม่สามารถยกเลิกได้</string>
|
||||
<string name="frequency_once_per_month">เดือนละครั้ง</string>
|
||||
<string name="no_manga_sources_text">เปิดใช้งานแหล่งมังงะเพื่ออ่านมังงะออนไลน์</string>
|
||||
<string name="enhanced_colors">โหมดสี 32 บิต</string>
|
||||
<string name="background">พื้นหลัง</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="use_fingerprint">ใช้สแกนลายนิ้วมือหากมี</string>
|
||||
<string name="zoom_out">ซูมออก</string>
|
||||
<string name="text_search_holder_secondary">โปรดลองเรียบเรียงคำค้นหา</string>
|
||||
<string name="last_successful_backup">สำรองข้อมูลสำเร็จล่าสุด: %s</string>
|
||||
</resources>
|
||||
@@ -503,4 +503,6 @@
|
||||
<string name="frequency_twice_per_month">Ayda 2 kere</string>
|
||||
<string name="frequency_once_per_month">Ayda 1 kere</string>
|
||||
<string name="backups_output_directory">Yedekleme dizini</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="last_successful_backup">Son başarılı yedekleme: %s</string>
|
||||
</resources>
|
||||
@@ -494,4 +494,16 @@
|
||||
<string name="enhanced_colors">32-бітний колірний режим</string>
|
||||
<string name="suggest_new_sources_summary">Пропонує включити нові джерела манґи після оновлення застосунку</string>
|
||||
<string name="online_variant">Онлайн варіант</string>
|
||||
<string name="frequency_every_day">Кожен день</string>
|
||||
<string name="backup_frequency">Частота резервного копіювання</string>
|
||||
<string name="periodic_backups_enable">Увімкніть періодичне резервне копіювання</string>
|
||||
<string name="frequency_every_2_days">Кожні 2 дні</string>
|
||||
<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="last_successful_backup">Останнє успішне резервне копіювання: %s</string>
|
||||
<string name="backups_output_directory">Вихідний каталог резервних копій</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="lock_screen_rotation">Блокування повороту екрану</string>
|
||||
</resources>
|
||||
@@ -17,16 +17,16 @@
|
||||
<string name="silent">无声</string>
|
||||
<string name="preparing_">准备…</string>
|
||||
<string name="file_not_found">未找到文件</string>
|
||||
<string name="yesterday">昨日</string>
|
||||
<string name="yesterday">昨天</string>
|
||||
<string name="backup_information">你可以创建你的历史和收藏的备份并恢复它</string>
|
||||
<string name="just_now">刚刚</string>
|
||||
<string name="long_ago">很久以前</string>
|
||||
<string name="group">分组</string>
|
||||
<string name="tap_to_try_again">轻击重试</string>
|
||||
<string name="tap_to_try_again">点击重试</string>
|
||||
<string name="reader_mode_hint">所选配置将被这部漫画记住</string>
|
||||
<string name="captcha_required">需要验证码</string>
|
||||
<string name="captcha_solve">解决</string>
|
||||
<string name="today">今日</string>
|
||||
<string name="today">今天</string>
|
||||
<string name="clear_cookies">清除cookies</string>
|
||||
<string name="new_sources_text">有新的漫画源可用</string>
|
||||
<string name="suggestions_summary">根据你的喜好推荐漫画</string>
|
||||
@@ -79,7 +79,7 @@
|
||||
<string name="clear">清除</string>
|
||||
<string name="text_clear_history_prompt">永久清除所有阅读历史\?</string>
|
||||
<string name="remove">删除</string>
|
||||
<string name="_s_deleted_from_local_storage">\"%s\"从本地存储中删除</string>
|
||||
<string name="_s_deleted_from_local_storage">已从本地存储中删除“%s”</string>
|
||||
<string name="save_page">保存页面</string>
|
||||
<string name="page_saved">保存</string>
|
||||
<string name="share_image">分享图片</string>
|
||||
@@ -110,8 +110,8 @@
|
||||
<string name="internal_storage">内部存储</string>
|
||||
<string name="external_storage">外部存储</string>
|
||||
<string name="domain">范围</string>
|
||||
<string name="app_update_available">新版本应用程序已经推出</string>
|
||||
<string name="open_in_browser">在网络浏览器中打开</string>
|
||||
<string name="app_update_available">发现新版本</string>
|
||||
<string name="open_in_browser">在浏览器中打开</string>
|
||||
<string name="large_manga_save_confirm">这部漫画有 %s 。全部保存?</string>
|
||||
<string name="save_manga">保存</string>
|
||||
<string name="notifications">通知</string>
|
||||
@@ -122,7 +122,7 @@
|
||||
<string name="light_indicator">LED指示器</string>
|
||||
<string name="vibration">振动</string>
|
||||
<string name="favourites_categories">收藏分类</string>
|
||||
<string name="remove_category">删除</string>
|
||||
<string name="remove_category">移除</string>
|
||||
<string name="text_empty_holder_primary">这里有点空…</string>
|
||||
<string name="text_search_holder_secondary">尝试重新表述查询。</string>
|
||||
<string name="text_history_holder_primary">你看过的内容将在这里显示</string>
|
||||
@@ -146,9 +146,9 @@
|
||||
<string name="new_version_s">新版本: %s</string>
|
||||
<string name="clear_updates_feed">清除订阅更新记录</string>
|
||||
<string name="updates_feed_cleared">已清除</string>
|
||||
<string name="rotate_screen">旋转屏幕</string>
|
||||
<string name="rotate_screen">屏幕旋转</string>
|
||||
<string name="update">更新</string>
|
||||
<string name="feed_will_update_soon">订阅更新即将开始</string>
|
||||
<string name="feed_will_update_soon">即将开始更新订阅</string>
|
||||
<string name="track_sources">查找更新</string>
|
||||
<string name="dont_check">不要检查</string>
|
||||
<string name="enter_password">输入密码</string>
|
||||
@@ -160,7 +160,7 @@
|
||||
<string name="about">关于</string>
|
||||
<string name="app_version">版本%s</string>
|
||||
<string name="check_for_updates">检查更新</string>
|
||||
<string name="no_update_available">没有更新</string>
|
||||
<string name="no_update_available">无可用更新</string>
|
||||
<string name="right_to_left">从右到左</string>
|
||||
<string name="create_category">新分类</string>
|
||||
<string name="scale_mode">缩放模式</string>
|
||||
@@ -245,7 +245,7 @@
|
||||
<string name="bookmark_added">书签已添加</string>
|
||||
<string name="undo">撤销</string>
|
||||
<string name="removed_from_history">从历史中删除</string>
|
||||
<string name="dns_over_https">DNS over HTTPS</string>
|
||||
<string name="dns_over_https">基于 HTTPS 的 DNS</string>
|
||||
<string name="default_mode">默认模式</string>
|
||||
<string name="detect_reader_mode">自动检测阅读器模式</string>
|
||||
<string name="detect_reader_mode_summary">自动检测漫画是否为条漫</string>
|
||||
@@ -369,7 +369,7 @@
|
||||
<string name="allow_unstable_updates">允许不稳定更新</string>
|
||||
<string name="allow_unstable_updates_summary">接收不稳定版本更新的通知</string>
|
||||
<string name="download_started">下载已开始</string>
|
||||
<string name="user_agent">UserAgent 标头</string>
|
||||
<string name="user_agent">UserAgent 标识</string>
|
||||
<string name="settings_apply_restart_required">要应用这些更改请重启程序</string>
|
||||
<string name="sources_reorder_tip">点击并长按项目排序</string>
|
||||
<string name="got_it">知道了</string>
|
||||
@@ -451,7 +451,7 @@
|
||||
<string name="progress">阅读进度</string>
|
||||
<string name="error_corrupted_file">无效数据回传或文件已损坏</string>
|
||||
<string name="related_manga_summary">显示相关漫画。可能并不准确或缺失</string>
|
||||
<string name="tracker_wifi_only_summary">使用计量网络时停止检查新章节</string>
|
||||
<string name="tracker_wifi_only_summary">使用按流量计费的网络时停止检查新章节</string>
|
||||
<string name="order_added">添加日期</string>
|
||||
<string name="on_device">本地</string>
|
||||
<string name="moved_to_top">移动到顶部</string>
|
||||
@@ -468,7 +468,7 @@
|
||||
<string name="advanced">高级</string>
|
||||
<string name="color_dark">深色</string>
|
||||
<string name="too_many_requests_message">请求次数过多,稍候再尝试</string>
|
||||
<string name="suggestions_wifi_only_summary">使用计量网络时停止推荐漫画</string>
|
||||
<string name="suggestions_wifi_only_summary">使用按流量计费的网络时停止推荐漫画</string>
|
||||
<string name="default_section">默认栏目</string>
|
||||
<string name="background">阅读背景色</string>
|
||||
<string name="manga_list">漫画列表</string>
|
||||
@@ -488,4 +488,17 @@
|
||||
<string name="enhanced_colors_summary">减少色带,但可能会影响性能</string>
|
||||
<string name="enhanced_colors">32位色彩模式</string>
|
||||
<string name="suggest_new_sources_summary">应用更新后快速启用新增的漫画源</string>
|
||||
<string name="frequency_every_day">每天一次</string>
|
||||
<string name="categories">分类</string>
|
||||
<string name="backup_frequency">备份频率</string>
|
||||
<string name="periodic_backups_enable">定期备份</string>
|
||||
<string name="view_list">查看列表</string>
|
||||
<string name="frequency_every_2_days">每两天一次</string>
|
||||
<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="last_successful_backup">上次备份成功:%s</string>
|
||||
<string name="backups_output_directory">备份保存路径</string>
|
||||
<string name="download_option_all_chapters">所有已翻译的章节 %s</string>
|
||||
</resources>
|
||||
@@ -510,4 +510,20 @@
|
||||
<string name="periodic_backups_enable">Enable periodic backups</string>
|
||||
<string name="backups_output_directory">Backups output directory</string>
|
||||
<string name="last_successful_backup">Last successful backup: %s</string>
|
||||
<string name="speed_value">x%.1f</string>
|
||||
<string name="lock_screen_rotation">Lock screen rotation</string>
|
||||
<string name="content_type_manga">Manga</string>
|
||||
<string name="content_type_hentai">Hentai</string>
|
||||
<string name="content_type_comics">Comics</string>
|
||||
<string name="content_type_other">Other</string>
|
||||
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||
<string name="sources_catalog">Sources catalog</string>
|
||||
<string name="source_enabled">Source enabled</string>
|
||||
<string name="no_manga_sources_catalog_text">No available sources in this section yet. Stay tuned</string>
|
||||
<string name="no_manga_sources_found">No available manga sources found by your query</string>
|
||||
<string name="catalog">Catalog</string>
|
||||
<string name="manage_sources">Manage sources</string>
|
||||
<string name="manual">Manual</string>
|
||||
<string name="available_d">Available: %1$d</string>
|
||||
<string name="disable_nsfw_summary">Disable NSFW sources and hide adult manga from list if possible</string>
|
||||
</resources>
|
||||
|
||||
@@ -46,21 +46,6 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/remote_sources">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="sources_grid"
|
||||
android:title="@string/show_in_grid_view" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="sources_new"
|
||||
android:summary="@string/suggest_new_sources_summary"
|
||||
android:title="@string/suggest_new_sources" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.nav.NavConfigFragment"
|
||||
android:key="nav_main"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user