Compare commits

...

50 Commits

Author SHA1 Message Date
cf33cb66c6 Fix release build 2023-11-23 13:25:12 +02:00
Макар Разин
8010c5079b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (524 of 524 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (524 of 524 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (524 of 524 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-11-23 12:47:59 +02:00
a87a77083e Reader fixes 2023-11-23 12:42:43 +02:00
ca20422344 Support Paused manga state 2023-11-23 12:33:48 +02:00
c213b9d4b5 Update parsers 2023-11-23 12:26:38 +02:00
95fbe496cb Search through all sources in catalog 2023-11-22 16:11:23 +02:00
b9fd2e100d Fallback to local manga on network error 2023-11-22 15:38:39 +02:00
1242a88f8e Fix disabling webtoon scale 2023-11-22 15:10:26 +02:00
55851fb22f Avoid replacing online manga wthin local in database 2023-11-18 16:03:01 +02:00
7801456d17 Enable desugaring to fit Jsoup requirements #553 2023-11-18 15:12:18 +02:00
38a1fafa26 Load local manga if not connection when possible #547 2023-11-18 13:35:12 +02:00
aa02233883 Update parsers 2023-11-18 13:35:12 +02:00
Abay Emes
5405fdb85a Translated using Weblate (Kazakh)
Currently translated at 100.0% (524 of 524 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2023-11-18 13:35:03 +02:00
InfinityDouki56
38ad7e1fd4 Translated using Weblate (Filipino)
Currently translated at 88.3% (463 of 524 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-11-18 12:31:21 +02:00
gallegonovato
06372083fd Translated using Weblate (Spanish)
Currently translated at 100.0% (524 of 524 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-11-18 12:31:21 +02:00
Isira Seneviratne
d5d3154074 Avoid accidental link clicks 2023-11-18 12:30:39 +02:00
1a279966d9 Update parsers 2023-11-14 07:54:03 +02:00
3222c2128e Translated using Weblate (Russian)
Currently translated at 99.8% (523 of 524 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-11-14 07:45:12 +02:00
gallegonovato
872c859efe Translated using Weblate (Spanish)
Currently translated at 100.0% (521 of 521 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (519 of 519 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (510 of 510 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-11-14 07:45:12 +02:00
Макар Разин
b79c00f8df Translated using Weblate (Ukrainian)
Currently translated at 100.0% (510 of 510 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (510 of 510 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (510 of 510 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-11-14 07:45:12 +02:00
e7d3d9811d Fix reader zoom buttons 2023-11-12 17:45:37 +02:00
4fdfc75833 Try fix strange crashes 2023-11-12 16:57:05 +02:00
9754ebf1bb Reduce main menu while search opened 2023-11-12 16:48:18 +02:00
fee35cceab Sources settings screen 2023-11-12 16:30:11 +02:00
b928c4123c Update explore navigation 2023-11-12 13:16:42 +02:00
b093a885c9 Sources catalog 2023-11-12 12:59:12 +02:00
dd898579c9 Option to lock reader screen rotation 2023-11-11 15:01:08 +02:00
73143d2f94 Rework favourite sheet 2023-11-11 14:40:30 +02:00
563752f6a4 Upgrade gradle 2023-11-11 12:59:16 +02:00
7135902100 Update parsers 2023-11-10 14:55:28 +02:00
Nayuki
969947ef71 Translated using Weblate (Thai)
Currently translated at 73.2% (373 of 509 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-11-10 14:48:38 +02:00
GpixeL
806e4eade6 Translated using Weblate (Indonesian)
Currently translated at 99.4% (506 of 509 strings)

Co-authored-by: GpixeL <gamesfire313@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-11-10 14:48:38 +02:00
Abay Emes
063cfbe6b9 Translated using Weblate (Kazakh)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Kazakh)

Currently translated at 100.0% (509 of 509 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/kk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-11-10 14:48:38 +02:00
InfinityDouki56
7cb94a3baa Translated using Weblate (Filipino)
Currently translated at 88.8% (452 of 509 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-11-10 14:48:38 +02:00
Oğuz Ersen
894c584c78 Translated using Weblate (Turkish)
Currently translated at 100.0% (509 of 509 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-11-10 14:48:38 +02:00
gallegonovato
2f65e7776a Translated using Weblate (Spanish)
Currently translated at 100.0% (509 of 509 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-11-10 14:48:38 +02:00
Макар Разин
76c56c9119 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (509 of 509 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (509 of 509 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (509 of 509 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-11-10 14:48:38 +02:00
InfinityDouki56
e0a803399c Translated using Weblate (Filipino)
Currently translated at 88.9% (452 of 508 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-11-07 18:49:34 +02:00
kenewjr
7803f42486 Translated using Weblate (Indonesian)
Currently translated at 96.4% (490 of 508 strings)

Co-authored-by: kenewjr <kenelewatan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-11-07 18:49:34 +02:00
Макар Разин
39713b3cf6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (508 of 508 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (508 of 508 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-11-07 18:49:34 +02:00
Nayuki
8ebf5cea62 Translated using Weblate (Thai)
Currently translated at 68.7% (349 of 508 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-11-07 18:49:34 +02:00
Abay Emes
663dabe218 Added translation using Weblate (Kazakh)
Translated using Weblate (Kazakh)

Currently translated at 57.4% (292 of 508 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2023-11-07 18:49:34 +02:00
Tommy12pl
3a5d0120bf Translated using Weblate (Chinese (Simplified))
Currently translated at 99.4% (505 of 508 strings)

Co-authored-by: Tommy12pl <tommy12pl@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-11-07 18:49:34 +02:00
gallegonovato
a773f932d4 Translated using Weblate (Spanish)
Currently translated at 100.0% (508 of 508 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-11-07 18:49:34 +02:00
2a5812735f Cubic reader scroll speed 2023-11-05 08:54:07 +02:00
06ec145802 Update parsers 2023-11-02 08:56:43 +02:00
6624778f7f Fix periodical backups 2023-11-02 08:50:51 +02:00
1af1f071ad Fix crashes 2023-11-01 17:25:55 +02:00
f87db4e6d3 Update dependencies 2023-11-01 16:38:10 +02:00
Crono
07bd66fb39 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (508 of 508 strings)

Co-authored-by: Crono <cronoreader@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2023-11-01 16:32:38 +02:00
107 changed files with 2169 additions and 660 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 593 versionCode = 598
versionName = '6.2.5' versionName = '6.3.1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner" testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp { ksp {
@@ -33,7 +33,6 @@ android {
applicationIdSuffix = '.debug' applicationIdSuffix = '.debug'
} }
release { release {
multiDexEnabled false
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
@@ -48,11 +47,12 @@ android {
main.java.srcDirs += 'src/main/kotlin/' main.java.srcDirs += 'src/main/kotlin/'
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 coreLibraryDesugaringEnabled true
targetCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [ freeCompilerArgs += [
'-opt-in=kotlin.ExperimentalStdlibApi', '-opt-in=kotlin.ExperimentalStdlibApi',
'-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', '-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi',
@@ -82,17 +82,18 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:4ca3a492b0') { implementation('com.github.KotatsuApp:kotatsu-parsers:c3613f3ba4') {
exclude group: 'org.json', module: 'json' 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 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.activity:activity-ktx:1.8.0' implementation 'androidx.activity:activity-ktx:1.8.1'
implementation 'androidx.fragment:fragment-ktx:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-service:2.6.2' implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
@@ -119,8 +120,8 @@ dependencies {
implementation 'androidx.room:room-ktx:2.6.0' implementation 'androidx.room:room-ktx:2.6.0'
ksp 'androidx.room:room-compiler:2.6.0' ksp 'androidx.room:room-compiler:2.6.0'
implementation 'com.squareup.okhttp3:okhttp:4.11.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
implementation 'com.squareup.okio:okio:3.6.0' implementation 'com.squareup.okio:okio:3.6.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
@@ -128,11 +129,11 @@ dependencies {
implementation 'com.google.dagger:hilt-android:2.48.1' implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1' kapt 'com.google.dagger:hilt-compiler:2.48.1'
implementation 'androidx.hilt:hilt-work:1.0.0' implementation 'androidx.hilt:hilt-work:1.1.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.1.0'
implementation 'io.coil-kt:coil-base:2.4.0' implementation 'io.coil-kt:coil-base:2.5.0'
implementation 'io.coil-kt:coil-svg:2.4.0' implementation 'io.coil-kt:coil-svg:2.5.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:cf089a264d' implementation 'com.github.KotatsuApp:subsampling-scale-image-view:cf089a264d'
implementation 'com.github.solkin:disk-lru-cache:1.4' implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'
@@ -143,7 +144,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
testImplementation 'junit:junit:4.13.2' 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' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.5.2'

View File

@@ -19,3 +19,5 @@
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment -keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; } -keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
-keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; } -keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; }
-keep class org.jsoup.parser.Tag
-keep class org.jsoup.internal.StringUtil

View File

@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -19,19 +20,14 @@ class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSourc
override val configKeyDomain: ConfigKey.Domain override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("") get() = ConfigKey.Domain("")
override val sortOrders: Set<SortOrder> override val availableSortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java) get() = EnumSet.allOf(SortOrder::class.java)
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getList( override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
offset: Int,
query: String?,
tags: Set<MangaTag>?,
sortOrder: SortOrder,
): List<Manga> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
@@ -39,7 +35,7 @@ class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSourc
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getTags(): Set<MangaTag> { override suspend fun getAvailableTags(): Set<MangaTag> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View File

@@ -221,6 +221,9 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity"
android:label="@string/sources_catalog" />
<service <service
android:name="androidx.work.impl.foreground.SystemForegroundService" android:name="androidx.work.impl.foreground.SystemForegroundService"

View File

@@ -48,8 +48,8 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
if (!catchingWebViewUnavailability { if (!catchingWebViewUnavailability {
setContentView( setContentView(
ActivityBrowserBinding.inflate( ActivityBrowserBinding.inflate(
layoutInflater layoutInflater,
) ),
) )
}) { }) {
return return
@@ -82,9 +82,11 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
} }
override fun onDestroy() { override fun onDestroy() {
viewBinding.webView.run { runCatching {
stopLoading() viewBinding.webView
destroy() }.onSuccess {
it.stopLoading()
it.destroy()
} }
super.onDestroy() super.onDestroy()
} }

View File

@@ -4,10 +4,15 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Upsert import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
@Dao @Dao
abstract class MangaSourcesDao { abstract class MangaSourcesDao {
@@ -15,11 +20,11 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources ORDER BY sort_key") @Query("SELECT * FROM sources ORDER BY sort_key")
abstract suspend fun findAll(): List<MangaSourceEntity> abstract suspend fun findAll(): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key") @Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key")
abstract suspend fun findAllEnabled(): List<MangaSourceEntity> abstract suspend fun findAllDisabled(): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key") @Query("SELECT * FROM sources WHERE enabled = 0")
abstract fun observeEnabled(): Flow<List<MangaSourceEntity>> abstract fun observeDisabled(): Flow<List<MangaSourceEntity>>
@Query("SELECT * FROM sources ORDER BY sort_key") @Query("SELECT * FROM sources ORDER BY sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>> abstract fun observeAll(): Flow<List<MangaSourceEntity>>
@@ -40,6 +45,22 @@ abstract class MangaSourcesDao {
@Upsert @Upsert
abstract suspend fun upsert(entry: MangaSourceEntity) 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 @Transaction
open suspend fun setEnabled(source: String, isEnabled: Boolean) { open suspend fun setEnabled(source: String, isEnabled: Boolean) {
if (updateIsEnabled(source, isEnabled) == 0) { if (updateIsEnabled(source, isEnabled) == 0) {
@@ -54,4 +75,16 @@ abstract class MangaSourcesDao {
@Query("UPDATE sources SET enabled = :isEnabled WHERE source = :source") @Query("UPDATE sources SET enabled = :isEnabled WHERE source = :source")
protected abstract suspend fun updateIsEnabled(source: String, isEnabled: Boolean): Int 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"
}
} }

View File

@@ -1,5 +1,8 @@
package org.koitharu.kotatsu.core.model 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.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase import org.koitharu.kotatsu.parsers.util.toTitleCase
@@ -18,3 +21,18 @@ fun MangaSource(name: String): MangaSource {
} }
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI 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)
}

View File

@@ -14,8 +14,8 @@ class CloudFlareInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request()) val response = chain.proceed(chain.request())
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) { if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
val content = response.body?.source()?.peek()?.use { val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use {
Jsoup.parse(it.inputStream(), Charsets.UTF_8.name(), response.request.url.toString()) Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString())
} ?: return response } ?: return response
if (content.getElementById("challenge-error-title") != null) { if (content.getElementById("challenge-error-title") != null) {
val request = response.request val request = response.request

View File

@@ -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.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags 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.core.prefs.ReaderMode
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -77,10 +78,18 @@ class MangaDataRepository @Inject constructor(
} }
suspend fun storeManga(manga: Manga) { suspend fun storeManga(manga: Manga) {
val tags = manga.tags.toEntities()
db.withTransaction { db.withTransaction {
db.getTagsDao().upsert(tags) // avoid storing local manga if remote one is already stored
db.getMangaDao().upsert(manga.toEntity(), tags) 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)
}
} }
} }

View File

@@ -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.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull 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.list.domain.ListSortOrder
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.find import org.koitharu.kotatsu.parsers.util.find
@@ -209,6 +210,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
return policy.isNetworkAllowed(connectivityManager) 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 var isSourcesGridMode: Boolean
get() = prefs.getBoolean(KEY_SOURCES_GRID, false) get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) } 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_RELATED_MANGA = "related_manga"
const val KEY_NAV_MAIN = "nav_main" const val KEY_NAV_MAIN = "nav_main"
const val KEY_32BIT_COLOR = "enhanced_colors" const val KEY_32BIT_COLOR = "enhanced_colors"
const val KEY_SOURCES_ORDER = "sources_sort_order"
const val KEY_SOURCES_CATALOG = "sources_catalog"
// About // About
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"

View File

@@ -4,11 +4,11 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.text.htmlEncode import androidx.core.text.htmlEncode
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -37,7 +37,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>() {
override fun onViewBindingCreated(binding: DialogErrorDetailsBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: DialogErrorDetailsBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
with(binding.textViewMessage) { with(binding.textViewMessage) {
movementMethod = LinkMovementMethod.getInstance() movementMethod = LinkMovementMethodCompat.getInstance()
text = context.getString( text = context.getString(
R.string.manga_error_description_pattern, R.string.manga_error_description_pattern,
exception.message?.htmlEncode().orEmpty(), exception.message?.htmlEncode().orEmpty(),

View File

@@ -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)
}
}
}

View File

@@ -26,7 +26,7 @@ class CompositeMutex<T : Any> : Set<T> {
} }
override fun isEmpty(): Boolean { override fun isEmpty(): Boolean {
return state.isEmpty return state.isEmpty()
} }
override fun iterator(): Iterator<T> { override fun iterator(): Iterator<T> {

View File

@@ -19,7 +19,7 @@ class CompositeMutex2<T : Any> : Set<T> {
} }
override fun isEmpty(): Boolean { override fun isEmpty(): Boolean {
return delegates.isEmpty return delegates.isEmpty()
} }
override fun iterator(): Iterator<T> { override fun iterator(): Iterator<T> {

View File

@@ -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)
}
}
}
}

View File

@@ -6,13 +6,16 @@ import android.content.res.Configuration
import android.database.ContentObserver import android.database.ContentObserver
import android.os.Handler import android.os.Handler
import android.provider.Settings import android.provider.Settings
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onStart 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 val isAutoRotationEnabled: Boolean
get() = Settings.System.getInt( get() = Settings.System.getInt(
@@ -31,9 +34,15 @@ class ScreenOrientationHelper(private val activity: Activity) {
} }
} }
fun toggleOrientation() { var isLocked: Boolean
isLandscape = !isLandscape 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 { fun observeAutoOrientation() = callbackFlow {
val observer = object : ContentObserver(Handler(activity.mainLooper)) { val observer = object : ContentObserver(Handler(activity.mainLooper)) {

View File

@@ -8,6 +8,7 @@ import okhttp3.Response
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import okio.IOException import okio.IOException
import org.json.JSONObject import org.json.JSONObject
import org.jsoup.HttpStatusException
import java.net.HttpURLConnection import java.net.HttpURLConnection
private val TYPE_JSON = "application/json".toMediaType() private val TYPE_JSON = "application/json".toMediaType()
@@ -34,9 +35,8 @@ val HttpUrl.isHttpOrHttps: Boolean
fun Response.ensureSuccess() = apply { fun Response.ensureSuccess() = apply {
if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) { if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) {
val message = "Invalid response: $code $message at ${request.url}"
closeQuietly() closeQuietly()
throw IllegalStateException(message) throw HttpStatusException(message, code, request.url.toString())
} }
} }

View File

@@ -12,6 +12,7 @@ import androidx.core.view.children
import androidx.core.view.descendants import androidx.core.view.descendants
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.progressindicator.BaseProgressIndicator
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
@@ -68,6 +69,10 @@ inline fun ViewPager2.doOnPageChanged(crossinline callback: (Int) -> Unit) {
val ViewPager2.recyclerView: RecyclerView? val ViewPager2.recyclerView: RecyclerView?
get() = children.firstNotNullOfOrNull { it as? RecyclerView } get() = children.firstNotNullOfOrNull { it as? RecyclerView }
fun ViewPager2.findCurrentViewHolder(): ViewHolder? {
return recyclerView?.findViewHolderForAdapterPosition(currentItem)
}
fun View.resetTransformations() { fun View.resetTransformations() {
alpha = 1f alpha = 1f
translationX = 0f translationX = 0f

View File

@@ -11,6 +11,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okio.IOException
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
@@ -46,9 +47,15 @@ class DetailsLoadUseCase @Inject constructor(
null null
} }
send(MangaDetails(manga, null, null, false)) send(MangaDetails(manga, null, null, false))
val details = getDetails(manga) try {
send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false)) val details = getDetails(manga)
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true)) send(MangaDetails(details, local?.peek(), details.description?.parseAsHtml(withImages = false), false))
send(MangaDetails(details, local?.await(), details.description?.parseAsHtml(withImages = true), true))
} catch (e: IOException) {
local?.await()?.manga?.also { localManga ->
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
} ?: throw e
}
} }
private suspend fun getDetails(seed: Manga) = runCatchingCancellable { private suspend fun getDetails(seed: Manga) = runCatchingCancellable {

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.transition.TransitionManager import android.transition.TransitionManager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -13,6 +12,7 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.color import androidx.core.text.color
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@@ -107,7 +107,7 @@ class DetailsFragment :
binding.infoLayout.textViewSource.setOnClickListener(this) binding.infoLayout.textViewSource.setOnClickListener(this)
binding.textViewDescription.addOnLayoutChangeListener(this) binding.textViewDescription.addOnLayoutChangeListener(this)
binding.textViewDescription.viewTreeObserver.addOnDrawListener(this) binding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
binding.chipsTags.onChipClickListener = this binding.chipsTags.onChipClickListener = this
binding.recyclerViewRelated.addItemDecoration( binding.recyclerViewRelated.addItemDecoration(
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)), SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
@@ -197,6 +197,11 @@ class DetailsFragment :
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_abandoned) drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_abandoned)
} }
MangaState.PAUSED -> infoLayout.textViewState.apply {
textAndVisible = resources.getString(R.string.state_paused)
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_action_pause)
}
null -> infoLayout.textViewState.isVisible = false null -> infoLayout.textViewState.isVisible = false
} }
if (manga.source == MangaSource.LOCAL) { if (manga.source == MangaSource.LOCAL) {

View File

@@ -19,7 +19,7 @@ import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption 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.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
@@ -63,7 +63,7 @@ class DetailsMenuProvider(
R.id.action_favourite -> { R.id.action_favourite -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
FavouriteSheet.show(activity.supportFragmentManager, it) FavoriteSheet.show(activity.supportFragmentManager, it)
} }
} }

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.details.ui.scrobbling
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -12,6 +11,7 @@ import android.widget.RatingBar
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import coil.ImageLoader import coil.ImageLoader
@@ -71,7 +71,7 @@ class ScrobblingInfoSheet :
binding.ratingBar.onRatingBarChangeListener = this binding.ratingBar.onRatingBarChangeListener = this
binding.buttonMenu.setOnClickListener(this) binding.buttonMenu.setOnClickListener(this)
binding.imageViewCover.setOnClickListener(this) binding.imageViewCover.setOnClickListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
menu = PopupMenu(binding.root.context, binding.buttonMenu).apply { menu = PopupMenu(binding.root.context, binding.buttonMenu).apply {
inflate(R.menu.opt_scrobbling) inflate(R.menu.opt_scrobbling)

View File

@@ -19,6 +19,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.Collections import java.util.Collections
import java.util.EnumSet import java.util.EnumSet
import javax.inject.Inject import javax.inject.Inject
@@ -43,15 +44,44 @@ class MangaSourcesRepository @Inject constructor(
get() = Collections.unmodifiableSet(remoteSources) get() = Collections.unmodifiableSet(remoteSources)
suspend fun getEnabledSources(): List<MangaSource> { 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 -> suspend fun getDisabledSources(): List<MangaSource> {
dao.observeEnabled().map { return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled, null)
it.toSources(skipNsfw)
}
} }
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 -> fun observeAll(): Flow<List<Pair<MangaSource, Boolean>>> = dao.observeAll().map { entities ->
val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size) val result = ArrayList<Pair<MangaSource, Boolean>>(entities.size)
for (entity in entities) { for (entity in entities) {
@@ -146,7 +176,10 @@ class MangaSourcesRepository @Inject constructor(
return result 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) val result = ArrayList<MangaSource>(size)
for (entity in this) { for (entity in this) {
val source = MangaSource(entity.source) val source = MangaSource(entity.source)
@@ -157,6 +190,9 @@ class MangaSourcesRepository @Inject constructor(
result.add(source) result.add(source)
} }
} }
if (sortOrder == SourcesSortOrder.ALPHABETIC) {
result.sortBy { it.title }
}
return result return result
} }
@@ -167,4 +203,8 @@ class MangaSourcesRepository @Inject constructor(
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) { private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(AppSettings.KEY_SOURCES_NEW) {
isNewSourcesTipEnabled isNewSourcesTipEnabled
} }
private fun observeSortOrder() = settings.observeAsFlow(AppSettings.KEY_SOURCES_ORDER) {
sourcesSortOrder
}
} }

View File

@@ -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),
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.explore.ui package org.koitharu.kotatsu.explore.ui
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem 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.search.ui.MangaListActivity
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
import javax.inject.Inject import javax.inject.Inject
@@ -83,7 +85,7 @@ class ExploreFragment :
SpanSizeResolver(this, resources.getDimensionPixelSize(R.dimen.explore_grid_width)).attach() SpanSizeResolver(this, resources.getDimensionPixelSize(R.dimen.explore_grid_width)).attach()
addItemDecoration(TypedListSpacingDecoration(context, false)) addItemDecoration(TypedListSpacingDecoration(context, false))
} }
addMenuProvider(ExploreMenuProvider(binding.root.context, viewModel)) addMenuProvider(ExploreMenuProvider(binding.root.context))
viewModel.content.observe(viewLifecycleOwner) { viewModel.content.observe(viewLifecycleOwner) {
exploreAdapter?.items = it exploreAdapter?.items = it
} }
@@ -109,7 +111,7 @@ class ExploreFragment :
} }
override fun onListHeaderClick(item: ListHeader, view: View) { override fun onListHeaderClick(item: ListHeader, view: View) {
startActivity(SettingsActivity.newManageSourcesIntent(view.context)) startActivity(Intent(view.context, SourcesCatalogActivity::class.java))
} }
override fun onPrimaryButtonClick(tipView: TipView) { override fun onPrimaryButtonClick(tipView: TipView) {
@@ -174,7 +176,6 @@ class ExploreFragment :
} else { } else {
LinearLayoutManager(requireContext()) LinearLayoutManager(requireContext())
} }
activity?.invalidateOptionsMenu()
} }
private fun showSuggestionsTip() { private fun showSuggestionsTip() {

View File

@@ -6,10 +6,10 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.settings.SettingsActivity
class ExploreMenuProvider( class ExploreMenuProvider(
private val context: Context, private val context: Context,
private val viewModel: ExploreViewModel,
) : MenuProvider { ) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
@@ -18,17 +18,12 @@ class ExploreMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) { return when (menuItem.itemId) {
R.id.action_grid -> { R.id.action_manage -> {
viewModel.setGridMode(!menuItem.isChecked) context.startActivity(SettingsActivity.newSourcesSettingsIntent(context))
true true
} }
else -> false else -> false
} }
} }
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_grid)?.isChecked = viewModel.isGrid.value == true
}
} }

View File

@@ -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.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository 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.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
@@ -50,11 +51,13 @@ class ExploreViewModel @Inject constructor(
valueProducer = { isSourcesGridMode }, valueProducer = { isSourcesGridMode },
) )
val isSuggestionsEnabled = settings.observeAsFlow( private val isSuggestionsEnabled = settings.observeAsFlow(
key = AppSettings.KEY_SUGGESTIONS, key = AppSettings.KEY_SUGGESTIONS,
valueProducer = { isSuggestionsEnabled }, valueProducer = { isSuggestionsEnabled },
) )
val sortOrder = MutableStateFlow(SourcesSortOrder.MANUAL) // TODO
val onOpenManga = MutableEventFlow<Manga>() val onOpenManga = MutableEventFlow<Manga>()
val onActionDone = MutableEventFlow<ReversibleAction>() val onActionDone = MutableEventFlow<ReversibleAction>()
val onShowSuggestionsTip = MutableEventFlow<Unit>() val onShowSuggestionsTip = MutableEventFlow<Unit>()
@@ -104,10 +107,6 @@ class ExploreViewModel @Inject constructor(
} }
} }
fun setGridMode(value: Boolean) {
settings.isSourcesGridMode = value
}
fun respondSuggestionTip(isAccepted: Boolean) { fun respondSuggestionTip(isAccepted: Boolean) {
settings.isSuggestionsEnabled = isAccepted settings.isSuggestionsEnabled = isAccepted
settings.closeTip(TIP_SUGGESTIONS) settings.closeTip(TIP_SUGGESTIONS)
@@ -137,7 +136,7 @@ class ExploreViewModel @Inject constructor(
result += RecommendationsItem(recommendation) result += RecommendationsItem(recommendation)
} }
if (sources.isNotEmpty()) { if (sources.isNotEmpty()) {
result += ListHeader(R.string.remote_sources, R.string.manage) result += ListHeader(R.string.remote_sources, R.string.catalog)
if (newSources.isNotEmpty()) { if (newSources.isNotEmpty()) {
result += TipModel( result += TipModel(
key = TIP_NEW_SOURCES, key = TIP_NEW_SOURCES,

View File

@@ -7,6 +7,7 @@ import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R 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.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
@@ -48,8 +49,8 @@ fun exploreButtonsAD(
icon.setColorSchemeColors( icon.setColorSchemeColors(
context.getThemeColor( context.getThemeColor(
materialR.attr.colorPrimary, materialR.attr.colorPrimary,
Color.DKGRAY Color.DKGRAY,
) ),
) )
binding.buttonRandom.icon = icon binding.buttonRandom.icon = icon
icon.start() icon.start()
@@ -98,7 +99,7 @@ fun exploreSourceListItemAD(
ItemExploreSourceListBinding.inflate( ItemExploreSourceListBinding.inflate(
layoutInflater, layoutInflater,
parent, parent,
false false,
) )
}, },
on = { item, _, _ -> item is MangaSourceItem && !item.isGrid }, on = { item, _, _ -> item is MangaSourceItem && !item.isGrid },
@@ -112,6 +113,7 @@ fun exploreSourceListItemAD(
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title
binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon) fallback(fallbackIcon)
@@ -132,7 +134,7 @@ fun exploreSourceGridItemAD(
ItemExploreSourceGridBinding.inflate( ItemExploreSourceGridBinding.inflate(
layoutInflater, layoutInflater,
parent, parent,
false false,
) )
}, },
on = { item, _, _ -> item is MangaSourceItem && item.isGrid }, on = { item, _, _ -> item is MangaSourceItem && item.isGrid },

View File

@@ -106,6 +106,9 @@ abstract class FavouritesDao {
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0") @Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0")
abstract fun observeIds(id: Long): Flow<List<Long>> 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 **/
@Insert(onConflict = OnConflictStrategy.REPLACE) @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.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id) DESC"
ListSortOrder.UPDATED, // for legacy support ListSortOrder.UPDATED, // for legacy support
ListSortOrder.PROGRESS -> "(SELECT percent FROM history WHERE history.manga_id = manga.manga_id) DESC" ListSortOrder.PROGRESS -> "(SELECT percent FROM history WHERE history.manga_id = manga.manga_id) DESC"
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported") else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
} }
} }

View File

@@ -102,6 +102,10 @@ class FavouritesRepository @Inject constructor(
return db.getFavouriteCategoriesDao().find(id.toInt()).toFavouriteCategory() return db.getFavouriteCategoriesDao().find(id.toInt()).toFavouriteCategory()
} }
suspend fun getCategoriesIds(mangaIds: Collection<Long>): Set<Long> {
return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet()
}
suspend fun createCategory( suspend fun createCategory(
title: String, title: String,
sortOrder: ListSortOrder, sortOrder: ListSortOrder,

View File

@@ -19,17 +19,12 @@ import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem 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 import org.koitharu.kotatsu.parsers.model.Manga
@AndroidEntryPoint @AndroidEntryPoint
class FavouriteSheet : class FavoriteSheet : BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(), OnListItemClickListener<MangaCategoryItem> {
BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(),
OnListItemClickListener<MangaCategoryItem> {
private val viewModel: MangaCategoriesViewModel by viewModels() private val viewModel by viewModels<FavoriteSheetViewModel>()
private var adapter: MangaCategoriesAdapter? = null
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -41,44 +36,32 @@ class FavouriteSheet :
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
) { ) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
adapter = MangaCategoriesAdapter(this) val adapter = MangaCategoriesAdapter(this)
binding.recyclerViewCategories.adapter = adapter binding.recyclerViewCategories.adapter = adapter
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged) viewModel.content.observe(viewLifecycleOwner, adapter)
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError) viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
} }
override fun onDestroyView() {
adapter = null
super.onDestroyView()
}
override fun onItemClick(item: MangaCategoryItem, view: View) { override fun onItemClick(item: MangaCategoryItem, view: View) {
viewModel.setChecked(item.category.id, !item.isChecked) viewModel.setChecked(item.category.id, !item.isChecked)
} }
private fun onContentChanged(categories: List<ListModel>) {
adapter?.items = categories
}
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show() Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show()
} }
companion object { companion object {
private const val TAG = "FavouriteCategoriesDialog" private const val TAG = "FavoriteSheet"
const val KEY_MANGA_LIST = "manga_list" 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>) = fun show(fm: FragmentManager, manga: Collection<Manga>) = FavoriteSheet().withArgs(1) {
FavouriteSheet().withArgs(1) { putParcelableArrayList(
putParcelableArrayList( KEY_MANGA_LIST,
KEY_MANGA_LIST, manga.mapTo(ArrayList(manga.size), ::ParcelableManga),
manga.mapTo(ArrayList(manga.size)) { )
ParcelableManga(it) }.showDistinct(fm, TAG)
},
)
}.showDistinct(fm, TAG)
} }
} }

View File

@@ -4,77 +4,70 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.ids import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.prefs.AppSettings 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.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.firstNotNull
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository 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.CategoriesHeaderItem
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem 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 import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MangaCategoriesViewModel @Inject constructor( class FavoriteSheetViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val favouritesRepository: FavouritesRepository, private val favouritesRepository: FavouritesRepository,
settings: AppSettings, settings: AppSettings,
) : BaseViewModel() { ) : 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() private val header = CategoriesHeaderItem()
private val checkedCategories = MutableStateFlow<Set<Long>?>(null)
val content: StateFlow<List<ListModel>> = combine( val content = combine(
favouritesRepository.observeCategories(), favouritesRepository.observeCategories(),
observeCategoriesIds(), checkedCategories.filterNotNull(),
) { all, checked -> settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
buildList(all.size + 1) { ) { categories, checked, tracker ->
buildList(categories.size + 1) {
add(header) add(header)
all.mapTo(this) { categories.mapTo(this) { cat ->
MangaCategoryItem( MangaCategoryItem(
category = it, category = cat,
isChecked = it.id in checked, isChecked = cat.id in checked,
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources, 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) { fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val checkedIds = checkedCategories.firstNotNull()
if (isChecked) { if (isChecked) {
checkedCategories.value = checkedIds + categoryId
favouritesRepository.addToCategory(categoryId, manga) favouritesRepository.addToCategory(categoryId, manga)
} else { } else {
checkedCategories.value = checkedIds - categoryId
favouritesRepository.removeFromCategory(categoryId, manga.ids()) 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
}
}
} }

View File

@@ -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.MangaHistory
import org.koitharu.kotatsu.core.model.findById import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal 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.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.core.util.ext.mapItems
@@ -36,6 +37,7 @@ class HistoryRepository @Inject constructor(
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
private val mangaRepository: MangaDataRepository,
) { ) {
suspend fun getList(offset: Int, limit: Int): List<Manga> { suspend fun getList(offset: Int, limit: Int): List<Manga> {
@@ -92,13 +94,8 @@ class HistoryRepository @Inject constructor(
if (shouldSkip(manga)) { if (shouldSkip(manga)) {
return return
} }
val tags = manga.tags.toEntities()
db.withTransaction { db.withTransaction {
val existing = db.getMangaDao().find(manga.id)?.manga mangaRepository.storeManga(manga)
if (existing == null || existing.source == manga.source.name) {
db.getTagsDao().upsert(tags)
db.getMangaDao().upsert(manga.toEntity(), tags)
}
db.getHistoryDao().upsert( db.getHistoryDao().upsert(
HistoryEntity( HistoryEntity(
mangaId = manga.id, mangaId = manga.id,

View File

@@ -41,7 +41,7 @@ import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver 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.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
@@ -284,7 +284,7 @@ abstract class MangaListFragment :
} }
R.id.action_favourite -> { R.id.action_favourite -> {
FavouriteSheet.show(childFragmentManager, selectedItems) FavoriteSheet.show(childFragmentManager, selectedItems)
mode.finish() mode.finish()
true true
} }

View File

@@ -1,12 +1,12 @@
package org.koitharu.kotatsu.list.ui.preview package org.koitharu.kotatsu.list.ui.preview
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import coil.ImageLoader import coil.ImageLoader
@@ -52,7 +52,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
binding.buttonClose.isVisible = activity is MangaListActivity binding.buttonClose.isVisible = activity is MangaListActivity
binding.buttonClose.setOnClickListener(this) binding.buttonClose.setOnClickListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
binding.chipsTags.onChipClickListener = this binding.chipsTags.onChipClickListener = this
binding.textViewAuthor.setOnClickListener(this) binding.textViewAuthor.setOnClickListener(this)
binding.imageViewCover.setOnClickListener(this) binding.imageViewCover.setOnClickListener(this)

View File

@@ -22,6 +22,7 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.withResumed import androidx.lifecycle.withResumed
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
@@ -229,6 +230,9 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
supportFragmentManager.commit { supportFragmentManager.commit {
setReorderingAllowed(true) setReorderingAllowed(true)
add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH) add(R.id.container, SearchSuggestionFragment.newInstance(), TAG_SEARCH)
navigationDelegate.primaryFragment?.let {
setMaxLifecycle(it, Lifecycle.State.STARTED)
}
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
runOnCommit { onSearchOpened() } runOnCommit { onSearchOpened() }
} }
@@ -414,16 +418,20 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
private inner class CloseSearchCallback : OnBackPressedCallback(false) { private inner class CloseSearchCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH) val fm = supportFragmentManager
val fragment = fm.findFragmentByTag(TAG_SEARCH)
viewBinding.searchView.clearFocus() viewBinding.searchView.clearFocus()
if (fragment == null) { if (fragment == null) {
// this should not happen but who knows // this should not happen but who knows
isEnabled = false isEnabled = false
return return
} }
supportFragmentManager.commit { fm.commit {
setReorderingAllowed(true) setReorderingAllowed(true)
remove(fragment) remove(fragment)
navigationDelegate.primaryFragment?.let {
setMaxLifecycle(it, Lifecycle.State.RESUMED)
}
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
runOnCommit { onSearchClosed() } runOnCommit { onSearchClosed() }
} }

View File

@@ -40,6 +40,7 @@ import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity 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.GridTouchHelper
import org.koitharu.kotatsu.core.util.IdlingDetector import org.koitharu.kotatsu.core.util.IdlingDetector
import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ShareHelper
@@ -73,7 +74,8 @@ class ReaderActivity :
ReaderConfigSheet.Callback, ReaderConfigSheet.Callback,
ReaderControlDelegate.OnInteractionListener, ReaderControlDelegate.OnInteractionListener,
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener,
IdlingDetector.Callback { IdlingDetector.Callback,
ZoomControl.ZoomControlListener {
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
@@ -111,6 +113,7 @@ class ReaderActivity :
controlDelegate = ReaderControlDelegate(resources, settings, this, this) controlDelegate = ReaderControlDelegate(resources, settings, this, this)
viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected) viewBinding.toolbarBottom.setOnMenuItemClickListener(::onOptionsItemSelected)
viewBinding.slider.setLabelFormatter(PageLabelFormatter()) viewBinding.slider.setLabelFormatter(PageLabelFormatter())
viewBinding.zoomControl.listener = this
ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider) ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider)
insetsDelegate.interceptingWindowInsetsListener = this insetsDelegate.interceptingWindowInsetsListener = this
idlingDetector.bindToLifecycle(this) idlingDetector.bindToLifecycle(this)
@@ -146,6 +149,9 @@ class ReaderActivity :
.setAnchorView(viewBinding.appbarBottom) .setAnchorView(viewBinding.appbarBottom)
.show() .show()
} }
viewModel.isZoomControlsEnabled.observe(this) {
viewBinding.zoomControl.isVisible = it
}
} }
override fun getParentActivityIntent(): Intent? { override fun getParentActivityIntent(): Intent? {
@@ -163,6 +169,14 @@ class ReaderActivity :
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
} }
override fun onZoomIn() {
readerManager.currentReader?.onZoomIn()
}
override fun onZoomOut() {
readerManager.currentReader?.onZoomOut()
}
private fun onInitReader(mode: ReaderMode?) { private fun onInitReader(mode: ReaderMode?) {
if (mode == null) { if (mode == null) {
return return
@@ -293,15 +307,13 @@ class ReaderActivity :
private fun onPageSaved(uri: Uri?) { private fun onPageSaved(uri: Uri?) {
if (uri != null) { if (uri != null) {
Snackbar.make(viewBinding.container, R.string.page_saved, Snackbar.LENGTH_LONG) Snackbar.make(viewBinding.container, R.string.page_saved, Snackbar.LENGTH_LONG)
.setAnchorView(viewBinding.appbarBottom)
.setAction(R.string.share) { .setAction(R.string.share) {
ShareHelper(this).shareImage(uri) ShareHelper(this).shareImage(uri)
}.show() }
} else { } else {
Snackbar.make(viewBinding.container, R.string.error_occurred, Snackbar.LENGTH_SHORT) Snackbar.make(viewBinding.container, R.string.error_occurred, Snackbar.LENGTH_SHORT)
.setAnchorView(viewBinding.appbarBottom) }.setAnchorView(viewBinding.appbarBottom)
.show() .show()
}
} }
private fun setWindowSecure(isSecure: Boolean) { private fun setWindowSecure(isSecure: Boolean) {

View File

@@ -118,17 +118,16 @@ class ReaderViewModel @Inject constructor(
valueProducer = { isReaderKeepScreenOn }, valueProducer = { isReaderKeepScreenOn },
) )
val isWebtoonZoomEnabled = settings.observeAsStateFlow( val isWebtoonZooEnabled = observeIsWebtoonZoomEnabled()
scope = viewModelScope + Dispatchers.Default, .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
key = AppSettings.KEY_WEBTOON_ZOOM,
valueProducer = { isWebtoonZoomEnable },
)
val isZoomControlEnabled = settings.observeAsStateFlow( val isZoomControlsEnabled = getObserveIsZoomControlEnabled().flatMapLatest { zoom ->
scope = viewModelScope + Dispatchers.Default, if (zoom) {
key = AppSettings.KEY_READER_ZOOM_BUTTONS, combine(readerMode, isWebtoonZooEnabled) { mode, ze -> ze || mode != ReaderMode.WEBTOON }
valueProducer = { isReaderZoomButtonsEnabled }, } else {
) flowOf(false)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
val readerSettings = ReaderSettings( val readerSettings = ReaderSettings(
parentScope = viewModelScope, parentScope = viewModelScope,
@@ -259,10 +258,13 @@ class ReaderViewModel @Inject constructor(
@MainThread @MainThread
fun onCurrentPageChanged(position: Int) { fun onCurrentPageChanged(position: Int) {
val prevJob = stateChangeJob val prevJob = stateChangeJob
val pages = content.value.pages // capture immediately
stateChangeJob = launchJob(Dispatchers.Default) { stateChangeJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin() prevJob?.cancelAndJoin()
loadingJob?.join() loadingJob?.join()
val pages = content.value.pages if (pages.size != content.value.pages.size) {
return@launchJob // TODO
}
pages.getOrNull(position)?.let { page -> pages.getOrNull(position)?.let { page ->
currentState.update { cs -> currentState.update { cs ->
cs?.copy(chapterId = page.chapterId, page = page.index) cs?.copy(chapterId = page.chapterId, page = page.index)
@@ -402,4 +404,14 @@ class ReaderViewModel @Inject constructor(
val ppc = 1f / chaptersCount val ppc = 1f / chaptersCount
return ppc * chapterIndex + ppc * pagePercent 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 },
)
} }

View File

@@ -47,7 +47,10 @@ class ReaderConfigSheet :
private val viewModel by activityViewModels<ReaderViewModel>() private val viewModel by activityViewModels<ReaderViewModel>()
private val savePageRequest = registerForActivityResult(PageSaveContract(), this) private val savePageRequest = registerForActivityResult(PageSaveContract(), this)
private var orientationHelper: ScreenOrientationHelper? = null
@Inject
lateinit var orientationHelper: ScreenOrientationHelper
private lateinit var mode: ReaderMode private lateinit var mode: ReaderMode
@Inject @Inject
@@ -113,7 +116,7 @@ class ReaderConfigSheet :
} }
R.id.button_screen_rotate -> { R.id.button_screen_rotate -> {
orientationHelper?.toggleOrientation() orientationHelper.isLandscape = !orientationHelper.isLandscape
} }
R.id.button_color_filter -> { R.id.button_color_filter -> {
@@ -128,9 +131,13 @@ class ReaderConfigSheet :
when (buttonView.id) { when (buttonView.id) {
R.id.switch_scroll_timer -> { R.id.switch_scroll_timer -> {
findCallback()?.isAutoScrollEnabled = isChecked findCallback()?.isAutoScrollEnabled = isChecked
requireViewBinding().labelTimer.isVisible = isChecked requireViewBinding().layoutTimer.isVisible = isChecked
requireViewBinding().sliderTimer.isVisible = isChecked requireViewBinding().sliderTimer.isVisible = isChecked
} }
R.id.switch_screen_lock_rotation -> {
orientationHelper.isLocked = isChecked
}
} }
} }
@@ -159,6 +166,7 @@ class ReaderConfigSheet :
if (fromUser) { if (fromUser) {
settings.readerAutoscrollSpeed = value settings.readerAutoscrollSpeed = value
} }
(viewBinding ?: return).labelTimerValue.text = getString(R.string.speed_value, value * 10f)
} }
override fun onActivityResult(result: Uri?) { override fun onActivityResult(result: Uri?) {
@@ -167,14 +175,23 @@ class ReaderConfigSheet :
} }
private fun observeScreenOrientation() { private fun observeScreenOrientation() {
val helper = ScreenOrientationHelper(requireActivity()) orientationHelper.observeAutoOrientation()
orientationHelper = helper
helper.observeAutoOrientation()
.onEach { .onEach {
requireViewBinding().buttonScreenRotate.isGone = it with(requireViewBinding()) {
buttonScreenRotate.isGone = it
switchScreenLockRotation.isVisible = it
updateOrientationLockSwitch()
}
}.launchIn(viewLifecycleScope) }.launchIn(viewLifecycleScope)
} }
private fun updateOrientationLockSwitch() {
val switch = viewBinding?.switchScreenLockRotation ?: return
switch.setOnCheckedChangeListener(null)
switch.isChecked = orientationHelper.isLocked
switch.setOnCheckedChangeListener(this)
}
private fun findCallback(): Callback? { private fun findCallback(): Callback? {
return (parentFragment as? Callback) ?: (activity as? Callback) return (parentFragment as? Callback) ?: (activity as? Callback)
} }

View File

@@ -46,9 +46,6 @@ class ReaderSettings(
val isPagesNumbersEnabled: Boolean val isPagesNumbersEnabled: Boolean
get() = settings.isPagesNumbersEnabled get() = settings.isPagesNumbersEnabled
val isZoomControlsEnabled: Boolean
get() = settings.isReaderZoomButtonsEnabled
fun applyBackground(view: View) { fun applyBackground(view: View) {
val bg = settings.readerBackground val bg = settings.readerBackground
view.background = bg.resolve(view.context) view.background = bg.resolve(view.context)
@@ -106,8 +103,6 @@ class ReaderSettings(
if ( if (
key == AppSettings.KEY_ZOOM_MODE || key == AppSettings.KEY_ZOOM_MODE ||
key == AppSettings.KEY_PAGES_NUMBERS || key == AppSettings.KEY_PAGES_NUMBERS ||
key == AppSettings.KEY_WEBTOON_ZOOM ||
key == AppSettings.KEY_READER_ZOOM_BUTTONS ||
key == AppSettings.KEY_READER_BACKGROUND || key == AppSettings.KEY_READER_BACKGROUND ||
key == AppSettings.KEY_32BIT_COLOR key == AppSettings.KEY_32BIT_COLOR
) { ) {

View File

@@ -6,6 +6,7 @@ import androidx.fragment.app.activityViewModels
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.prefs.ReaderAnimation import org.koitharu.kotatsu.core.prefs.ReaderAnimation
import org.koitharu.kotatsu.core.ui.BaseFragment 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.getParcelableCompat
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.observe 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" 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>() protected val viewModel by activityViewModels<ReaderViewModel>()
private var stateToSave: ReaderState? = null private var stateToSave: ReaderState? = null

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.ReaderAnimation import org.koitharu.kotatsu.core.prefs.ReaderAnimation
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged 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.observe
import org.koitharu.kotatsu.core.util.ext.recyclerView import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.resetTransformations 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.BaseReaderFragment
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage 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.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.PagerEventSupplier
import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment import org.koitharu.kotatsu.reader.ui.pager.standard.PagerReaderFragment
import javax.inject.Inject import javax.inject.Inject
@@ -104,6 +106,15 @@ class ReversedReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>
exceptionResolver = exceptionResolver, 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) { override fun switchPageBy(delta: Int) {
with(requireViewBinding().pager) { with(requireViewBinding().pager) {
setCurrentItem(currentItem - delta, isAnimationEnabled()) setCurrentItem(currentItem - delta, isAnimationEnabled())

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.graphics.PointF import android.graphics.PointF
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.davemorrissey.labs.subscaleview.ImageSource 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.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.os.NetworkState 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.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.ifZero import org.koitharu.kotatsu.core.util.ext.ifZero
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
@@ -29,7 +31,8 @@ open class PageHolder(
networkState: NetworkState, networkState: NetworkState,
exceptionResolver: ExceptionResolver, exceptionResolver: ExceptionResolver,
) : BasePageHolder<ItemPageBinding>(binding, loader, settings, networkState, exceptionResolver), ) : BasePageHolder<ItemPageBinding>(binding, loader, settings, networkState, exceptionResolver),
View.OnClickListener { View.OnClickListener,
ZoomControl.ZoomControlListener {
init { init {
binding.ssiv.bindToLifecycle(owner) binding.ssiv.bindToLifecycle(owner)
@@ -40,12 +43,10 @@ open class PageHolder(
@Suppress("LeakingThis") @Suppress("LeakingThis")
bindingInfo.buttonErrorDetails.setOnClickListener(this) bindingInfo.buttonErrorDetails.setOnClickListener(this)
binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled binding.textViewNumber.isVisible = settings.isPagesNumbersEnabled
binding.zoomControl.listener = SsivZoomListener(binding.ssiv)
} }
override fun onConfigChanged() { override fun onConfigChanged() {
super.onConfigChanged() super.onConfigChanged()
binding.zoomControl.isVisible = settings.isZoomControlsEnabled
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (settings.applyBitmapConfig(binding.ssiv) && delegate != null) { if (settings.applyBitmapConfig(binding.ssiv) && delegate != null) {
delegate.reload() delegate.reload()
@@ -141,4 +142,23 @@ open class PageHolder(
bindingInfo.layoutError.isVisible = true bindingInfo.layoutError.isVisible = true
bindingInfo.progressBar.hide() 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()
}
}
} }

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.ReaderAnimation import org.koitharu.kotatsu.core.prefs.ReaderAnimation
import org.koitharu.kotatsu.core.util.ext.doOnPageChanged 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.observe
import org.koitharu.kotatsu.core.util.ext.recyclerView import org.koitharu.kotatsu.core.util.ext.recyclerView
import org.koitharu.kotatsu.core.util.ext.resetTransformations import org.koitharu.kotatsu.core.util.ext.resetTransformations
@@ -82,6 +83,14 @@ class PagerReaderFragment : BaseReaderFragment<FragmentReaderStandardBinding>(),
super.onDestroyView() 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 { override fun onGenericMotion(v: View?, event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) { if (event.source and InputDevice.SOURCE_CLASS_POINTER != 0) {
if (event.actionMasked == MotionEvent.ACTION_SCROLL) { if (event.actionMasked == MotionEvent.ACTION_SCROLL) {

View File

@@ -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()
}
}
}

View File

@@ -4,12 +4,10 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkState import org.koitharu.kotatsu.core.os.NetworkState
@@ -47,15 +45,9 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
adapter = readerAdapter adapter = readerAdapter
addOnPageScrollListener(PageScrollListener()) addOnPageScrollListener(PageScrollListener())
} }
binding.zoomControl.listener = binding.frame viewModel.isWebtoonZooEnabled.observe(viewLifecycleOwner) {
viewModel.isWebtoonZoomEnabled.observe(viewLifecycleOwner) {
binding.frame.isZoomEnable = it binding.frame.isZoomEnable = it
} }
combine(viewModel.isWebtoonZoomEnabled, viewModel.isZoomControlEnabled, Boolean::and)
.observe(viewLifecycleOwner) {
binding.zoomControl.isVisible = it
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -111,6 +103,14 @@ class WebtoonReaderFragment : BaseReaderFragment<FragmentReaderWebtoonBinding>()
) )
} }
override fun onZoomIn() {
viewBinding?.frame?.onZoomIn()
}
override fun onZoomOut() {
viewBinding?.frame?.onZoomOut()
}
private fun notifyPageChanged(page: Int) { private fun notifyPageChanged(page: Int) {
viewModel.onCurrentPageChanged(page) viewModel.onCurrentPageChanged(page)
} }

View File

@@ -51,7 +51,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
private val targetHitRect = Rect() private val targetHitRect = Rect()
private var animator: ValueAnimator? = null private var animator: ValueAnimator? = null
var isZoomEnable = true var isZoomEnable = false
set(value) { set(value) {
field = value field = value
if (scale != 1f) { if (scale != 1f) {

View File

@@ -8,7 +8,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import coil.ImageLoader 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.databinding.ActivitySearchMultiBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver 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.MangaSelectionDecoration
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
@@ -159,7 +158,7 @@ class MultiSearchActivity :
} }
R.id.action_favourite -> { R.id.action_favourite -> {
FavouriteSheet.show(supportFragmentManager, collectSelectedItems()) FavoriteSheet.show(supportFragmentManager, collectSelectedItems())
mode.finish() mode.finish()
true true
} }

View File

@@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R 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.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
@@ -40,6 +41,7 @@ fun searchSuggestionSourceAD(
} else { } else {
item.source.title item.source.title
} }
binding.textViewSubtitle.text = item.source.getSummary(context)
binding.switchLocal.isChecked = item.isEnabled binding.switchLocal.isChecked = item.isEnabled
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name) val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run { binding.imageViewCover.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
@@ -9,7 +8,6 @@ import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.LocaleManagerCompat
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint 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.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle 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.getLocalesConfig
import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.postDelayed import org.koitharu.kotatsu.core.util.ext.postDelayed
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
import org.koitharu.kotatsu.core.util.ext.toList 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.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.utils.ActivityListPreference import org.koitharu.kotatsu.settings.utils.ActivityListPreference
import org.koitharu.kotatsu.settings.utils.SliderPreference import org.koitharu.kotatsu.settings.utils.SliderPreference
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -110,7 +107,7 @@ class AppearanceSettingsFragment :
private fun initLocalePicker(preference: ListPreference) { private fun initLocalePicker(preference: ListPreference) {
val locales = preference.context.getLocalesConfig() val locales = preference.context.getLocalesConfig()
.toList() .toList()
.sortedWith(LocaleComparator(preference.context)) .sortedWith(LocaleComparator())
preference.entries = Array(locales.size + 1) { i -> preference.entries = Array(locales.size + 1) { i ->
if (i == 0) { if (i == 0) {
getString(R.string.automatic) getString(R.string.automatic)
@@ -134,24 +131,4 @@ class AppearanceSettingsFragment :
getString(it.title) 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)
}
}
}
}
} }

View File

@@ -4,7 +4,6 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -18,7 +17,6 @@ class RootSettingsViewModel @Inject constructor(
val totalSourcesCount = sourcesRepository.allMangaSources.size val totalSourcesCount = sourcesRepository.allMangaSources.size
val enabledSourcesCount = sourcesRepository.observeEnabledSources() val enabledSourcesCount = sourcesRepository.observeEnabledSourcesCount()
.map { it.size }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, -1)
} }

View File

@@ -31,7 +31,8 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.about.AboutSettingsFragment import org.koitharu.kotatsu.settings.about.AboutSettingsFragment
import org.koitharu.kotatsu.settings.about.AppUpdateDialog import org.koitharu.kotatsu.settings.about.AppUpdateDialog
import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment 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.tracker.TrackerSettingsFragment
import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment
@@ -153,6 +154,7 @@ class SettingsActivity :
ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
ACTION_HISTORY -> UserDataSettingsFragment() ACTION_HISTORY -> UserDataSettingsFragment()
ACTION_TRACKER -> TrackerSettingsFragment() ACTION_TRACKER -> TrackerSettingsFragment()
ACTION_SOURCES -> SourcesSettingsFragment()
ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment() ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment()
ACTION_SOURCE -> SourceSettingsFragment.newInstance( ACTION_SOURCE -> SourceSettingsFragment.newInstance(
intent.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource ?: MangaSource.LOCAL, 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_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
private const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY" 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_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_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
private const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS" private const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS"
private const val EXTRA_SOURCE = "source" private const val EXTRA_SOURCE = "source"
@@ -206,6 +209,10 @@ class SettingsActivity :
Intent(context, SettingsActivity::class.java) Intent(context, SettingsActivity::class.java)
.setAction(ACTION_HISTORY) .setAction(ACTION_HISTORY)
fun newSourcesSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SOURCES)
fun newManageSourcesIntent(context: Context) = fun newManageSourcesIntent(context: Context) =
Intent(context, SettingsActivity::class.java) Intent(context, SettingsActivity::class.java)
.setAction(ACTION_MANAGE_SOURCES) .setAction(ACTION_MANAGE_SOURCES)

View File

@@ -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.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.DialogProgressBinding import org.koitharu.kotatsu.databinding.DialogProgressBinding
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -28,7 +29,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
private var backup: File? = null private var backup: File? = null
private val saveFileContract = registerForActivityResult( private val saveFileContract = registerForActivityResult(
ActivityResultContracts.CreateDocument("*/*"), ActivityResultContracts.CreateDocument("application/zip"),
) { uri -> ) { uri ->
val file = backup val file = backup
if (uri != null && file != null) { if (uri != null && file != null) {
@@ -81,7 +82,10 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
private fun onBackupDone(file: File) { private fun onBackupDone(file: File) {
this.backup = 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) { private fun saveBackup(file: File, output: Uri) {
@@ -91,7 +95,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
it.write(file.readBytes()) 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() dismiss()
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
throw e throw e

View File

@@ -16,15 +16,11 @@ import androidx.work.workDataOf
import dagger.Reusable import dagger.Reusable
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject 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.BackupRepository
import org.koitharu.kotatsu.core.backup.BackupZipOutput import org.koitharu.kotatsu.core.backup.BackupZipOutput
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -52,12 +48,10 @@ class PeriodicalBackupWorker @AssistedInject constructor(
} }
val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData) val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData)
val target = DocumentFile.fromTreeUri(applicationContext, dirUri) val target = DocumentFile.fromTreeUri(applicationContext, dirUri)
?.createFile("application/zip", file.name) ?.createFile("application/zip", file.nameWithoutExtension)
?.uri ?: return Result.failure() ?.uri ?: return Result.failure()
applicationContext.contentResolver.openOutputStream(target)?.use { output -> applicationContext.contentResolver.openOutputStream(target, "wt")?.use { output ->
file.source().use { input -> file.inputStream().copyTo(output)
output.sink().buffer().writeAllCancellable(input)
}
} ?: return Result.failure() } ?: return Result.failure()
file.deleteAwait() file.deleteAwait()
return Result.success(resultData) return Result.success(resultData)
@@ -79,6 +73,7 @@ class PeriodicalBackupWorker @AssistedInject constructor(
settings.periodicalBackupFrequency, settings.periodicalBackupFrequency,
TimeUnit.DAYS, TimeUnit.DAYS,
).setConstraints(constraints.build()) ).setConstraints(constraints.build())
.keepResultsForAtLeast(20, TimeUnit.DAYS)
.addTag(TAG) .addTag(TAG)
.build() .build()
workManager workManager

View File

@@ -65,8 +65,6 @@ class NewSourcesDialogFragment :
viewModel.onItemEnabledChanged(item, isEnabled) viewModel.onItemEnabledChanged(item, isEnabled)
} }
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) = Unit
override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit override fun onCloseTip(tip: SourceConfigItem.Tip) = Unit
companion object { companion object {

View File

@@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
@@ -35,7 +34,6 @@ class NewSourcesViewModel @Inject constructor(
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = source, source = source,
isEnabled = enabled, isEnabled = enabled,
summary = source.getLocaleTitle(),
isDraggable = false, isDraggable = false,
isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI, isAvailable = !skipNsfw || source.contentType != ContentType.HENTAI,
) )

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -13,8 +13,6 @@ class SourceConfigAdapter(
init { init {
with(delegatesManager) { with(delegatesManager) {
addDelegate(sourceConfigHeaderDelegate())
addDelegate(sourceConfigGroupDelegate(listener))
addDelegate(sourceConfigItemDelegate2(listener, coil, lifecycleOwner)) addDelegate(sourceConfigItemDelegate2(listener, coil, lifecycleOwner))
addDelegate(sourceConfigEmptySearchDelegate()) addDelegate(sourceConfigEmptySearchDelegate())
addDelegate(sourceConfigTipDelegate(listener)) addDelegate(sourceConfigTipDelegate(listener))

View File

@@ -18,6 +18,7 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R 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.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener 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.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source 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.ItemSourceConfigBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding import org.koitharu.kotatsu.databinding.ItemSourceConfigCheckableBinding
import org.koitharu.kotatsu.databinding.ItemTipBinding import org.koitharu.kotatsu.databinding.ItemTipBinding
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem 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( fun sourceConfigItemCheckableDelegate(
listener: SourceConfigListener, listener: SourceConfigListener,
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = ) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>(
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigCheckableBinding>( { layoutInflater, parent ->
{ layoutInflater, parent -> ItemSourceConfigCheckableBinding.inflate(
ItemSourceConfigCheckableBinding.inflate( layoutInflater,
layoutInflater, parent,
parent, false,
false, )
) },
}, ) {
) {
binding.switchToggle.setOnCheckedChangeListener { _, isChecked -> binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
listener.onItemEnabledChanged(item, isChecked) listener.onItemEnabledChanged(item, isChecked)
}
bind {
binding.textViewTitle.text = if (item.isNsfw) {
buildSpannedString {
append(item.source.title)
append(' ')
appendNsfwLabel(context)
}
} else {
item.source.title
} }
binding.switchToggle.isChecked = item.isEnabled
bind { binding.switchToggle.isEnabled = item.isAvailable
binding.textViewTitle.text = if (item.isNsfw) { binding.textViewDescription.text = item.source.getSummary(context)
buildSpannedString { val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
append(item.source.title) binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
append(' ') crossfade(context)
appendNsfwLabel(context) error(fallbackIcon)
} placeholder(fallbackIcon)
} else { fallback(fallbackIcon)
item.source.title source(item.source)
} enqueueWith(coil)
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)
}
} }
} }
}
fun sourceConfigItemDelegate2( fun sourceConfigItemDelegate2(
listener: SourceConfigListener, listener: SourceConfigListener,
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
) = ) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>( { layoutInflater, parent ->
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(
ItemSourceConfigBinding.inflate( layoutInflater,
layoutInflater, parent,
parent, false,
false, )
) },
}, ) {
) {
val eventListener = View.OnClickListener { v -> val eventListener = View.OnClickListener { v ->
when (v.id) { when (v.id) {
R.id.imageView_add -> listener.onItemEnabledChanged(item, true) R.id.imageView_add -> listener.onItemEnabledChanged(item, true)
R.id.imageView_remove -> listener.onItemEnabledChanged(item, false) R.id.imageView_remove -> listener.onItemEnabledChanged(item, false)
R.id.imageView_menu -> showSourceMenu(v, item, listener) 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)
}
} }
} }
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( fun sourceConfigTipDelegate(
listener: OnTipCloseListener<SourceConfigItem.Tip>, listener: OnTipCloseListener<SourceConfigItem.Tip>,
@@ -208,6 +169,7 @@ private fun showSourceMenu(
menu.inflate(R.menu.popup_source_config) menu.inflate(R.menu.popup_source_config)
menu.menu.findItem(R.id.action_shortcut) menu.menu.findItem(R.id.action_shortcut)
?.isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(anchor.context) ?.isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(anchor.context)
menu.menu.findItem(R.id.action_lift)?.isVisible = item.isDraggable
menu.setOnMenuItemClickListener { menu.setOnMenuItemClickListener {
when (it.itemId) { when (it.itemId) {
R.id.action_settings -> listener.onItemSettingsClick(item) R.id.action_settings -> listener.onItemSettingsClick(item)

View File

@@ -12,6 +12,4 @@ interface SourceConfigListener : OnTipCloseListener<SourceConfigItem.Tip> {
fun onItemShortcutClick(item: SourceConfigItem.SourceItem) fun onItemShortcutClick(item: SourceConfigItem.SourceItem)
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
fun onHeaderClick(header: SourceConfigItem.LocaleGroup)
} }

View File

@@ -0,0 +1,30 @@
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,
val showSummary: Boolean,
) : 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
}
}
}

View File

@@ -0,0 +1,79 @@
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.getSummary
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
}
if (item.showSummary) {
binding.textViewDescription.text = item.source.getSummary(context)
binding.textViewDescription.isVisible = true
} else {
binding.textViewDescription.isVisible = false
}
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)
}
}

View File

@@ -0,0 +1,123 @@
package org.koitharu.kotatsu.settings.sources.catalog
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
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, MenuItem.OnActionExpandListener {
@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, this))
}
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
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
viewBinding.tabs.isVisible = false
val sq = (item.actionView as? SearchView)?.query?.trim()?.toString().orEmpty()
viewModel.performSearch(sq)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
viewBinding.tabs.isVisible = true
viewModel.performSearch(null)
return true
}
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)
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,105 @@
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? = null
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()
when (val q = query) {
null -> sources.retainAll { it.contentType == contentType && it.locale == locale }
"" -> return emptyList()
else -> sources.retainAll { it.title.contains(q, ignoreCase = true) }
}
return if (sources.isEmpty()) {
listOf(
if (query == null) {
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,
showSummary = query != null,
)
}
}
}
@AssistedFactory
interface Factory {
fun create(
locale: String?,
contentType: ContentType,
lifecycle: ViewModelLifecycle,
): SourcesCatalogListProducer
}
}

View File

@@ -0,0 +1,75 @@
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,
private val expandListener: MenuItem.OnActionExpandListener,
) : 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 expandListener.onMenuItemActionExpand(item)
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
(item.actionView as SearchView).setQuery("", false)
return expandListener.onMenuItemActionCollapse(item)
}
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.performSearch(newText?.trim().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()
}
}

View File

@@ -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,
settings: AppSettings,
) : BaseViewModel() {
private val lifecycle = RetainedLifecycleImpl()
private var searchQuery: String? = null
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())
}
}

View File

@@ -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 androidx.room.InvalidationTracker
import dagger.hilt.android.ViewModelLifecycle import dagger.hilt.android.ViewModelLifecycle
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
@@ -15,19 +14,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_SOURCES 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.model.isNsfw
import org.koitharu.kotatsu.core.prefs.AppSettings 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.lifecycleScope
import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.toEnumSet import org.koitharu.kotatsu.core.util.ext.toEnumSet
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem import org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import java.util.Locale
import java.util.TreeMap
import javax.inject.Inject import javax.inject.Inject
@ViewModelScoped @ViewModelScoped
@@ -39,7 +32,6 @@ class SourcesListProducer @Inject constructor(
private val scope = lifecycle.lifecycleScope private val scope = lifecycle.lifecycleScope
private var query: String = "" private var query: String = ""
private val expanded = HashSet<String?>()
val list = MutableStateFlow(emptyList<SourceConfigItem>()) val list = MutableStateFlow(emptyList<SourceConfigItem>())
private var job = scope.launch(Dispatchers.Default) { private var job = scope.launch(Dispatchers.Default) {
@@ -67,27 +59,19 @@ class SourcesListProducer @Inject constructor(
onInvalidated(emptySet()) onInvalidated(emptySet())
} }
fun expandCollapse(group: String?) {
if (!expanded.remove(group)) {
expanded.add(group)
}
onInvalidated(emptySet())
}
private suspend fun buildList(): List<SourceConfigItem> { private suspend fun buildList(): List<SourceConfigItem> {
val allSources = repository.allMangaSources
val enabledSources = repository.getEnabledSources() val enabledSources = repository.getEnabledSources()
val isNsfwDisabled = settings.isNsfwContentDisabled 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() val enabledSet = enabledSources.toEnumSet()
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
return allSources.mapNotNull { return enabledSources.mapNotNull {
if (!it.title.contains(query, ignoreCase = true)) { if (!it.title.contains(query, ignoreCase = true)) {
return@mapNotNull null return@mapNotNull null
} }
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = it, source = it,
summary = it.getLocaleTitle(),
isEnabled = it in enabledSet, isEnabled = it in enabledSet,
isDraggable = false, isDraggable = false,
isAvailable = !isNsfwDisabled || !it.isNsfw(), isAvailable = !isNsfwDisabled || !it.isNsfw(),
@@ -96,17 +80,8 @@ class SourcesListProducer @Inject constructor(
listOf(SourceConfigItem.EmptySearchResult) listOf(SourceConfigItem.EmptySearchResult)
} }
} }
val map = allSources.groupByTo(TreeMap(LocaleKeyComparator())) { val result = ArrayList<SourceConfigItem>(enabledSources.size + 1)
if (it in enabledSet) {
KEY_ENABLED
} else {
it.locale
}
}
map.remove(KEY_ENABLED)
val result = ArrayList<SourceConfigItem>(allSources.size + map.size + 2)
if (enabledSources.isNotEmpty()) { if (enabledSources.isNotEmpty()) {
result += SourceConfigItem.Header(R.string.enabled_sources)
if (withTip) { if (withTip) {
result += SourceConfigItem.Tip( result += SourceConfigItem.Tip(
TIP_REORDER, TIP_REORDER,
@@ -117,70 +92,17 @@ class SourcesListProducer @Inject constructor(
enabledSources.mapTo(result) { enabledSources.mapTo(result) {
SourceConfigItem.SourceItem( SourceConfigItem.SourceItem(
source = it, source = it,
summary = it.getLocaleTitle(),
isEnabled = true, isEnabled = true,
isDraggable = true, isDraggable = isReorderAvailable,
isAvailable = false, 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 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 { 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" const val TIP_REORDER = "src_reorder"
} }
} }

View File

@@ -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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu 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.databinding.FragmentSettingsSourcesBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.settings.SettingsActivity 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.SourceConfigAdapter
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener 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 org.koitharu.kotatsu.settings.sources.model.SourceConfigItem
import javax.inject.Inject import javax.inject.Inject
@@ -77,14 +80,14 @@ class SourcesManageFragment :
viewModel.content.observe(viewLifecycleOwner, sourcesAdapter) viewModel.content.observe(viewLifecycleOwner, sourcesAdapter)
viewModel.onActionDone.observeEvent( viewModel.onActionDone.observeEvent(
viewLifecycleOwner, viewLifecycleOwner,
ReversibleActionObserver(binding.recyclerView) ReversibleActionObserver(binding.recyclerView),
) )
addMenuProvider(SourcesMenuProvider()) addMenuProvider(SourcesMenuProvider())
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
activity?.setTitle(R.string.remote_sources) activity?.setTitle(R.string.manage_sources)
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -119,10 +122,6 @@ class SourcesManageFragment :
viewModel.setEnabled(item.source, isEnabled) viewModel.setEnabled(item.source, isEnabled)
} }
override fun onHeaderClick(header: SourceConfigItem.LocaleGroup) {
viewModel.expandOrCollapse(header.localeId)
}
override fun onCloseTip(tip: SourceConfigItem.Tip) { override fun onCloseTip(tip: SourceConfigItem.Tip) {
viewModel.onTipClosed(tip) viewModel.onTipClosed(tip)
} }
@@ -143,6 +142,11 @@ class SourcesManageFragment :
} }
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { 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 -> { R.id.action_disable_all -> {
viewModel.disableAll() viewModel.disableAll()
true true

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.settings.sources package org.koitharu.kotatsu.settings.sources.manage
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -103,10 +103,6 @@ class SourcesManageViewModel @Inject constructor(
} }
} }
fun expandOrCollapse(headerId: String?) {
listProducer.expandCollapse(headerId)
}
fun performSearch(query: String?) { fun performSearch(query: String?) {
listProducer.setQuery(query?.trim().orEmpty()) listProducer.setQuery(query?.trim().orEmpty())
} }

View File

@@ -2,45 +2,15 @@ package org.koitharu.kotatsu.settings.sources.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface SourceConfigItem : ListModel { 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( data class SourceItem(
val source: MangaSource, val source: MangaSource,
val isEnabled: Boolean, val isEnabled: Boolean,
val summary: String?,
val isDraggable: Boolean, val isDraggable: Boolean,
val isAvailable: Boolean, val isAvailable: Boolean,
) : SourceConfigItem { ) : SourceConfigItem {
@@ -51,14 +21,6 @@ sealed interface SourceConfigItem : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is SourceItem && other.source == source 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( data class Tip(

View File

@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.settings.utils package org.koitharu.kotatsu.settings.utils
import android.content.Context import android.content.Context
import android.text.method.LinkMovementMethod
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.TextView import android.widget.TextView
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
@@ -13,11 +13,9 @@ class LinksPreference @JvmOverloads constructor(
defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle,
defStyleRes: Int = 0, defStyleRes: Int = 0,
) : Preference(context, attrs, defStyleAttr, defStyleRes) { ) : Preference(context, attrs, defStyleAttr, defStyleRes) {
override fun onBindViewHolder(holder: PreferenceViewHolder) { override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder) super.onBindViewHolder(holder)
val summaryView = holder.findViewById(android.R.id.summary) as TextView val summaryView = holder.findViewById(android.R.id.summary) as TextView
summaryView.movementMethod = LinkMovementMethod.getInstance() summaryView.movementMethod = LinkMovementMethodCompat.getInstance()
} }
} }

View 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>

View File

@@ -22,6 +22,17 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> 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 <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_top" android:id="@+id/appbar_top"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -12,6 +12,17 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:background="@color/grey" /> 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 <org.koitharu.kotatsu.reader.ui.ReaderInfoBarView
android:id="@+id/infoBar" android:id="@+id/infoBar"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -62,9 +73,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:stepSize="1" android:stepSize="1"
android:valueFrom="0" android:valueFrom="0"
app:trackColorInactive="?attr/m3ColorBackground"
app:labelBehavior="floating" app:labelBehavior="floating"
app:tickVisible="false" /> app:tickVisible="false"
app:trackColorInactive="?attr/m3ColorBackground" />
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>

View 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>

View File

@@ -2,7 +2,6 @@
<org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame <org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/frame" android:id="@+id/frame"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -17,14 +16,4 @@
android:orientation="vertical" android:orientation="vertical"
app:layoutManager="org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonLayoutManager" /> 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> </org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonScalingFrame>

View File

@@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/list_selector" 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 <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_icon" android:id="@+id/imageView_icon"
@@ -23,19 +26,32 @@
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<TextView <LinearLayout
android:id="@+id/textView_title" android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="@dimen/margin_small"
android:ellipsize="end" android:orientation="vertical">
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" />
</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>

View File

@@ -27,14 +27,4 @@
<include layout="@layout/layout_page_info" /> <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> </FrameLayout>

View File

@@ -4,10 +4,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/listPreferredItemHeightSmall" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:gravity="center_vertical" 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 <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover" android:id="@+id/imageView_cover"
@@ -18,17 +20,34 @@
app:shapeAppearance="?shapeAppearanceCornerSmall" app:shapeAppearance="?shapeAppearanceCornerSmall"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<TextView <LinearLayout
android:id="@+id/textView_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/margin_small" android:layout_marginEnd="@dimen/margin_small"
android:layout_weight="1" android:layout_weight="1"
android:ellipsize="end" android:orientation="vertical">
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyMedium" <TextView
tools:text="@tools:sample/lorem[2]" /> 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 <View
android:layout_width="1dp" android:layout_width="1dp"
@@ -42,4 +61,4 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingHorizontal="?listPreferredItemPaddingEnd" /> android:paddingHorizontal="?listPreferredItemPaddingEnd" />
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,66 @@
<?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" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="?android:listPreferredItemPaddingStart"
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[15]" />
<TextView
android:id="@+id/textView_description"
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="English" />
</LinearLayout>
<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>

View File

@@ -7,6 +7,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:windowBackground" android:background="?android:windowBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeightSmall"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingVertical="@dimen/margin_small" android:paddingVertical="@dimen/margin_small"
android:paddingStart="?listPreferredItemPaddingStart" android:paddingStart="?listPreferredItemPaddingStart"
@@ -36,7 +37,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge" android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[15]" /> tools:text="@tools:sample/lorem[15]" />
<TextView <TextView

View File

@@ -48,6 +48,22 @@
app:drawableStartCompat="@drawable/ic_screen_rotation" app:drawableStartCompat="@drawable/ic_screen_rotation"
tools:visibility="visible" /> 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 <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -119,18 +135,35 @@
android:textColor="?colorOnSurfaceVariant" android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_timer" /> app:drawableStartCompat="@drawable/ic_timer" />
<TextView <LinearLayout
android:id="@+id/label_timer" android:id="@+id/layout_timer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal" android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small" android:layout_marginTop="@dimen/margin_normal"
android:text="@string/speed" android:textAppearance="?textAppearanceTitleSmall"
android:textAppearance="?attr/textAppearanceBodySmall"
android:visibility="gone" 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:id="@+id/slider_timer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -21,10 +21,16 @@
android:title="@string/save" android:title="@string/save"
app:showAsAction="ifRoom|withText" /> app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_favourite"
android:icon="@drawable/ic_heart"
android:title="@string/categories"
app:showAsAction="ifRoom|withText" />
<item <item
android:id="@+id/action_select_all" android:id="@+id/action_select_all"
android:icon="?actionModeSelectAllDrawable" android:icon="?actionModeSelectAllDrawable"
android:title="@android:string/selectAll" android:title="@android:string/selectAll"
app:showAsAction="ifRoom|withText" /> app:showAsAction="ifRoom|withText" />
</menu> </menu>

View File

@@ -3,9 +3,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:id="@+id/action_grid" android:id="@+id/action_manage"
android:checkable="true" android:title="@string/manage_sources"
android:title="@string/show_in_grid_view" android:titleCondensed="@string/manage" />
android:titleCondensed="@string/grid" />
</menu> </menu>

View File

@@ -3,6 +3,12 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_catalog"
android:icon="@drawable/ic_add"
android:title="@string/sources_catalog"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/action_search" android:id="@+id/action_search"
android:icon="?actionModeWebSearchDrawable" android:icon="?actionModeWebSearchDrawable"

View 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>

View File

@@ -494,4 +494,30 @@
<string name="enhanced_colors">32-бітны каляровы рэжым</string> <string name="enhanced_colors">32-бітны каляровы рэжым</string>
<string name="suggest_new_sources_summary">Прапаноўваць крыніцы мангі, дададзеныя ў апошнім абнаўленні праграмы</string> <string name="suggest_new_sources_summary">Прапаноўваць крыніцы мангі, дададзеныя ў апошнім абнаўленні праграмы</string>
<string name="online_variant">Анлайн варыянт</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>
<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="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> </resources>

View File

@@ -503,4 +503,21 @@
<string name="frequency_twice_per_month">Dos veces al mes</string> <string name="frequency_twice_per_month">Dos veces al mes</string>
<string name="frequency_once_per_month">Una vez 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="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> </resources>

View File

@@ -503,4 +503,21 @@
<string name="frequency_twice_per_month">Dalawang beses bawat buwan</string> <string name="frequency_twice_per_month">Dalawang beses bawat buwan</string>
<string name="frequency_once_per_month">Isang 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="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> </resources>

View File

@@ -9,13 +9,13 @@
<string name="grid">Kisi</string> <string name="grid">Kisi</string>
<string name="list_mode">Mode daftar</string> <string name="list_mode">Mode daftar</string>
<string name="settings">Pengaturan</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="loading_">Memuat…</string>
<string name="computing_">Menghitung…</string> <string name="computing_">Menghitung…</string>
<string name="chapter_d_of_d">Bab %1$d dari %2$d</string> <string name="chapter_d_of_d">Bab %1$d dari %2$d</string>
<string name="close">Tutup</string> <string name="close">Tutup</string>
<string name="try_again">Coba lagi</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="history_is_empty">Belum ada riwayat</string>
<string name="read">Baca</string> <string name="read">Baca</string>
<string name="you_have_not_favourites_yet">Belum ada favorit</string> <string name="you_have_not_favourites_yet">Belum ada favorit</string>
@@ -37,8 +37,8 @@
<string name="updated">Diperbarui</string> <string name="updated">Diperbarui</string>
<string name="newest">Terbaru</string> <string name="newest">Terbaru</string>
<string name="by_rating">Peringkat</string> <string name="by_rating">Peringkat</string>
<string name="sort_order">Urutan penyortiran</string> <string name="sort_order">Urutkan berdasarkan</string>
<string name="filter">Saring</string> <string name="filter">Filter</string>
<string name="theme">Tema</string> <string name="theme">Tema</string>
<string name="light">Terang</string> <string name="light">Terang</string>
<string name="dark">Gelap</string> <string name="dark">Gelap</string>
@@ -478,4 +478,31 @@
<string name="directories">Direktori</string> <string name="directories">Direktori</string>
<string name="main_screen_sections">Bagian layar utama</string> <string name="main_screen_sections">Bagian layar utama</string>
<string name="to_top">Ke atas</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> </resources>

View 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>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="text_search_holder_secondary">Сұрауыңызды қайталап көріңіз.</string> <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="text_local_holder_secondary">Файл импорттаңыз не онлайн каталогтан бірдеңе сақтаңыз.</string>
<string name="other_storage">Басқа бума</string> <string name="other_storage">Басқа бума</string>
<string name="new_version_s">Жаңа нұсқа: %s</string> <string name="new_version_s">Жаңа нұсқа: %s</string>
@@ -15,7 +15,7 @@
<string name="new_chapters">Жаңа тараулар</string> <string name="new_chapters">Жаңа тараулар</string>
<string name="local_storage">Құрылғыда</string> <string name="local_storage">Құрылғыда</string>
<string name="history">Тарих</string> <string name="history">Тарих</string>
<string name="chapters">Тараулар</string> <string name="chapters">Тарау</string>
<string name="detailed_list">Егжей-тегжейлі тізім</string> <string name="detailed_list">Егжей-тегжейлі тізім</string>
<string name="list_mode">Тізім түрі</string> <string name="list_mode">Тізім түрі</string>
<string name="remote_sources">Маңга қайнары</string> <string name="remote_sources">Маңга қайнары</string>
@@ -23,8 +23,8 @@
<string name="computing_">Есептеу…</string> <string name="computing_">Есептеу…</string>
<string name="favourites">Таңдаулы</string> <string name="favourites">Таңдаулы</string>
<string name="network_error">Желі қатесі</string> <string name="network_error">Желі қатесі</string>
<string name="details">Деректер</string> <string name="details">Дерек</string>
<string name="grid">Тор</string> <string name="grid">Кесте</string>
<string name="try_again">Қайта көру</string> <string name="try_again">Қайта көру</string>
<string name="clear_history">Тарихты тазалау</string> <string name="clear_history">Тарихты тазалау</string>
<string name="read">Оқу</string> <string name="read">Оқу</string>
@@ -61,18 +61,18 @@
<string name="text_file_sizes">Б|кБ|МБ|ГБ|ТБ</string> <string name="text_file_sizes">Б|кБ|МБ|ГБ|ТБ</string>
<string name="standard">Стандарт</string> <string name="standard">Стандарт</string>
<string name="webtoon">Уебтүн</string> <string name="webtoon">Уебтүн</string>
<string name="grid_size">Тор өлшемі</string> <string name="grid_size">Кесте өлшемі</string>
<string name="search_on_s">%s-те іздеу</string> <string name="search_on_s">%s-те іздеу</string>
<string name="delete_manga">Маңганы жою</string> <string name="delete_manga">Маңганы жою</string>
<string name="reader_settings">Оқыманы баптау</string> <string name="reader_settings">Оқыманы баптау</string>
<string name="switch_pages">Бет ауыстыру</string> <string name="switch_pages">Парақтау</string>
<string name="taps_on_edges">Шет жақты түру</string> <string name="taps_on_edges">Шет жақты түру</string>
<string name="clear_thumbs_cache">Нобай кәшін тазалау</string> <string name="clear_thumbs_cache">Нобай кәшін тазалау</string>
<string name="search_history_cleared">Тазаланды</string> <string name="search_history_cleared">Тазаланды</string>
<string name="gestures_only">Ым ғана</string> <string name="gestures_only">Ым ғана</string>
<string name="internal_storage">Ішкі жады</string> <string name="internal_storage">Ішкі жады</string>
<string name="external_storage">Сыртқы жады</string> <string name="external_storage">Сыртқы жады</string>
<string name="domain">Домен</string> <string name="domain">Дәмейін</string>
<string name="app_update_available">Жаңа нұсқа қолжетімді</string> <string name="app_update_available">Жаңа нұсқа қолжетімді</string>
<string name="open_in_browser">Уеб браузер арқылы ашу</string> <string name="open_in_browser">Уеб браузер арқылы ашу</string>
<string name="save_manga">Сақтау</string> <string name="save_manga">Сақтау</string>
@@ -92,7 +92,7 @@
<string name="text_local_holder_primary">Алдымен бірдеңе сақтаңыз</string> <string name="text_local_holder_primary">Алдымен бірдеңе сақтаңыз</string>
<string name="manga_shelf">Сөре</string> <string name="manga_shelf">Сөре</string>
<string name="recent_manga">Соңғы</string> <string name="recent_manga">Соңғы</string>
<string name="pages_animation">Бет анимациясы</string> <string name="pages_animation">Бет анимасасы</string>
<string name="manga_save_location">Жүктеу бумасы</string> <string name="manga_save_location">Жүктеу бумасы</string>
<string name="not_available">Қолжетімсіз</string> <string name="not_available">Қолжетімсіз</string>
<string name="cannot_find_available_storage">Қолжетімді бума жоқ</string> <string name="cannot_find_available_storage">Қолжетімді бума жоқ</string>
@@ -208,4 +208,316 @@
<string name="protect_application_subtitle">Қолданбаға кіру үшін құпиясөз енгізіңіз</string> <string name="protect_application_subtitle">Қолданбаға кіру үшін құпиясөз енгізіңіз</string>
<string name="confirm">Растау</string> <string name="confirm">Растау</string>
<string name="password_length_hint">Құпиясөзде 4, не одан көп таңба болу керек</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">Қате мәліметі:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Дереккөзде бар ма екенін тексеру үшін &lt;a href=%2$s&gt;маңганы уеб браузер арқылы&lt;/a&gt; ашып көріңіз&lt;br&gt;2. &lt;a href=kotatsu://about&gt;Kotatsu-ның ең соңғы нұсқасын&lt;/a&gt;&lt;br&gt;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> </resources>

View File

@@ -483,4 +483,25 @@
<string name="show">Mostrar</string> <string name="show">Mostrar</string>
<string name="color_black">Preto</string> <string name="color_black">Preto</string>
<string name="this_month">Esse mês</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> </resources>

View File

@@ -504,4 +504,20 @@
<string name="frequency_once_per_month">Один раз в месяц</string> <string name="frequency_once_per_month">Один раз в месяц</string>
<string name="backups_output_directory">Каталог для сохранения резервных копий</string> <string name="backups_output_directory">Каталог для сохранения резервных копий</string>
<string name="last_successful_backup">Последняя резервная копия: %s</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>
<string name="source_summary_pattern">%1$s, %2$s</string>
</resources> </resources>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="light">สว่าง</string> <string name="light">สว่าง</string>
<string name="automatic">ตั้งค่าตามเครื่อง</string> <string name="automatic">ตั้งค่าตามเครื่อง</string>
<string name="text_clear_history_prompt">จะเคลียร์ประวัติการอ่านทั้งหมดแบบถาวรใช่ไหม\?</string> <string name="text_clear_history_prompt">จะเคลียร์ประวัติการอ่านทั้งหมดแบบถาวรใช่ไหม\?</string>
@@ -160,7 +160,7 @@
<string name="zoom_mode_fit_center">พอดีตรงกลาง</string> <string name="zoom_mode_fit_center">พอดีตรงกลาง</string>
<string name="zoom_mode_fit_height">พอดีกับความสูง</string> <string name="zoom_mode_fit_height">พอดีกับความสูง</string>
<string name="black_dark_theme">ดำ</string> <string name="black_dark_theme">ดำ</string>
<string name="just_now">เมื่อกี้</string> <string name="just_now">เมื่อเร็วนี้</string>
<string name="clear_feed">เคลียร์ฟีด</string> <string name="clear_feed">เคลียร์ฟีด</string>
<string name="backup_restore">สำรองและคืนค่า</string> <string name="backup_restore">สำรองและคืนค่า</string>
<string name="create_backup">สร้างข้อมูลสำรอง</string> <string name="create_backup">สร้างข้อมูลสำรอง</string>
@@ -344,4 +344,29 @@
<string name="exit_confirmation_summary">กดย้อนกลับสองครั้งเพื่อออกจากแอป</string> <string name="exit_confirmation_summary">กดย้อนกลับสองครั้งเพื่อออกจากแอป</string>
<string name="bookmarks_removed">ลบบุ๊คมาร์กแล้ว</string> <string name="bookmarks_removed">ลบบุ๊คมาร์กแล้ว</string>
<string name="error_no_space_left">ไม่มีพื้นที่เหลือบนอุปกรณ์แล้ว</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> </resources>

View File

@@ -503,4 +503,6 @@
<string name="frequency_twice_per_month">Ayda 2 kere</string> <string name="frequency_twice_per_month">Ayda 2 kere</string>
<string name="frequency_once_per_month">Ayda 1 kere</string> <string name="frequency_once_per_month">Ayda 1 kere</string>
<string name="backups_output_directory">Yedekleme dizini</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> </resources>

View File

@@ -494,4 +494,30 @@
<string name="enhanced_colors">32-бітний колірний режим</string> <string name="enhanced_colors">32-бітний колірний режим</string>
<string name="suggest_new_sources_summary">Пропонує включити нові джерела манґи після оновлення застосунку</string> <string name="suggest_new_sources_summary">Пропонує включити нові джерела манґи після оновлення застосунку</string>
<string name="online_variant">Онлайн варіант</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>
<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="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> </resources>

View File

@@ -17,16 +17,16 @@
<string name="silent">无声</string> <string name="silent">无声</string>
<string name="preparing_">准备…</string> <string name="preparing_">准备…</string>
<string name="file_not_found">未找到文件</string> <string name="file_not_found">未找到文件</string>
<string name="yesterday">昨日</string> <string name="yesterday">昨天</string>
<string name="backup_information">你可以创建你的历史和收藏的备份并恢复它</string> <string name="backup_information">你可以创建你的历史和收藏的备份并恢复它</string>
<string name="just_now">刚刚</string> <string name="just_now">刚刚</string>
<string name="long_ago">很久以前</string> <string name="long_ago">很久以前</string>
<string name="group">分组</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="reader_mode_hint">所选配置将被这部漫画记住</string>
<string name="captcha_required">需要验证码</string> <string name="captcha_required">需要验证码</string>
<string name="captcha_solve">解决</string> <string name="captcha_solve">解决</string>
<string name="today">今日</string> <string name="today">今天</string>
<string name="clear_cookies">清除cookies</string> <string name="clear_cookies">清除cookies</string>
<string name="new_sources_text">有新的漫画源可用</string> <string name="new_sources_text">有新的漫画源可用</string>
<string name="suggestions_summary">根据你的喜好推荐漫画</string> <string name="suggestions_summary">根据你的喜好推荐漫画</string>
@@ -79,7 +79,7 @@
<string name="clear">清除</string> <string name="clear">清除</string>
<string name="text_clear_history_prompt">永久清除所有阅读历史\?</string> <string name="text_clear_history_prompt">永久清除所有阅读历史\?</string>
<string name="remove">删除</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="save_page">保存页面</string>
<string name="page_saved">保存</string> <string name="page_saved">保存</string>
<string name="share_image">分享图片</string> <string name="share_image">分享图片</string>
@@ -110,8 +110,8 @@
<string name="internal_storage">内部存储</string> <string name="internal_storage">内部存储</string>
<string name="external_storage">外部存储</string> <string name="external_storage">外部存储</string>
<string name="domain">范围</string> <string name="domain">范围</string>
<string name="app_update_available">新版本应用程序已经推出</string> <string name="app_update_available">发现新版本</string>
<string name="open_in_browser">在网络浏览器中打开</string> <string name="open_in_browser">在浏览器中打开</string>
<string name="large_manga_save_confirm">这部漫画有 %s 。全部保存?</string> <string name="large_manga_save_confirm">这部漫画有 %s 。全部保存?</string>
<string name="save_manga">保存</string> <string name="save_manga">保存</string>
<string name="notifications">通知</string> <string name="notifications">通知</string>
@@ -122,7 +122,7 @@
<string name="light_indicator">LED指示器</string> <string name="light_indicator">LED指示器</string>
<string name="vibration">振动</string> <string name="vibration">振动</string>
<string name="favourites_categories">收藏分类</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_empty_holder_primary">这里有点空…</string>
<string name="text_search_holder_secondary">尝试重新表述查询。</string> <string name="text_search_holder_secondary">尝试重新表述查询。</string>
<string name="text_history_holder_primary">你看过的内容将在这里显示</string> <string name="text_history_holder_primary">你看过的内容将在这里显示</string>
@@ -146,9 +146,9 @@
<string name="new_version_s">新版本: %s</string> <string name="new_version_s">新版本: %s</string>
<string name="clear_updates_feed">清除订阅更新记录</string> <string name="clear_updates_feed">清除订阅更新记录</string>
<string name="updates_feed_cleared">已清除</string> <string name="updates_feed_cleared">已清除</string>
<string name="rotate_screen">旋转屏幕</string> <string name="rotate_screen">屏幕旋转</string>
<string name="update">更新</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="track_sources">查找更新</string>
<string name="dont_check">不要检查</string> <string name="dont_check">不要检查</string>
<string name="enter_password">输入密码</string> <string name="enter_password">输入密码</string>
@@ -160,7 +160,7 @@
<string name="about">关于</string> <string name="about">关于</string>
<string name="app_version">版本%s</string> <string name="app_version">版本%s</string>
<string name="check_for_updates">检查更新</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="right_to_left">从右到左</string>
<string name="create_category">新分类</string> <string name="create_category">新分类</string>
<string name="scale_mode">缩放模式</string> <string name="scale_mode">缩放模式</string>
@@ -245,7 +245,7 @@
<string name="bookmark_added">书签已添加</string> <string name="bookmark_added">书签已添加</string>
<string name="undo">撤销</string> <string name="undo">撤销</string>
<string name="removed_from_history">从历史中删除</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="default_mode">默认模式</string>
<string name="detect_reader_mode">自动检测阅读器模式</string> <string name="detect_reader_mode">自动检测阅读器模式</string>
<string name="detect_reader_mode_summary">自动检测漫画是否为条漫</string> <string name="detect_reader_mode_summary">自动检测漫画是否为条漫</string>
@@ -369,7 +369,7 @@
<string name="allow_unstable_updates">允许不稳定更新</string> <string name="allow_unstable_updates">允许不稳定更新</string>
<string name="allow_unstable_updates_summary">接收不稳定版本更新的通知</string> <string name="allow_unstable_updates_summary">接收不稳定版本更新的通知</string>
<string name="download_started">下载已开始</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="settings_apply_restart_required">要应用这些更改请重启程序</string>
<string name="sources_reorder_tip">点击并长按项目排序</string> <string name="sources_reorder_tip">点击并长按项目排序</string>
<string name="got_it">知道了</string> <string name="got_it">知道了</string>
@@ -451,7 +451,7 @@
<string name="progress">阅读进度</string> <string name="progress">阅读进度</string>
<string name="error_corrupted_file">无效数据回传或文件已损坏</string> <string name="error_corrupted_file">无效数据回传或文件已损坏</string>
<string name="related_manga_summary">显示相关漫画。可能并不准确或缺失</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="order_added">添加日期</string>
<string name="on_device">本地</string> <string name="on_device">本地</string>
<string name="moved_to_top">移动到顶部</string> <string name="moved_to_top">移动到顶部</string>
@@ -468,7 +468,7 @@
<string name="advanced">高级</string> <string name="advanced">高级</string>
<string name="color_dark">深色</string> <string name="color_dark">深色</string>
<string name="too_many_requests_message">请求次数过多,稍候再尝试</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="default_section">默认栏目</string>
<string name="background">阅读背景色</string> <string name="background">阅读背景色</string>
<string name="manga_list">漫画列表</string> <string name="manga_list">漫画列表</string>
@@ -488,4 +488,17 @@
<string name="enhanced_colors_summary">减少色带,但可能会影响性能</string> <string name="enhanced_colors_summary">减少色带,但可能会影响性能</string>
<string name="enhanced_colors">32位色彩模式</string> <string name="enhanced_colors">32位色彩模式</string>
<string name="suggest_new_sources_summary">应用更新后快速启用新增的漫画源</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> </resources>

Some files were not shown because too many files have changed in this diff Show More