Compare commits

..

59 Commits

Author SHA1 Message Date
3b7d83dd6f Bump version 2023-12-31 10:59:30 +02:00
877a018ced Fix crashes 2023-12-31 10:28:46 +02:00
2e80b330e9 Fix tracker duplicates 2023-12-31 10:17:43 +02:00
42ca38e693 Update dependencies 2023-12-31 10:06:48 +02:00
Feroli
d2fc3354af Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Feroli <feroli@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt_BR/
Translation: Kotatsu/plurals
2023-12-31 10:03:25 +02:00
2a870e6167 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (548 of 548 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-12-31 10:03:25 +02:00
gallegonovato
393a9c2791 Translated using Weblate (Spanish)
Currently translated at 100.0% (548 of 548 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-12-31 10:03:25 +02:00
dont wana say
4c69839076 Translated using Weblate (Estonian)
Currently translated at 75.5% (414 of 548 strings)

Added translation using Weblate (Estonian)

Added translation using Weblate (Estonian)

Co-authored-by: dont wana say <273ex2vl6@mozmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/et/
Translation: Kotatsu/Strings
2023-12-31 10:03:25 +02:00
e37455e790 Update parsers and add support for Upcoming state 2023-12-28 17:48:00 +02:00
Paper Jack
36259ba901 Translated using Weblate (Italian)
Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Paper Jack <paperjack@tutanota.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2023-12-28 16:50:47 +02:00
Nicolò Bertazzo
5b041b9a49 Translated using Weblate (Italian)
Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Nicolò Bertazzo <n.bertazzo@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2023-12-28 16:50:47 +02:00
1734e888d6 Move new sources tip to catalog 2023-12-26 20:24:14 +02:00
9108646cea Update parsers 2023-12-25 19:52:37 +02:00
Макар Разин
c6d1cf2f72 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (546 of 546 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (546 of 546 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (546 of 546 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-12-25 13:26:51 +02:00
Anon
a317236cb0 Translated using Weblate (Serbian)
Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2023-12-25 13:26:51 +02:00
Deivinni Silva
3703c07a98 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Deivinni Silva <deivinnimds3656@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2023-12-25 13:26:51 +02:00
b7b32dd447 Update parsers 2023-12-25 13:18:28 +02:00
c103386dc5 Fix crash on file size compution 2023-12-25 10:01:43 +02:00
a9d6ee4a95 Update dependencies 2023-12-25 09:57:01 +02:00
71f2c91e5a Improve sources catalog 2023-12-23 08:44:09 +02:00
b878f358ff Improve explore screen 2023-12-23 07:26:31 +02:00
Deivinni Silva
4c9989da78 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Deivinni Silva <deivinnimds3656@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2023-12-20 13:27:57 +02:00
ssantos
8e8424022a Translated using Weblate (Portuguese)
Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: ssantos <ssantos@web.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2023-12-20 13:27:57 +02:00
86504b8bde Update parsers 2023-12-20 13:26:33 +02:00
f082fa084f Recreate splash screen 2023-12-19 21:36:23 +03:00
040fe258e9 Fix ActionMode popup coloring 2023-12-19 20:20:20 +03:00
1076009572 Temporary fix checking for new chapters on large collections #584 2023-12-19 17:37:23 +02:00
40dde71a1d Update parsers 2023-12-19 17:37:23 +02:00
Himawariin
9503aabf78 Translated using Weblate (Filipino)
Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Himawariin <milkytrackz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-12-19 17:32:43 +02:00
Deivinni Silva
4ee16cfa2f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Deivinni Silva <deivinnimds3656@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-12-19 17:32:43 +02:00
f1c9eacaf0 Fix color correction apply buttons #597 2023-12-18 18:38:48 +02:00
920e16be10 Merge branch 'master' into devel 2023-12-16 08:46:02 +02:00
4e0e5be726 Update parsers
(cherry picked from commit 451a155e08)
2023-12-16 08:13:24 +02:00
Weblate (bot)
f18eca52af Translations update from Hosted Weblate (#594)
* Translated using Weblate (Thai)

Currently translated at 71.1% (387 of 544 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings

* Translated using Weblate (Spanish)

Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings

* Translated using Weblate (Turkish)

Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings

* Translated using Weblate (Russian)

Currently translated at 99.8% (545 of 546 strings)

Co-authored-by: Hotarun <ihotarun@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings

---------

Co-authored-by: Nayuki <me@nayuki.cyou>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Hotarun <ihotarun@proton.me>
(cherry picked from commit 0612a7ad2c)
2023-12-16 08:13:17 +02:00
451a155e08 Update parsers 2023-12-16 08:12:42 +02:00
Weblate (bot)
0612a7ad2c Translations update from Hosted Weblate (#594)
* Translated using Weblate (Thai)

Currently translated at 71.1% (387 of 544 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings

* Translated using Weblate (Spanish)

Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings

* Translated using Weblate (Turkish)

Currently translated at 100.0% (546 of 546 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings

* Translated using Weblate (Russian)

Currently translated at 99.8% (545 of 546 strings)

Co-authored-by: Hotarun <ihotarun@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings

---------

Co-authored-by: Nayuki <me@nayuki.cyou>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Hotarun <ihotarun@proton.me>
2023-12-15 23:26:31 +03:00
9c34f25eda Fix navBar color in details screen 2023-12-15 23:02:21 +03:00
42360c678f Revert BottomNav and CTBLayout styles 2023-12-15 22:25:24 +03:00
HotarunIchijou
04a3d02aa9 Minority UI color changes (#593)
* Transparent navigation bar

* bump com.android.material to 1.11.0 and androidx.activity to 1.8.2

* bunp gradle to 8.2.0

* color changes for dynamic theme

* removed unused things
2023-12-15 22:14:16 +03:00
808fd13ad0 Fix saved chapters mapping 2023-12-14 15:16:15 +02:00
88c8dc4761 Fix crashes 2023-12-14 14:50:56 +02:00
a7eba67a97 Webtoon reader fixes 2023-12-14 14:36:03 +02:00
c27586231a Select which data will be restored from backup 2023-12-13 14:37:05 +02:00
db3db4637c Support manga sources in backups 2023-12-13 12:16:06 +02:00
bb2294f248 UI improvements 2023-12-13 11:33:55 +02:00
afd56c02e6 Update parsers 2023-12-12 19:13:29 +02:00
dcf1ffc976 Translated using Weblate (Russian)
Currently translated at 99.8% (543 of 544 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-12-12 19:13:17 +02:00
Nayuki
91b7028b1a Translated using Weblate (Thai)
Currently translated at 70.4% (382 of 542 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-12-12 19:13:17 +02:00
Anon
734c217c03 Translated using Weblate (Serbian)
Currently translated at 100.0% (542 of 542 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2023-12-12 19:13:17 +02:00
SENPAi-03
de18c6eb71 Translated using Weblate (Arabic)
Currently translated at 34.3% (186 of 542 strings)

Co-authored-by: SENPAi-03 <naemamro1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2023-12-12 19:13:17 +02:00
Oğuz Ersen
034be6b44e Translated using Weblate (Turkish)
Currently translated at 100.0% (544 of 544 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (542 of 542 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-12-12 19:13:17 +02:00
gallegonovato
995ff5a764 Translated using Weblate (Spanish)
Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (542 of 542 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (542 of 542 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-12-12 19:13:17 +02:00
d05d807614 Slightly improve welcome screen 2023-12-12 20:05:59 +03:00
bffd75f4d9 Fix crashes 2023-12-12 18:55:22 +02:00
bdaf3da7e0 New welcome screen 2023-12-12 15:58:45 +02:00
353d856bf5 Fix chips style 2023-12-12 14:18:49 +02:00
fca9ba98cd Update parsers 2023-12-11 20:02:09 +03:00
5df76fd881 Fix crashes 2023-12-11 18:46:35 +03:00
116 changed files with 2777 additions and 1081 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@
/.idea/compiler.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/ktlint-plugin.xml
/.idea/assetWizardSettings.xml
/.idea/kotlinScripting.xml
/.idea/kotlinc.xml

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 603
versionName = '6.5-b1'
versionCode = 610
versionName = '6.5.4'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -82,17 +82,17 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:d6b578acb0') {
implementation('com.github.KotatsuApp:kotatsu-parsers:ea095084cc') {
exclude group: 'org.json', module: 'json'
}
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.21'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.activity:activity-ktx:1.8.1'
implementation 'androidx.activity:activity-ktx:1.8.2'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-service:2.6.2'
@@ -103,7 +103,7 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.10.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.2'
implementation 'androidx.work:work-runtime:2.9.0'
@@ -120,13 +120,13 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
implementation 'com.squareup.okio:okio:3.6.0'
implementation 'com.squareup.okio:okio:3.7.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.49'
kapt 'com.google.dagger:hilt-compiler:2.49'
implementation 'com.google.dagger:hilt-android:2.50'
kapt 'com.google.dagger:hilt-compiler:2.50'
implementation 'androidx.hilt:hilt-work:1.1.0'
kapt 'androidx.hilt:hilt-compiler:1.1.0'
@@ -155,6 +155,6 @@ dependencies {
androidTestImplementation 'androidx.room:room-testing:2.6.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.49'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.49'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.50'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.50'
}

View File

@@ -61,13 +61,20 @@ class CaptchaNotifier(
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
val e = result.throwable
if (e is CloudFlareProtectedException) {
if (e is CloudFlareProtectedException && request.parameters.value<Boolean>(PARAM_IGNORE_CAPTCHA) != true) {
notify(e)
}
}
private companion object {
companion object {
fun ImageRequest.Builder.ignoreCaptchaErrors() = setParameter(
key = PARAM_IGNORE_CAPTCHA,
value = true,
memoryCacheKey = null,
)
private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha"
private const val CHANNEL_ID = "captcha"
private const val TAG = CHANNEL_ID
}

View File

@@ -3,17 +3,20 @@ package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
class BackupEntry(
val name: String,
val name: Name,
val data: JSONArray
) {
companion object Names {
enum class Name(
val key: String,
) {
const val INDEX = "index"
const val HISTORY = "history"
const val CATEGORIES = "categories"
const val FAVOURITES = "favourites"
const val SETTINGS = "settings"
const val BOOKMARKS = "bookmarks"
INDEX("index"),
HISTORY("history"),
CATEGORIES("categories"),
FAVOURITES("favourites"),
SETTINGS("settings"),
BOOKMARKS("bookmarks"),
SOURCES("sources"),
}
}

View File

@@ -7,8 +7,10 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Date
import javax.inject.Inject
private const val PAGE_SIZE = 10
@@ -20,7 +22,7 @@ class BackupRepository @Inject constructor(
suspend fun dumpHistory(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray())
while (true) {
val history = db.getHistoryDao().findAll(offset, PAGE_SIZE)
if (history.isEmpty()) {
@@ -41,7 +43,7 @@ class BackupRepository @Inject constructor(
}
suspend fun dumpCategories(): BackupEntry {
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
val entry = BackupEntry(BackupEntry.Name.CATEGORIES, JSONArray())
val categories = db.getFavouriteCategoriesDao().findAll()
for (item in categories) {
entry.data.put(JsonSerializer(item).toJson())
@@ -51,7 +53,7 @@ class BackupRepository @Inject constructor(
suspend fun dumpFavourites(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray())
while (true) {
val favourites = db.getFavouritesDao().findAll(offset, PAGE_SIZE)
if (favourites.isEmpty()) {
@@ -72,7 +74,7 @@ class BackupRepository @Inject constructor(
}
suspend fun dumpBookmarks(): BackupEntry {
val entry = BackupEntry(BackupEntry.BOOKMARKS, JSONArray())
val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray())
val all = db.getBookmarksDao().findAll()
for ((m, b) in all) {
val json = JSONObject()
@@ -90,7 +92,7 @@ class BackupRepository @Inject constructor(
}
fun dumpSettings(): BackupEntry {
val entry = BackupEntry(BackupEntry.SETTINGS, JSONArray())
val entry = BackupEntry(BackupEntry.Name.SETTINGS, JSONArray())
val settingsDump = settings.getAllValues().toMutableMap()
settingsDump.remove(AppSettings.KEY_APP_PASSWORD)
settingsDump.remove(AppSettings.KEY_PROXY_PASSWORD)
@@ -101,8 +103,18 @@ class BackupRepository @Inject constructor(
return entry
}
suspend fun dumpSources(): BackupEntry {
val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray())
val all = db.getSourcesDao().findAll()
for (source in all) {
val json = JsonSerializer(source).toJson()
entry.data.put(json)
}
return entry
}
fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
val entry = BackupEntry(BackupEntry.Name.INDEX, JSONArray())
val json = JSONObject()
json.put("app_id", BuildConfig.APPLICATION_ID)
json.put("app_version", BuildConfig.VERSION_CODE)
@@ -111,6 +123,11 @@ class BackupRepository @Inject constructor(
return entry
}
fun getBackupDate(entry: BackupEntry?): Date? {
val timestamp = entry?.data?.optJSONObject(0)?.getLongOrDefault("created_at", 0) ?: 0
return if (timestamp == 0L) null else Date(timestamp)
}
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
@@ -184,6 +201,17 @@ class BackupRepository @Inject constructor(
return result
}
suspend fun restoreSources(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val source = JsonDeserializer(item).toMangaSourceEntity()
result += runCatchingCancellable {
db.getSourcesDao().upsert(source)
}
}
return result
}
fun restoreSettings(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {

View File

@@ -1,25 +1,44 @@
package org.koitharu.kotatsu.core.backup
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import okio.Closeable
import org.json.JSONArray
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import java.io.File
import java.util.EnumSet
import java.util.zip.ZipFile
class BackupZipInput(val file: File) : Closeable {
private val zipFile = ZipFile(file)
suspend fun getEntry(name: String): BackupEntry? = runInterruptible(Dispatchers.IO) {
val entry = zipFile.getEntry(name) ?: return@runInterruptible null
suspend fun getEntry(name: BackupEntry.Name): BackupEntry? = runInterruptible(Dispatchers.IO) {
val entry = zipFile.getEntry(name.key) ?: return@runInterruptible null
val json = zipFile.getInputStream(entry).use {
JSONArray(it.bufferedReader().readText())
}
BackupEntry(name, json)
}
suspend fun entries(): Set<BackupEntry.Name> = runInterruptible(Dispatchers.IO) {
zipFile.entries().toList().mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { ze ->
BackupEntry.Name.entries.find { it.key == ze.name }
}
}
override fun close() {
zipFile.close()
}
fun cleanupAsync() {
processLifecycleScope.launch(Dispatchers.IO, CoroutineStart.ATOMIC) {
runCatching {
close()
file.delete()
}
}
}
}

View File

@@ -17,7 +17,7 @@ class BackupZipOutput(val file: File) : Closeable {
private val output = ZipOutput(file, Deflater.BEST_COMPRESSION)
suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) {
output.put(entry.name, entry.data.toString(2))
output.put(entry.name.key, entry.data.toString(2))
}
suspend fun finish() = runInterruptible(Dispatchers.IO) {

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.backup
import org.json.JSONObject
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
@@ -78,6 +79,12 @@ class JsonDeserializer(private val json: JSONObject) {
percent = json.getDouble("percent").toFloat(),
)
fun toMangaSourceEntity() = MangaSourceEntity(
source = json.getString("source"),
isEnabled = json.getBoolean("enabled"),
sortKey = json.getInt("sort_key"),
)
fun toMap(): Map<String, Any?> {
val map = mutableMapOf<String, Any?>()
val keys = json.keys()

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.backup
import org.json.JSONObject
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
@@ -82,6 +83,14 @@ class JsonSerializer private constructor(private val json: JSONObject) {
},
)
constructor(e: MangaSourceEntity) : this(
JSONObject().apply {
put("source", e.source)
put("enabled", e.isEnabled)
put("sort_key", e.sortKey)
},
)
constructor(m: Map<String, *>) : this(
JSONObject(m),
)

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.util.mapToSet
import com.google.android.material.R as materialR
@JvmName("mangaIds")
fun Collection<Manga>.ids() = mapToSet { it.id }
@@ -42,15 +43,17 @@ val MangaState.titleResId: Int
MangaState.FINISHED -> R.string.state_finished
MangaState.ABANDONED -> R.string.state_abandoned
MangaState.PAUSED -> R.string.state_paused
MangaState.UPCOMING -> R.string.state_upcoming
}
@get:DrawableRes
val MangaState.iconResId: Int
get() = when (this) {
MangaState.ONGOING -> R.drawable.ic_state_ongoing
MangaState.ONGOING -> R.drawable.ic_play
MangaState.FINISHED -> R.drawable.ic_state_finished
MangaState.ABANDONED -> R.drawable.ic_state_abandoned
MangaState.PAUSED -> R.drawable.ic_action_pause
MangaState.UPCOMING -> materialR.drawable.ic_clock_black_24dp
}
fun Manga.findChapter(id: Long): MangaChapter? {

View File

@@ -1,17 +1,23 @@
package org.koitharu.kotatsu.core.model
import android.content.Context
import android.graphics.Color
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.SuperscriptSpan
import androidx.annotation.StringRes
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.Locale
fun MangaSource.getLocaleTitle(): String? {
val lc = Locale(locale ?: return null)
return lc.getDisplayLanguage(lc).toTitleCase(lc)
}
import com.google.android.material.R as materialR
fun MangaSource(name: String): MangaSource {
MangaSource.entries.forEach {
@@ -33,6 +39,24 @@ val ContentType.titleResId
fun MangaSource.getSummary(context: Context): String {
val type = context.getString(contentType.titleResId)
val locale = getLocaleTitle() ?: context.getString(R.string.various_languages)
val locale = locale?.toLocale().getDisplayName(context)
return context.getString(R.string.source_summary_pattern, type, locale)
}
fun MangaSource.getTitle(context: Context): CharSequence = if (isNsfw()) {
buildSpannedString {
append(title)
append(' ')
appendNsfwLabel(context)
}
} else {
title
}
private fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
ForegroundColorSpan(context.getThemeColor(materialR.attr.colorError, Color.RED)),
RelativeSizeSpan(0.74f),
SuperscriptSpan(),
) {
append(context.getString(R.string.nsfw))
}

View File

@@ -295,13 +295,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true)
var readerColorFilter: ReaderColorFilter?
get() {
get() = runCatching {
val brightness = prefs.getFloat(KEY_CF_BRIGHTNESS, ReaderColorFilter.EMPTY.brightness)
val contrast = prefs.getFloat(KEY_CF_CONTRAST, ReaderColorFilter.EMPTY.contrast)
val inverted = prefs.getBoolean(KEY_CF_INVERTED, ReaderColorFilter.EMPTY.isInverted)
val grayscale = prefs.getBoolean(KEY_CF_GRAYSCALE, ReaderColorFilter.EMPTY.isGrayscale)
return ReaderColorFilter(brightness, contrast, inverted, grayscale).takeUnless { it.isEmpty }
}
ReaderColorFilter(brightness, contrast, inverted, grayscale).takeUnless { it.isEmpty }
}.getOrNull()
set(value) {
prefs.edit {
val cf = value ?: ReaderColorFilter.EMPTY

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.core.ui
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.annotation.CallSuper
@@ -8,7 +10,9 @@ import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
@@ -62,4 +66,12 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
protected fun setTitle(title: CharSequence?) {
(activity as? SettingsActivity)?.setSectionTitle(title)
}
protected fun startActivitySafe(intent: Intent) {
try {
startActivity(intent)
} catch (_: ActivityNotFoundException) {
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
}
}

View File

@@ -12,4 +12,5 @@ val SortOrder.titleRes: Int
SortOrder.RATING -> R.string.by_rating
SortOrder.NEWEST -> R.string.newest
SortOrder.ALPHABETICAL -> R.string.by_name
SortOrder.ALPHABETICAL_DESC -> R.string.by_name_reverse
}

View File

@@ -70,7 +70,7 @@ class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeLis
lastInsets = null
}
interface WindowInsetsListener {
fun interface WindowInsetsListener {
fun onWindowInsetsChanged(insets: Insets)
}

View File

@@ -9,7 +9,6 @@ import androidx.core.view.children
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup
import com.google.android.material.theme.overlay.MaterialThemeOverlay
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.castOrNull
@@ -42,9 +41,10 @@ class ChipsView @JvmOverloads constructor(
}
init {
chipStyle = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0).use {
it.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
}
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
chipStyle = ta.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
ta.recycle()
if (isInEditMode) {
setChips(
List(5) {
@@ -103,14 +103,12 @@ class ChipsView @JvmOverloads constructor(
}
private fun addChip(): Chip {
val themedContext = MaterialThemeOverlay.wrap(context, null, 0, chipStyle)
val chip = Chip(themedContext, null)
val drawable = ChipDrawable.createFromAttributes(themedContext, null, 0, chipStyle)
val chip = Chip(context)
val drawable = ChipDrawable.createFromAttributes(context, null, 0, chipStyle)
chip.setChipDrawable(drawable)
chip.isCheckedIconVisible = true
chip.isChipIconVisible = false
chip.setCheckedIconResource(R.drawable.ic_check)
// chip.checkedIconTint = chip.ic
chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false)

View File

@@ -130,7 +130,7 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float,
} else {
// Set navbar scrim 70% of navigationBarColor
ElevationOverlayProvider(context).compositeOverlayIfNeeded(
context.getThemeColor(android.R.attr.navigationBarColor, alphaFactor),
context.getThemeColor(R.attr.m3ColorBottomMenuBackground, alphaFactor),
elevation,
)
}

View File

@@ -50,7 +50,7 @@ private fun getVolumePathBeforeAndroid11(volumeId: String, context: Context): St
val length = ArrayReflect.getLength(checkNotNull(result))
(0 until length).firstNotNullOfOrNull { i ->
val storageVolumeElement = ArrayReflect.get(result, i)
val uuid = getUuid.invoke(storageVolumeElement) as String
val uuid = getUuid.invoke(storageVolumeElement) as String?
val primary = isPrimary.invoke(storageVolumeElement) as Boolean
when {
primary && volumeId == PRIMARY_VOLUME_NAME -> getPath.invoke(storageVolumeElement) as String

View File

@@ -20,12 +20,13 @@ inline fun <T> LocaleListCompat.mapToSet(block: (Locale) -> T): Set<T> {
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementException()
fun String?.getLocaleDisplayName(context: Context): String {
fun String.toLocale() = Locale(this)
fun Locale?.getDisplayName(context: Context): String {
if (this == null) {
return context.getString(R.string.various_languages)
}
val lc = Locale(this)
return lc.getDisplayLanguage(lc).toTitleCase(lc)
return getDisplayLanguage(this).toTitleCase(this)
}
private class LocaleListCompatIterator(private val list: LocaleListCompat) : ListIterator<Locale> {

View File

@@ -91,7 +91,7 @@ class MangaPrefetchService : CoroutineIntentService() {
val intent = Intent(context, MangaPrefetchService::class.java)
intent.action = ACTION_PREFETCH_DETAILS
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga))
context.startService(intent)
tryStart(context, intent)
}
fun prefetchPages(context: Context, chapter: MangaChapter) {
@@ -99,19 +99,14 @@ class MangaPrefetchService : CoroutineIntentService() {
val intent = Intent(context, MangaPrefetchService::class.java)
intent.action = ACTION_PREFETCH_PAGES
intent.putExtra(EXTRA_CHAPTER, ParcelableChapter(chapter))
try {
context.startService(intent)
} catch (e: IllegalStateException) {
// probably app is in background
e.printStackTraceDebug()
}
tryStart(context, intent)
}
fun prefetchLast(context: Context) {
if (!isPrefetchAvailable(context, null)) return
val intent = Intent(context, MangaPrefetchService::class.java)
intent.action = ACTION_PREFETCH_LAST
context.startService(intent)
tryStart(context, intent)
}
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
@@ -127,5 +122,14 @@ class MangaPrefetchService : CoroutineIntentService() {
)
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
}
private fun tryStart(context: Context, intent: Intent) {
try {
context.startService(intent)
} catch (e: IllegalStateException) {
// probably app is in background
e.printStackTraceDebug()
}
}
}
}

View File

@@ -30,6 +30,8 @@ import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksSheet
import org.koitharu.kotatsu.core.model.countChaptersByBranch
import org.koitharu.kotatsu.core.model.iconResId
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
@@ -66,7 +68,6 @@ import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
@@ -181,28 +182,13 @@ class DetailsFragment :
ratingBar.isVisible = false
}
when (manga.state) {
MangaState.FINISHED -> infoLayout.textViewState.apply {
textAndVisible = resources.getString(R.string.state_finished)
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_finished)
infoLayout.textViewState.apply {
manga.state?.let { state ->
textAndVisible = resources.getString(state.titleResId)
drawableTop = ContextCompat.getDrawable(context, state.iconResId)
} ?: run {
isVisible = false
}
MangaState.ONGOING -> infoLayout.textViewState.apply {
textAndVisible = resources.getString(R.string.state_ongoing)
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_ongoing)
}
MangaState.ABANDONED -> infoLayout.textViewState.apply {
textAndVisible = resources.getString(R.string.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
}
if (manga.source == MangaSource.LOCAL) {
infoLayout.textViewSource.isVisible = false

View File

@@ -53,6 +53,7 @@ import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
@@ -134,8 +135,14 @@ class DetailsViewModel @Inject constructor(
.map { it?.local }
.distinctUntilChanged()
.map { local ->
local?.file?.computeSize() ?: 0L
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), 0)
if (local != null) {
runCatchingCancellable {
local.file.computeSize()
}.getOrDefault(0L)
} else {
0L
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(5000), 0L)
@Deprecated("")
val description = details

View File

@@ -160,7 +160,7 @@ class ExploreFragment :
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() {
startActivity(SettingsActivity.newManageSourcesIntent(context ?: return))
startActivity(Intent(context ?: return, SourcesCatalogActivity::class.java))
}
private fun onOpenManga(manga: Manga) {

View File

@@ -21,7 +21,6 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
@@ -30,7 +29,6 @@ import org.koitharu.kotatsu.list.ui.model.EmptyHint
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.TipModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -56,8 +54,6 @@ class ExploreViewModel @Inject constructor(
valueProducer = { isSuggestionsEnabled },
)
val sortOrder = MutableStateFlow(SourcesSortOrder.MANUAL) // TODO
val onOpenManga = MutableEventFlow<Manga>()
val onActionDone = MutableEventFlow<ReversibleAction>()
val onShowSuggestionsTip = MutableEventFlow<Unit>()
@@ -129,31 +125,25 @@ class ExploreViewModel @Inject constructor(
randomLoading: Boolean,
newSources: Set<MangaSource>,
): List<ListModel> {
val result = ArrayList<ListModel>(sources.size + 4)
val result = ArrayList<ListModel>(sources.size + 3)
result += ExploreButtons(randomLoading)
if (recommendation != null) {
result += ListHeader(R.string.suggestions)
result += RecommendationsItem(recommendation)
}
if (sources.isNotEmpty()) {
result += ListHeader(R.string.remote_sources, R.string.catalog)
if (newSources.isNotEmpty()) {
result += TipModel(
key = TIP_NEW_SOURCES,
title = R.string.new_sources_text,
text = R.string.new_sources_text,
icon = R.drawable.ic_explore_normal,
primaryButtonText = R.string.manage,
secondaryButtonText = R.string.discard,
)
}
result += ListHeader(
textRes = R.string.remote_sources,
buttonTextRes = R.string.catalog,
badge = if (newSources.isNotEmpty()) "" else null,
)
sources.mapTo(result) { MangaSourceItem(it, isGrid) }
} else {
result += EmptyHint(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.no_manga_sources,
textSecondary = R.string.no_manga_sources_text,
actionStringRes = R.string.manage,
actionStringRes = R.string.catalog,
)
}
return result

View File

@@ -8,6 +8,7 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
@@ -112,7 +113,7 @@ fun exploreSourceListItemAD(
binding.root.setOnContextClickListenerCompat(eventListener)
bind {
binding.textViewTitle.text = item.source.title
binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewSubtitle.text = item.source.getSummary(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
@@ -147,7 +148,7 @@ fun exploreSourceGridItemAD(
binding.root.setOnContextClickListenerCompat(eventListener)
bind {
binding.textViewTitle.text = item.source.title
binding.textViewTitle.text = item.source.getTitle(context)
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Large, item.source.name)
binding.imageViewIcon.newImageRequest(lifecycleOwner, item.source.faviconUri())?.run {
fallback(fallbackIcon)

View File

@@ -65,10 +65,7 @@ fun categoryAD(
binding.imageViewEdit.setOnClickListener(eventListener)
binding.imageViewHandle.setOnTouchListener(eventListener)
bind { payloads ->
if (payloads.isNotEmpty()) {
return@bind
}
bind {
binding.textViewTitle.text = item.category.title
binding.textViewSubtitle.text = if (item.mangaCount == 0) {
getString(R.string.empty)

View File

@@ -15,13 +15,13 @@ fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, List
val onClickListener = View.OnClickListener { v ->
val intent = when (v.id) {
R.id.button_create -> FavouritesCategoryEditActivity.newIntent(v.context)
R.id.button_manage -> FavouriteCategoriesActivity.newIntent(v.context)
R.id.chip_create -> FavouritesCategoryEditActivity.newIntent(v.context)
R.id.chip_manage -> FavouriteCategoriesActivity.newIntent(v.context)
else -> return@OnClickListener
}
v.context.startActivity(intent)
}
binding.buttonCreate.setOnClickListener(onClickListener)
binding.buttonManage.setOnClickListener(onClickListener)
binding.chipCreate.setOnClickListener(onClickListener)
binding.chipManage.setOnClickListener(onClickListener)
}

View File

@@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.core.view.isGone
import androidx.core.view.updatePadding
import androidx.fragment.app.FragmentManager
import com.google.android.material.chip.Chip
import org.koitharu.kotatsu.R
@@ -39,8 +40,11 @@ class FilterSheetFragment :
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.scrollView.scrollIndicators = 0
if (dialog == null) {
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.scrollView.scrollIndicators = 0
}
}
val filter = requireFilter()
filter.filterSortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)

View File

@@ -14,22 +14,37 @@ import com.google.android.material.R as materialR
@CheckResult
fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? {
return if (counter > 0) {
val badgeDrawable = badge ?: initBadge(this)
badgeDrawable.number = counter
badgeDrawable.isVisible = true
badgeDrawable.align(this)
badgeDrawable
} else {
badge?.isVisible = false
badge
}
return bindBadgeImpl(badge, null, counter)
}
@CheckResult
fun View.bindBadge(badge: BadgeDrawable?, text: String?): BadgeDrawable? {
return bindBadgeImpl(badge, text, 0)
}
fun View.clearBadge(badge: BadgeDrawable?) {
BadgeUtils.detachBadgeDrawable(badge, this)
}
private fun View.bindBadgeImpl(
badge: BadgeDrawable?,
text: String?,
counter: Int,
): BadgeDrawable? = if (text != null || counter > 0) {
val badgeDrawable = badge ?: initBadge(this)
if (counter > 0) {
badgeDrawable.number = counter
} else {
badgeDrawable.text = text?.takeUnless { it.isEmpty() }
}
badgeDrawable.isVisible = true
badgeDrawable.align(this)
badgeDrawable
} else {
badge?.isVisible = false
badge
}
private fun initBadge(anchor: View): BadgeDrawable {
val badge = BadgeDrawable.create(anchor.context)
val resources = anchor.resources

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
import org.koitharu.kotatsu.list.ui.model.ListHeader
@@ -12,6 +13,8 @@ fun listHeaderAD(
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderButtonBinding>(
{ inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) },
) {
var badge: BadgeDrawable? = null
if (listener != null) {
binding.buttonMore.setOnClickListener {
listener.onListHeaderClick(item, it)
@@ -23,9 +26,11 @@ fun listHeaderAD(
if (item.buttonTextRes == 0) {
binding.buttonMore.isInvisible = true
binding.buttonMore.text = null
binding.buttonMore.clearBadge(badge)
} else {
binding.buttonMore.setText(item.buttonTextRes)
binding.buttonMore.isVisible = true
badge = itemView.bindBadge(badge, item.badge)
}
}
}

View File

@@ -6,38 +6,41 @@ import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
@Suppress("DataClassPrivateConstructor")
data class ListHeader private constructor(
private val text: CharSequence?,
@StringRes private val textRes: Int,
private val dateTimeAgo: DateTimeAgo?,
private val textRaw: Any,
@StringRes val buttonTextRes: Int,
val payload: Any?,
val badge: String?,
) : ListModel {
constructor(
text: CharSequence,
@StringRes buttonTextRes: Int = 0,
payload: Any? = null,
) : this(text, 0, null, buttonTextRes, payload)
badge: String? = null,
) : this(textRaw = text, buttonTextRes, payload, badge)
constructor(
@StringRes textRes: Int,
@StringRes buttonTextRes: Int = 0,
payload: Any? = null,
) : this(null, textRes, null, buttonTextRes, payload)
badge: String? = null,
) : this(textRaw = textRes, buttonTextRes, payload, badge)
constructor(
dateTimeAgo: DateTimeAgo,
@StringRes buttonTextRes: Int = 0,
payload: Any? = null,
) : this(null, 0, dateTimeAgo, buttonTextRes, payload)
badge: String? = null,
) : this(textRaw = dateTimeAgo, buttonTextRes, payload, badge)
fun getText(context: Context): CharSequence? = when {
text != null -> text
textRes != 0 -> context.getString(textRes)
else -> dateTimeAgo?.format(context.resources)
fun getText(context: Context): CharSequence? = when (textRaw) {
is CharSequence -> textRaw
is Int -> if (textRaw != 0) context.getString(textRaw) else null
is DateTimeAgo -> textRaw.format(context.resources)
else -> null
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is ListHeader && text == other.text && dateTimeAgo == other.dateTimeAgo && textRes == other.textRes
return other is ListHeader && textRaw == other.textRaw
}
}

View File

@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import javax.inject.Inject
@@ -56,8 +57,10 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
binding.textViewAuthor.setOnClickListener(this)
binding.imageViewCover.setOnClickListener(this)
binding.buttonOpen.setOnClickListener(this)
binding.buttonRead.setOnClickListener(this)
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.footer.observe(viewLifecycleOwner, ::onFooterUpdated)
viewModel.tagsChips.observe(viewLifecycleOwner, ::onTagsChipsChanged)
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
}
@@ -70,6 +73,14 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
DetailsActivity.newIntent(v.context, manga),
)
R.id.button_read -> {
startActivity(
ReaderActivity.IntentBuilder(v.context)
.manga(manga)
.build(),
)
}
R.id.textView_author -> startActivity(
SearchActivity.newIntent(
context = v.context,
@@ -118,6 +129,43 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
}
}
private fun onFooterUpdated(footer: PreviewViewModel.FooterInfo?) {
with(requireViewBinding()) {
toolbarBottom.isVisible = footer != null
if (footer == null) {
return
}
toolbarBottom.title = when {
footer.isInProgress() -> {
getString(R.string.chapter_d_of_d, footer.currentChapter, footer.totalChapters)
}
footer.totalChapters > 0 -> {
resources.getQuantityString(R.plurals.chapters, footer.totalChapters, footer.totalChapters)
}
else -> {
getString(R.string.no_chapters)
}
}
buttonRead.isEnabled = footer.totalChapters > 0
buttonRead.setIconResource(
when {
footer.isIncognito -> R.drawable.ic_incognito
footer.isInProgress() -> R.drawable.ic_play
else -> R.drawable.ic_read
},
)
buttonRead.setText(
if (footer.isInProgress()) {
R.string._continue
} else {
R.string.read
},
)
}
}
private fun onDescriptionChanged(description: CharSequence?) {
val tv = viewBinding?.textViewDescription ?: return
when {

View File

@@ -13,11 +13,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -25,6 +28,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.sanitize
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import javax.inject.Inject
@@ -33,6 +37,7 @@ class PreviewViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val extraProvider: ListExtraProvider,
private val repositoryFactory: MangaRepository.Factory,
private val historyRepository: HistoryRepository,
private val imageGetter: Html.ImageGetter,
) : BaseViewModel() {
@@ -40,6 +45,26 @@ class PreviewViewModel @Inject constructor(
savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga,
)
val footer = combine(
manga,
historyRepository.observeOne(manga.value.id),
manga.flatMapLatest { historyRepository.observeShouldSkip(it) }.distinctUntilChanged(),
) { m, history, incognito ->
if (m.chapters == null) {
return@combine null
}
val b = m.getPreferredBranch(history)
val chapters = m.getChapters(b).orEmpty()
FooterInfo(
branch = b,
currentChapter = history?.chapterId?.let {
chapters.indexOfFirst { x -> x.id == it }
} ?: -1,
totalChapters = chapters.size,
isIncognito = incognito,
)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, null)
val description = manga
.distinctUntilChangedBy { it.description.orEmpty() }
.transformLatest {
@@ -82,4 +107,14 @@ class PreviewViewModel @Inject constructor(
}
return spannable.trim()
}
data class FooterInfo(
val branch: String?,
val currentChapter: Int,
val totalChapters: Int,
val isIncognito: Boolean,
) {
fun isInProgress() = currentChapter >= 0
}
}

View File

@@ -133,7 +133,7 @@ class LocalMangaRepository @Inject constructor(
}.getOrNull()
}
suspend fun findSavedManga(remoteManga: Manga): LocalManga? {
suspend fun findSavedManga(remoteManga: Manga): LocalManga? = runCatchingCancellable {
// fast path
LocalMangaInput.find(storageManager.getReadableDirs(), remoteManga)?.let {
return it.getManga()
@@ -155,7 +155,9 @@ class LocalMangaRepository @Inject constructor(
}
}
}.firstOrNull()?.getManga()
}
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
override suspend fun getPageUrl(page: MangaPage) = page.url

View File

@@ -20,6 +20,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toCamelCase
import java.io.File
import java.util.TreeMap
import java.util.zip.ZipFile
/**
@@ -49,8 +50,15 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
url = mangaUri,
coverUrl = cover,
largeCoverUrl = cover,
chapters = info.chapters?.mapIndexed { i, c ->
c.copy(url = chapterFiles[i].toUri().toString(), source = MangaSource.LOCAL)
chapters = info.chapters?.mapIndexedNotNull { i, c ->
val fileName = index.getChapterFileName(c.id)
val file = if (fileName != null) {
chapterFiles[fileName]
} else {
// old downloads
chapterFiles.values.elementAtOrNull(i)
} ?: return@mapIndexedNotNull null
c.copy(url = file.toUri().toString(), source = MangaSource.LOCAL)
},
) ?: Manga(
id = root.absolutePath.longHashCode(),
@@ -59,7 +67,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
publicUrl = mangaUri,
source = MangaSource.LOCAL,
coverUrl = findFirstImageEntry().orEmpty(),
chapters = chapterFiles.mapIndexed { i, f ->
chapters = chapterFiles.values.mapIndexed { i, f ->
MangaChapter(
id = "$i${f.name}".longHashCode(),
name = f.nameWithoutExtension.toHumanReadable(),
@@ -120,9 +128,9 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
private fun String.toHumanReadable() = replace("_", " ").toCamelCase()
private fun getChaptersFiles(): List<File> = root.walkCompat()
private fun getChaptersFiles() = root.walkCompat()
.filter { it.hasCbzExtension() }
.toListSorted(compareBy(AlphanumComparator()) { it.name })
.associateByTo(TreeMap(AlphanumComparator())) { it.name }
private fun findFirstImageEntry(): String? {
return root.walkCompat().firstOrNull { hasImageExtension(it) }?.toUri()?.toString()

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import java.io.File
@@ -54,7 +55,8 @@ sealed class LocalMangaInput(
zip.isFile -> LocalMangaZipInput(zip)
else -> null
}
if (input?.getMangaInfo()?.id == manga.id) {
val info = runCatchingCancellable { input?.getMangaInfo() }.getOrNull()
if (info?.id == manga.id) {
send(input)
}
}

View File

@@ -5,9 +5,11 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okio.Closeable
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
import java.io.File
@@ -86,7 +88,11 @@ sealed class LocalMangaOutput(
}
private suspend fun canWriteTo(file: File, manga: Manga): Boolean {
val info = LocalMangaInput.of(file).getMangaInfo() ?: return false
val info = runCatchingCancellable {
LocalMangaInput.of(file).getMangaInfo()
}.onFailure {
it.printStackTraceDebug()
}.getOrNull() ?: return false
return info.id == manga.id
}
}

View File

@@ -13,8 +13,6 @@ import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
@@ -47,7 +45,6 @@ import org.koitharu.kotatsu.core.util.ext.hideKeyboard
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.databinding.ActivityMainBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.DetailsActivity
@@ -55,6 +52,7 @@ import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -66,7 +64,6 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.about.AppUpdateDialog
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -98,17 +95,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater))
if (bottomNav != null) {
ViewCompat.setOnApplyWindowInsetsListener(viewBinding.root) { _, insets ->
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
val elevation = bottomNav?.elevation ?: 0f
window.setNavigationBarTransparentCompat(this@MainActivity, elevation)
}
insets
}
ViewCompat.requestApplyInsets(viewBinding.root)
}
with(viewBinding.searchView) {
onFocusChangeListener = this@MainActivity
searchSuggestionListener = this@MainActivity
@@ -139,10 +125,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.container, null))
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
viewModel.isResumeEnabled.observe(this, this::onResumeEnabledChanged)
viewModel.counters.observe(this, ::onCountersChanged)
viewModel.feedCounter.observe(this, ::onFeedCounterChanged)
viewModel.appUpdate.observe(this, MenuInvalidator(this))
viewModel.onFirstStart.observeEvent(this) {
OnboardDialogFragment.show(supportFragmentManager)
WelcomeSheet.show(supportFragmentManager)
}
viewModel.isIncognitoMode.observe(this) {
adjustSearchUI(isSearchOpened(), false)
@@ -292,10 +278,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
startActivity(IntentBuilder(this).manga(manga).build(), options)
}
private fun onCountersChanged(counters: Map<NavItem, Int>) {
counters.forEach { (navItem, counter) ->
navigationDelegate.setCounter(navItem, counter)
}
private fun onFeedCounterChanged(counter: Int) {
navigationDelegate.setCounter(NavItem.FEED, counter)
}
private fun onIncognitoModeChanged(isIncognito: Boolean) {

View File

@@ -4,15 +4,11 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.NavItem
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
@@ -22,7 +18,6 @@ import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.main.domain.ReadingResumeEnabledUseCase
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import java.util.EnumMap
import javax.inject.Inject
@HiltViewModel
@@ -52,19 +47,8 @@ class MainViewModel @Inject constructor(
val appUpdate = appUpdateRepository.observeAvailableUpdate()
val counters = combine(
trackingRepository.observeUpdatedMangaCount(),
observeNewSourcesCount(),
) { tracks, newSources ->
val em = EnumMap<NavItem, Int>(NavItem::class.java)
em[NavItem.EXPLORE] = newSources
em[NavItem.FEED] = tracks
em
}.stateIn(
scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyMap<NavItem, Int>(),
)
val feedCounter = trackingRepository.observeUpdatedMangaCount()
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, 0)
init {
launchJob {
@@ -87,8 +71,4 @@ class MainViewModel @Inject constructor(
fun setIncognitoMode(isEnabled: Boolean) {
settings.isIncognitoModeEnabled = isEnabled
}
private fun observeNewSourcesCount() = sourcesRepository.observeNewSources()
.map { if (sourcesRepository.isSetupRequired()) 0 else it.size }
.distinctUntilChanged()
}

View File

@@ -0,0 +1,133 @@
package org.koitharu.kotatsu.main.ui.welcome
import android.accounts.AccountManager
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isGone
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.SheetWelcomeBinding
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
import java.util.Locale
@AndroidEntryPoint
class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipClickListener, View.OnClickListener,
ActivityResultCallback<Uri?> {
private val viewModel by viewModels<WelcomeViewModel>()
private val backupSelectCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(),
this,
)
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetWelcomeBinding {
return SheetWelcomeBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: SheetWelcomeBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
binding.textViewWelcomeTitle.isGone = resources.getBoolean(R.bool.is_tablet)
binding.chipsLocales.onChipClickListener = this
binding.chipsType.onChipClickListener = this
binding.chipBackup.setOnClickListener(this)
binding.chipSync.setOnClickListener(this)
viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged)
viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged)
}
override fun onChipClick(chip: Chip, data: Any?) {
when (data) {
is ContentType -> viewModel.setTypeChecked(data, chip.isChecked)
is Locale? -> viewModel.setLocaleChecked(data, chip.isChecked)
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.chip_backup -> {
if (!backupSelectCall.tryLaunch(arrayOf("*/*"))) {
Snackbar.make(
v, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
).show()
}
}
R.id.chip_sync -> {
val am = AccountManager.get(v.context)
val accountType = getString(R.string.account_type_sync)
am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
}
}
}
override fun onActivityResult(result: Uri?) {
if (result != null) {
RestoreDialogFragment.show(parentFragmentManager, result)
}
}
private fun onLocalesChanged(value: FilterProperty<Locale?>) {
val chips = viewBinding?.chipsLocales ?: return
chips.setChips(
value.availableItems.map {
ChipsView.ChipModel(
tint = 0,
title = it?.getDisplayLanguage(it)?.toTitleCase(it) ?: getString(R.string.various_languages),
icon = 0,
isCheckable = true,
isChecked = it in value.selectedItems,
data = it,
)
},
)
}
private fun onTypesChanged(value: FilterProperty<ContentType>) {
val chips = viewBinding?.chipsType ?: return
chips.setChips(
value.availableItems.map {
ChipsView.ChipModel(
tint = 0,
title = getString(it.titleResId),
icon = 0,
isCheckable = true,
isChecked = it in value.selectedItems,
data = it,
)
},
)
}
companion object {
private const val TAG = "WelcomeSheet"
fun show(fm: FragmentManager) = WelcomeSheet().showDistinct(fm, TAG)
fun dismiss(fm: FragmentManager): Boolean {
val sheet = fm.findFragmentByTag(TAG) as? WelcomeSheet ?: return false
sheet.dismissAllowingStateLoss()
return true
}
}
}

View File

@@ -0,0 +1,107 @@
package org.koitharu.kotatsu.main.ui.welcome
import android.content.Context
import androidx.core.os.ConfigurationCompat
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class WelcomeViewModel @Inject constructor(
private val repository: MangaSourcesRepository,
@ApplicationContext context: Context,
) : BaseViewModel() {
private val allSources = repository.allMangaSources
private val localesGroups by lazy { allSources.groupBy { it.locale?.let { x -> Locale(x) } } }
private var updateJob: Job
val locales = MutableStateFlow(
FilterProperty<Locale?>(
availableItems = listOf(null),
selectedItems = setOf(null),
isLoading = true,
error = null,
),
)
val types = MutableStateFlow(
FilterProperty(
availableItems = ContentType.entries.toList(),
selectedItems = setOf(ContentType.MANGA),
isLoading = false,
error = null,
),
)
init {
updateJob = launchJob(Dispatchers.Default) {
val languages = localesGroups.keys.associateBy { x -> x?.language }
val selectedLocales = HashSet<Locale?>(2)
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList()
.firstNotNullOfOrNull { lc -> languages[lc.language] }
selectedLocales += null
locales.value = locales.value.copy(
availableItems = localesGroups.keys.sortedWithSafe(nullsFirst(LocaleComparator())),
selectedItems = selectedLocales,
isLoading = false,
)
}
}
fun setLocaleChecked(locale: Locale?, isChecked: Boolean) {
val snapshot = locales.value
locales.value = snapshot.copy(
selectedItems = if (isChecked) {
snapshot.selectedItems + locale
} else {
snapshot.selectedItems - locale
},
)
val prevJob = updateJob
updateJob = launchJob(Dispatchers.Default) {
prevJob.join()
commit()
}
}
fun setTypeChecked(type: ContentType, isChecked: Boolean) {
val snapshot = types.value
types.value = snapshot.copy(
selectedItems = if (isChecked) {
snapshot.selectedItems + type
} else {
snapshot.selectedItems - type
},
)
val prevJob = updateJob
updateJob = launchJob(Dispatchers.Default) {
prevJob.join()
commit()
}
}
private suspend fun commit() {
val languages = locales.value.selectedItems.mapToSet { it?.language }
val types = types.value.selectedItems
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
x.contentType in types && x.locale in languages
}
repository.setSourcesEnabledExclusive(enabledSources)
}
}

View File

@@ -119,9 +119,9 @@ class ColorFilterConfigActivity :
.setMessage(R.string.color_correction_apply_text)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.this_manga) { _, _ ->
viewModel.saveGlobally()
}.setNeutralButton(R.string.globally) { _, _ ->
viewModel.save()
}.setNeutralButton(R.string.globally) { _, _ ->
viewModel.saveGlobally()
}.show()
}

View File

@@ -12,9 +12,11 @@ class WebtoonFrameLayout @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) {
val target: WebtoonImageView by lazy(LazyThreadSafetyMode.NONE) {
findViewById(R.id.ssiv)
}
private var _target: WebtoonImageView? = null
val target: WebtoonImageView
get() = _target ?: findViewById<WebtoonImageView?>(R.id.ssiv).also {
_target = it
}
fun dispatchVerticalScroll(dy: Int): Int {
if (dy == 0) {

View File

@@ -1,14 +1,16 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PointF
import android.util.AttributeSet
import androidx.core.view.ancestors
import androidx.recyclerview.widget.RecyclerView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.parsers.util.toIntUp
private const val SCROLL_UNKNOWN = -1
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.util.ext.resolveDp
import kotlin.math.roundToInt
class WebtoonImageView @JvmOverloads constructor(
context: Context,
@@ -18,7 +20,14 @@ class WebtoonImageView @JvmOverloads constructor(
private val ct = PointF()
private var scrollPos = 0
private var scrollRange = SCROLL_UNKNOWN
private var debugPaint: Paint? = null
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (BuildConfig.DEBUG) {
drawDebug(canvas)
}
}
fun scrollBy(delta: Int) {
val maxScroll = getScrollRange()
@@ -41,14 +50,14 @@ class WebtoonImageView @JvmOverloads constructor(
fun getScroll() = scrollPos
fun getScrollRange(): Int {
if (scrollRange == SCROLL_UNKNOWN) {
computeScrollRange()
if (!isReady) {
return 0
}
return scrollRange.coerceAtLeast(0)
val totalHeight = (sHeight * width / sWidth.toFloat()).roundToInt()
return (totalHeight - height).coerceAtLeast(0)
}
override fun recycle() {
scrollRange = SCROLL_UNKNOWN
scrollPos = 0
super.recycle()
}
@@ -91,8 +100,6 @@ class WebtoonImageView @JvmOverloads constructor(
override fun onDownsamplingChanged() {
super.onDownsamplingChanged()
adjustScale()
computeScrollRange()
ancestors.firstNotNullOfOrNull { it as? WebtoonRecyclerView }?.updateChildrenScroll()
}
override fun onReady() {
@@ -102,13 +109,10 @@ class WebtoonImageView @JvmOverloads constructor(
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (oldh == h || oldw == 0 || oldh == 0 || scrollRange == SCROLL_UNKNOWN) return
computeScrollRange()
val container = ancestors.firstNotNullOfOrNull { it as? WebtoonFrameLayout } ?: return
val parentHeight = parentHeight()
if (scrollPos != 0 && container.bottom < parentHeight) {
scrollTo(scrollRange)
if (oldh != h && oldw != 0 && oldh != 0 && isReady) {
ancestors.firstNotNullOfOrNull { it as? WebtoonRecyclerView }?.updateChildrenScroll()
} else {
return
}
}
@@ -120,14 +124,6 @@ class WebtoonImageView @JvmOverloads constructor(
setScaleAndCenter(minScale, ct)
}
private fun computeScrollRange() {
if (!isReady) {
return
}
val totalHeight = (sHeight * minScale).toIntUp()
scrollRange = (totalHeight - height).coerceAtLeast(0)
}
private fun adjustScale() {
minScale = width / sWidth.toFloat()
maxScale = minScale
@@ -137,4 +133,18 @@ class WebtoonImageView @JvmOverloads constructor(
private fun parentHeight(): Int {
return ancestors.firstNotNullOfOrNull { it as? RecyclerView }?.height ?: 0
}
private fun drawDebug(canvas: Canvas) {
val paint = debugPaint ?: Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = android.graphics.Color.RED
strokeWidth = context.resources.resolveDp(2f)
textAlign = android.graphics.Paint.Align.LEFT
textSize = context.resources.resolveDp(14f)
debugPaint = this
}
paint.style = Paint.Style.STROKE
canvas.drawRect(1f, 1f, width.toFloat() - 1f, height.toFloat() - 1f, paint)
paint.style = Paint.Style.FILL
canvas.drawText("${getScroll()} / ${getScrollRange()}", 100f, 100f, paint)
}
}

View File

@@ -3,11 +3,10 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Toast
import androidx.core.view.ViewCompat.TYPE_TOUCH
import androidx.core.view.forEach
import androidx.core.view.iterator
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
import java.util.LinkedList
import java.util.WeakHashMap
@@ -18,6 +17,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null
private val detachedViews = WeakHashMap<View, Unit>()
private var isFixingScroll: Boolean = false
override fun onChildDetachedFromWindow(child: View) {
super.onChildDetachedFromWindow(child)
@@ -56,6 +56,13 @@ class WebtoonRecyclerView @JvmOverloads constructor(
return consumedY != 0 || dy == 0
}
override fun onScrollStateChanged(state: Int) {
super.onScrollStateChanged(state)
if (state == SCROLL_STATE_IDLE) {
updateChildrenScroll()
}
}
private fun consumeVerticalScroll(dy: Int): Int {
if (childCount == 0) {
return 0
@@ -106,16 +113,12 @@ class WebtoonRecyclerView @JvmOverloads constructor(
}
private fun notifyScrollChanged(dy: Int) {
updateChildrenScroll()
val listeners = onPageScrollListeners
if (listeners.isNullOrEmpty()) {
return
}
val centerPosition = findCenterViewPosition()
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
if (BuildConfig.DEBUG) {
validateLayout()
}
}
fun relayoutChildren() {
@@ -128,29 +131,35 @@ class WebtoonRecyclerView @JvmOverloads constructor(
}
fun updateChildrenScroll() {
forEach { child ->
if (isFixingScroll) {
return
}
isFixingScroll = true
for (child in this) {
val ssiv = (child as WebtoonFrameLayout).target
when {
child.top < 0 -> ssiv.scrollTo(ssiv.getScrollRange())
child.top > 0 -> ssiv.scrollTo(0)
else -> ssiv.scrollBy(0)
if (adjustScroll(child, ssiv)) {
break
}
}
isFixingScroll = false
}
private fun validateLayout() {
forEach { child ->
val ssiv = (child as WebtoonFrameLayout).target
val scroll = ssiv.getScroll()
val assertion = when {
child.top < 0 -> scroll == ssiv.getScrollRange()
child.top > 0 -> scroll == 0
else -> true
}
if (!assertion) {
Toast.makeText(context, "Scroll = $scroll for view with top: ${child.top}", Toast.LENGTH_SHORT).show()
}
private fun adjustScroll(child: View, ssiv: WebtoonImageView): Boolean = when {
child.bottom < height && ssiv.getScroll() < ssiv.getScrollRange() -> {
val distance = minOf(height - child.bottom, ssiv.getScrollRange() - ssiv.getScroll())
scrollBy(0, -distance)
ssiv.scrollBy(distance)
true
}
child.top > 0 && ssiv.getScroll() > 0 -> {
val distance = minOf(child.top, ssiv.getScroll())
scrollBy(0, distance)
ssiv.scrollBy(-distance)
true
}
else -> false
}
abstract class OnPageScrollListener {

View File

@@ -1,11 +1,11 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import androidx.core.text.buildSpannedString
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.enqueueWith
@@ -14,7 +14,6 @@ import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionSourceBinding
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
import org.koitharu.kotatsu.settings.sources.adapter.appendNsfwLabel
fun searchSuggestionSourceAD(
coil: ImageLoader,
@@ -32,15 +31,7 @@ fun searchSuggestionSourceAD(
}
bind {
binding.textViewTitle.text = if (item.isNsfw) {
buildSpannedString {
append(item.source.title)
append(' ')
appendNsfwLabel(context)
}
} else {
item.source.title
}
binding.textViewTitle.text = item.source.getTitle(context)
binding.textViewSubtitle.text = item.source.getSummary(context)
binding.switchLocal.isChecked = item.isEnabled
val fallbackIcon = FaviconDrawable(context, R.style.FaviconDrawable_Small, item.source.name)

View File

@@ -1,23 +1,23 @@
package org.koitharu.kotatsu.search.ui.suggestion.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import org.koitharu.kotatsu.R
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.databinding.ItemSearchSuggestionTagsBinding
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.model.SearchSuggestionItem
fun searchSuggestionTagsAD(
listener: SearchSuggestionListener,
) = adapterDelegate<SearchSuggestionItem.Tags, SearchSuggestionItem>(R.layout.item_search_suggestion_tags) {
) = adapterDelegateViewBinding<SearchSuggestionItem.Tags, SearchSuggestionItem, ItemSearchSuggestionTagsBinding>(
{ layoutInflater, parent -> ItemSearchSuggestionTagsBinding.inflate(layoutInflater, parent, false) },
) {
val chipGroup = itemView as ChipsView
chipGroup.onChipClickListener = ChipsView.OnChipClickListener { _, data ->
binding.chipsGenres.onChipClickListener = ChipsView.OnChipClickListener { _, data ->
listener.onTagClick(data as? MangaTag ?: return@OnChipClickListener)
}
bind {
chipGroup.setChips(item.tags)
binding.chipsGenres.setChips(item.tags)
}
}

View File

@@ -110,11 +110,7 @@ class ServicesSettingsFragment : BasePreferenceFragment(R.string.services),
if (account == null) {
am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
} else {
try {
startActivity(SyncSettingsIntent(account))
} catch (_: ActivityNotFoundException) {
Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
startActivitySafe(SyncSettingsIntent(account))
}
true
}

View File

@@ -83,7 +83,7 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
private fun openLink(url: String, title: CharSequence?) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = url.toUri()
startActivity(
startActivitySafe(
if (title != null) {
Intent.createChooser(intent, title)
} else {

View File

@@ -54,7 +54,11 @@ class AppBackupAgent : BackupAgent() {
mtime: Long
) {
if (destination?.name?.endsWith(".bk.zip") == true) {
restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)))
restoreBackupFile(
data.fileDescriptor,
size,
BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)),
)
destination.delete()
} else {
super.onRestoreFile(data, size, destination, type, mode, mtime)
@@ -69,6 +73,7 @@ class AppBackupAgent : BackupAgent() {
backup.put(repository.dumpCategories())
backup.put(repository.dumpFavourites())
backup.put(repository.dumpBookmarks())
backup.put(repository.dumpSources())
backup.put(repository.dumpSettings())
backup.finish()
backup.file
@@ -86,11 +91,12 @@ class AppBackupAgent : BackupAgent() {
val backup = BackupZipInput(tempFile)
try {
runBlocking {
backup.getEntry(BackupEntry.HISTORY)?.let { repository.restoreHistory(it) }
backup.getEntry(BackupEntry.CATEGORIES)?.let { repository.restoreCategories(it) }
backup.getEntry(BackupEntry.FAVOURITES)?.let { repository.restoreFavourites(it) }
backup.getEntry(BackupEntry.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
backup.getEntry(BackupEntry.SETTINGS)?.let { repository.restoreSettings(it) }
backup.getEntry(BackupEntry.Name.HISTORY)?.let { repository.restoreHistory(it) }
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let { repository.restoreCategories(it) }
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { repository.restoreFavourites(it) }
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
backup.getEntry(BackupEntry.Name.SOURCES)?.let { repository.restoreSources(it) }
backup.getEntry(BackupEntry.Name.SETTINGS)?.let { repository.restoreSettings(it) }
}
} finally {
backup.close()

View File

@@ -0,0 +1,37 @@
package org.koitharu.kotatsu.settings.backup
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_CHECKED_CHANGED
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
class BackupEntriesAdapter(
clickListener: OnListItemClickListener<BackupEntryModel>,
) : BaseListAdapter<BackupEntryModel>() {
init {
addDelegate(ListItemType.NAV_ITEM, backupEntryAD(clickListener))
}
}
private fun backupEntryAD(
clickListener: OnListItemClickListener<BackupEntryModel>,
) = adapterDelegateViewBinding<BackupEntryModel, BackupEntryModel, ItemCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v ->
clickListener.onItemClick(item, v)
}
bind { payloads ->
with(binding.root) {
setText(item.titleResId)
setChecked(item.isChecked, PAYLOAD_CHECKED_CHANGED in payloads)
isEnabled = item.isEnabled
}
}
}

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.settings.backup
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.BackupEntry
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
data class BackupEntryModel(
val name: BackupEntry.Name,
val isChecked: Boolean,
val isEnabled: Boolean,
) : ListModel {
@get:StringRes
val titleResId: Int
get() = when (name) {
BackupEntry.Name.INDEX -> 0 // should not appear here
BackupEntry.Name.HISTORY -> R.string.history
BackupEntry.Name.CATEGORIES -> R.string.favourites_categories
BackupEntry.Name.FAVOURITES -> R.string.favourites
BackupEntry.Name.SETTINGS -> R.string.settings
BackupEntry.Name.BOOKMARKS -> R.string.bookmarks
BackupEntry.Name.SOURCES -> R.string.remote_sources
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is BackupEntryModel && other.name == name
}
override fun getChangePayload(previousState: ListModel): Any? {
if (previousState !is BackupEntryModel) {
return null
}
return if (previousState.isEnabled != isEnabled) {
ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
} else if (previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}

View File

@@ -24,21 +24,25 @@ class BackupViewModel @Inject constructor(
init {
launchLoadingJob {
val file = BackupZipOutput(context).use { backup ->
val step = 1f / 6f
backup.put(repository.createIndex())
progress.value = 0f
backup.put(repository.dumpHistory())
progress.value = 0.2f
progress.value += step
backup.put(repository.dumpCategories())
progress.value = 0.4f
progress.value += step
backup.put(repository.dumpFavourites())
progress.value = 0.6f
progress.value += step
backup.put(repository.dumpBookmarks())
progress.value = 0.8f
progress.value += step
backup.put(repository.dumpSources())
progress.value += step
backup.put(repository.dumpSettings())
backup.finish()

View File

@@ -42,6 +42,7 @@ class PeriodicalBackupWorker @AssistedInject constructor(
backup.put(repository.dumpCategories())
backup.put(repository.dumpFavourites())
backup.put(repository.dumpBookmarks())
backup.put(repository.dumpSources())
backup.put(repository.dumpSettings())
backup.finish()
backup.file

View File

@@ -3,40 +3,58 @@ package org.koitharu.kotatsu.settings.backup
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.CompositeResult
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.DialogProgressBinding
import org.koitharu.kotatsu.databinding.DialogRestoreBinding
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.math.roundToInt
@AndroidEntryPoint
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnListItemClickListener<BackupEntryModel>,
View.OnClickListener {
private val viewModel: RestoreViewModel by viewModels()
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
) = DialogProgressBinding.inflate(inflater, container, false)
) = DialogRestoreBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: DialogProgressBinding, savedInstanceState: Bundle?) {
override fun onViewBindingCreated(binding: DialogRestoreBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
binding.textViewTitle.setText(R.string.restore_backup)
binding.textViewSubtitle.setText(R.string.preparing_)
val adapter = BackupEntriesAdapter(this)
binding.recyclerView.adapter = adapter
binding.buttonCancel.setOnClickListener(this)
binding.buttonRestore.setOnClickListener(this)
viewModel.availableEntries.observe(viewLifecycleOwner, adapter)
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
viewModel.onRestoreDone.observeEvent(viewLifecycleOwner, this::onRestoreDone)
viewModel.onError.observeEvent(viewLifecycleOwner, this::onError)
combine(
viewModel.isLoading,
viewModel.availableEntries,
viewModel.backupDate,
::Triple,
).observe(viewLifecycleOwner, this::onLoadingChanged)
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
@@ -44,6 +62,40 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
.setCancelable(false)
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_cancel -> dismiss()
R.id.button_restore -> viewModel.restore()
}
}
override fun onItemClick(item: BackupEntryModel, view: View) {
viewModel.onItemClick(item)
}
private fun onLoadingChanged(value: Triple<Boolean, List<BackupEntryModel>, Date?>) {
val (isLoading, entries, backupDate) = value
val hasEntries = entries.isNotEmpty()
with(requireViewBinding()) {
progressBar.isVisible = isLoading
recyclerView.isGone = isLoading
textViewSubtitle.textAndVisible =
when {
!isLoading -> backupDate?.formatBackupDate()
hasEntries -> getString(R.string.processing_)
else -> getString(R.string.loading_)
}
buttonRestore.isEnabled = !isLoading && entries.any { it.isChecked }
}
}
private fun Date.formatBackupDate(): String {
return getString(
R.string.backup_date_,
SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(this),
)
}
private fun onError(e: Throwable) {
MaterialAlertDialogBuilder(context ?: return)
.setNegativeButton(R.string.close, null)
@@ -89,6 +141,9 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
}
builder.setPositiveButton(android.R.string.ok, null)
.show()
if (!result.isEmpty && !result.isAllFailed) {
WelcomeSheet.dismiss(parentFragmentManager)
}
dismiss()
}

View File

@@ -15,8 +15,12 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import java.io.File
import java.io.FileNotFoundException
import java.util.Date
import java.util.EnumMap
import java.util.EnumSet
import javax.inject.Inject
@HiltViewModel
@@ -26,57 +30,127 @@ class RestoreViewModel @Inject constructor(
@ApplicationContext context: Context,
) : BaseViewModel() {
private val backupInput = SuspendLazy {
val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE)
?.toUriOrNull() ?: throw FileNotFoundException()
val contentResolver = context.contentResolver
runInterruptible(Dispatchers.IO) {
val tempFile = File.createTempFile("backup_", ".tmp")
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}
BackupZipInput(tempFile)
}
}
val progress = MutableStateFlow(-1f)
val onRestoreDone = MutableEventFlow<CompositeResult>()
val availableEntries = MutableStateFlow<List<BackupEntryModel>>(emptyList())
val backupDate = MutableStateFlow<Date?>(null)
init {
launchLoadingJob {
val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE)
?.toUriOrNull() ?: throw FileNotFoundException()
val contentResolver = context.contentResolver
val backup = runInterruptible(Dispatchers.IO) {
val tempFile = File.createTempFile("backup_", ".tmp")
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
launchLoadingJob(Dispatchers.Default) {
val backup = backupInput.get()
val entries = backup.entries()
availableEntries.value = BackupEntry.Name.entries.mapNotNull { entry ->
if (entry == BackupEntry.Name.INDEX || entry !in entries) {
return@mapNotNull null
}
BackupZipInput(tempFile)
BackupEntryModel(
name = entry,
isChecked = true,
isEnabled = true,
)
}
try {
val result = CompositeResult()
backupDate.value = repository.getBackupDate(backup.getEntry(BackupEntry.Name.INDEX))
}
}
progress.value = 0f
backup.getEntry(BackupEntry.HISTORY)?.let {
override fun onCleared() {
super.onCleared()
backupInput.peek()?.cleanupAsync()
}
fun onItemClick(item: BackupEntryModel) {
val map = availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name }
map[item.name] = item.copy(isChecked = !item.isChecked)
map.validate()
availableEntries.value = map.values.sortedBy { it.name.ordinal }
}
fun restore() {
launchLoadingJob {
val backup = backupInput.get()
val checkedItems = availableEntries.value.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) {
if (it.isChecked) it.name else null
}
val result = CompositeResult()
val step = 1f / 6f
progress.value = 0f
if (BackupEntry.Name.HISTORY in checkedItems) {
backup.getEntry(BackupEntry.Name.HISTORY)?.let {
result += repository.restoreHistory(it)
}
}
progress.value = 0.2f
backup.getEntry(BackupEntry.CATEGORIES)?.let {
progress.value += step
if (BackupEntry.Name.CATEGORIES in checkedItems) {
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let {
result += repository.restoreCategories(it)
}
}
progress.value = 0.4f
backup.getEntry(BackupEntry.FAVOURITES)?.let {
progress.value += step
if (BackupEntry.Name.FAVOURITES in checkedItems) {
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let {
result += repository.restoreFavourites(it)
}
}
progress.value = 0.6f
backup.getEntry(BackupEntry.BOOKMARKS)?.let {
progress.value += step
if (BackupEntry.Name.BOOKMARKS in checkedItems) {
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let {
result += repository.restoreBookmarks(it)
}
}
progress.value = 0.8f
backup.getEntry(BackupEntry.SETTINGS)?.let {
progress.value += step
if (BackupEntry.Name.SOURCES in checkedItems) {
backup.getEntry(BackupEntry.Name.SOURCES)?.let {
result += repository.restoreSources(it)
}
}
progress.value += step
if (BackupEntry.Name.SETTINGS in checkedItems) {
backup.getEntry(BackupEntry.Name.SETTINGS)?.let {
result += repository.restoreSettings(it)
}
}
progress.value = 1f
onRestoreDone.call(result)
} finally {
backup.close()
backup.file.delete()
progress.value = 1f
onRestoreDone.call(result)
}
}
/**
* Check for inconsistent user selection
* Favorites cannot be restored without categories
*/
private fun MutableMap<BackupEntry.Name, BackupEntryModel>.validate() {
val favorites = this[BackupEntry.Name.FAVOURITES] ?: return
val categories = this[BackupEntry.Name.CATEGORIES]
if (categories?.isChecked == true) {
if (!favorites.isEnabled) {
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = true)
}
} else {
if (favorites.isEnabled) {
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = false, isChecked = false)
}
}
}

View File

@@ -1,64 +0,0 @@
package org.koitharu.kotatsu.settings.onboard
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showAllowStateLoss
import org.koitharu.kotatsu.databinding.DialogOnboardBinding
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocaleListener
import org.koitharu.kotatsu.settings.onboard.adapter.SourceLocalesAdapter
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
@AndroidEntryPoint
class OnboardDialogFragment :
AlertDialogFragment<DialogOnboardBinding>(),
DialogInterface.OnClickListener, SourceLocaleListener {
private val viewModel by viewModels<OnboardViewModel>()
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
) = DialogOnboardBinding.inflate(inflater, container, false)
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
super.onBuildDialog(builder)
.setPositiveButton(R.string.done, this)
.setCancelable(false)
builder.setTitle(R.string.welcome)
return builder
}
override fun onViewBindingCreated(binding: DialogOnboardBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = SourceLocalesAdapter(this)
binding.recyclerView.adapter = adapter
binding.textViewTitle.setText(R.string.onboard_text)
viewModel.list.observe(viewLifecycleOwner, adapter)
}
override fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean) {
viewModel.setItemChecked(item.key, isChecked)
}
override fun onClick(dialog: DialogInterface, which: Int) {
when (which) {
DialogInterface.BUTTON_POSITIVE -> dialog.dismiss()
}
}
companion object {
private const val TAG = "OnboardDialog"
fun show(fm: FragmentManager) = OnboardDialogFragment().showAllowStateLoss(fm, TAG)
}
}

View File

@@ -1,91 +0,0 @@
package org.koitharu.kotatsu.settings.onboard
import android.content.Context
import androidx.core.os.ConfigurationCompat
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class OnboardViewModel @Inject constructor(
private val repository: MangaSourcesRepository,
@ApplicationContext context: Context,
) : BaseViewModel() {
private val allSources = repository.allMangaSources
private val locales = allSources.groupBy { it.locale }
private val selectedLocales = HashSet<String?>()
val list = MutableStateFlow<List<SourceLocale>?>(null)
private var updateJob: Job
init {
updateJob = launchJob(Dispatchers.Default) {
if (repository.isSetupRequired()) {
selectedLocales += ConfigurationCompat.getLocales(context.resources.configuration).toList()
.firstOrNull { lc -> lc.language in locales.keys }?.language
selectedLocales += null
} else {
selectedLocales.addAll(
repository.getEnabledSources().mapNotNullToSet { x -> x.locale },
)
}
commit()
}
}
fun setItemChecked(key: String?, isChecked: Boolean) {
val isModified = if (isChecked) {
selectedLocales.add(key)
} else {
selectedLocales.remove(key)
}
if (isModified) {
val prevJob = updateJob
updateJob = launchJob(Dispatchers.Default) {
prevJob.join()
commit()
}
}
}
private suspend fun commit() {
val enabledSources = allSources.filterTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
x.locale in selectedLocales
}
repository.setSourcesEnabledExclusive(enabledSources)
list.value = locales.map { (key, srcs) ->
val locale = key?.let { Locale(it) }
SourceLocale(
key = key,
title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale),
summary = srcs.joinToString { it.title },
isChecked = key in selectedLocales,
)
}.sortedWithSafe(SourceLocaleComparator())
}
private class SourceLocaleComparator : Comparator<SourceLocale> {
private val delegate = nullsFirst(LocaleComparator())
override fun compare(a: SourceLocale, b: SourceLocale): Int {
val localeA = a.key?.let { Locale(it) }
val localeB = b.key?.let { Locale(it) }
return delegate.compare(localeA, localeB)
}
}
}

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.settings.onboard.adapter
import android.widget.CompoundButton
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemSourceLocaleBinding
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
fun sourceLocaleAD(
listener: SourceLocaleListener,
) = adapterDelegateViewBinding<SourceLocale, SourceLocale, ItemSourceLocaleBinding>(
{ inflater, parent -> ItemSourceLocaleBinding.inflate(inflater, parent, false) },
) {
val checkedChangeListener = CompoundButton.OnCheckedChangeListener { _, isChecked ->
listener.onItemCheckedChanged(item, isChecked)
}
binding.switchToggle.setOnCheckedChangeListener(checkedChangeListener)
bind { payloads ->
binding.textViewTitle.text = item.title ?: getString(R.string.different_languages)
binding.textViewDescription.textAndVisible = item.summary
binding.switchToggle.setOnCheckedChangeListener(null)
binding.switchToggle.setChecked(item.isChecked, payloads.isNotEmpty())
binding.switchToggle.setOnCheckedChangeListener(checkedChangeListener)
}
}

View File

@@ -1,8 +0,0 @@
package org.koitharu.kotatsu.settings.onboard.adapter
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
interface SourceLocaleListener {
fun onItemCheckedChanged(item: SourceLocale, isChecked: Boolean)
}

View File

@@ -1,13 +0,0 @@
package org.koitharu.kotatsu.settings.onboard.adapter
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
class SourceLocalesAdapter(
listener: SourceLocaleListener,
) : BaseListAdapter<SourceLocale>() {
init {
delegatesManager.addDelegate(sourceLocaleAD(listener))
}
}

View File

@@ -1,35 +0,0 @@
package org.koitharu.kotatsu.settings.onboard.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.util.Locale
data class SourceLocale(
val key: String?,
val title: String?,
val summary: String?,
val isChecked: Boolean,
) : ListModel, Comparable<SourceLocale> {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is SourceLocale && key == other.key
}
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is SourceLocale && previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
override fun compareTo(other: SourceLocale): Int {
return when {
this === other -> 0
key == Locale.getDefault().language -> -2
key == null -> 1
other.key == null -> -1
else -> compareValues(title, other.title)
}
}
}

View File

@@ -1,16 +1,8 @@
package org.koitharu.kotatsu.settings.sources.adapter
import android.content.Context
import android.graphics.Color
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.SuperscriptSpan
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
@@ -19,12 +11,12 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
@@ -51,15 +43,7 @@ fun sourceConfigItemCheckableDelegate(
}
bind {
binding.textViewTitle.text = if (item.isNsfw) {
buildSpannedString {
append(item.source.title)
append(' ')
appendNsfwLabel(context)
}
} else {
item.source.title
}
binding.textViewTitle.text = item.source.getTitle(context)
binding.switchToggle.isChecked = item.isEnabled
binding.switchToggle.isEnabled = item.isAvailable
binding.textViewDescription.text = item.source.getSummary(context)
@@ -101,15 +85,7 @@ fun sourceConfigItemDelegate2(
binding.imageViewMenu.setOnClickListener(eventListener)
bind {
binding.textViewTitle.text = if (item.isNsfw) {
buildSpannedString {
append(item.source.title)
append(' ')
appendNsfwLabel(context)
}
} else {
item.source.title
}
binding.textViewTitle.text = item.source.getTitle(context)
binding.imageViewAdd.isGone = item.isEnabled || !item.isAvailable
binding.imageViewRemove.isVisible = item.isEnabled
binding.imageViewMenu.isVisible = item.isEnabled
@@ -147,19 +123,6 @@ fun sourceConfigEmptySearchDelegate() =
R.layout.item_sources_empty,
) { }
fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
ForegroundColorSpan(
context.getThemeColor(
com.google.android.material.R.attr.colorError,
Color.RED,
),
),
RelativeSizeSpan(0.74f),
SuperscriptSpan(),
) {
append(context.getString(R.string.nsfw))
}
private fun showSourceMenu(
anchor: View,
item: SourceConfigItem.SourceItem,

View File

@@ -1,24 +1,27 @@
package org.koitharu.kotatsu.settings.sources.catalog
import androidx.core.text.buildSpannedString
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier.Companion.ignoreCaptchaErrors
import org.koitharu.kotatsu.core.model.getSummary
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
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.ItemCatalogPageBinding
import org.koitharu.kotatsu.databinding.ItemEmptyHintBinding
import org.koitharu.kotatsu.databinding.ItemSourceCatalogBinding
import org.koitharu.kotatsu.settings.sources.adapter.appendNsfwLabel
fun sourceCatalogItemSourceAD(
coil: ImageLoader,
@@ -35,15 +38,7 @@ fun sourceCatalogItemSourceAD(
}
bind {
binding.textViewTitle.text = if (item.source.isNsfw()) {
buildSpannedString {
append(item.source.title)
append(' ')
appendNsfwLabel(context)
}
} else {
item.source.title
}
binding.textViewTitle.text = item.source.getTitle(context)
if (item.showSummary) {
binding.textViewDescription.text = item.source.getSummary(context)
binding.textViewDescription.isVisible = true
@@ -57,6 +52,7 @@ fun sourceCatalogItemSourceAD(
placeholder(fallbackIcon)
fallback(fallbackIcon)
source(item.source)
ignoreCaptchaErrors()
enqueueWith(coil)
}
}
@@ -77,3 +73,30 @@ fun sourceCatalogItemHintAD(
binding.textSecondary.setTextAndVisible(item.text)
}
}
fun sourceCatalogPageAD(
listener: OnListItemClickListener<SourceCatalogItem.Source>,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceCatalogPage, SourceCatalogPage, ItemCatalogPageBinding>(
{ inflater, parent -> ItemCatalogPageBinding.inflate(inflater, parent, false) },
) {
val sourcesAdapter = SourcesCatalogAdapter(listener, coil, lifecycleOwner)
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = sourcesAdapter
}
val insetsDelegate = WindowInsetsDelegate()
ViewCompat.setOnApplyWindowInsetsListener(itemView, insetsDelegate)
itemView.addOnLayoutChangeListener(insetsDelegate)
insetsDelegate.addInsetsListener { insets ->
binding.recyclerView.updatePadding(
bottom = insets.bottom + binding.recyclerView.paddingTop,
)
}
bind {
sourcesAdapter.items = item.items
}
}

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.settings.sources.catalog
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.ContentType
data class SourceCatalogPage(
val type: ContentType,
val items: List<SourceCatalogItem>,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is SourceCatalogPage && other.type == type
}
override fun getChangePayload(previousState: ListModel): Any? {
return ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED
}
}

View File

@@ -10,30 +10,32 @@ 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 com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.R
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.getLocaleDisplayName
import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.toLocale
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.settings.newsources.NewSourcesDialogFragment
import javax.inject.Inject
@AndroidEntryPoint
class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
TabLayout.OnTabSelectedListener,
OnListItemClickListener<SourceCatalogItem.Source>,
AppBarOwner, MenuItem.OnActionExpandListener {
@Inject
lateinit var coil: ImageLoader
private var newSourcesSnackbar: Snackbar? = null
override val appBar: AppBarLayout
get() = viewBinding.appbar
@@ -43,19 +45,18 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
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)
val pagerAdapter = SourcesCatalogPagerAdapter(this, coil, this)
viewBinding.pager.adapter = pagerAdapter
val tabMediator = TabLayoutMediator(viewBinding.tabs, viewBinding.pager, pagerAdapter)
tabMediator.attach()
viewModel.content.observe(this, pagerAdapter)
viewModel.hasNewSources.observe(this, ::onHasNewSourcesChanged)
viewModel.onActionDone.observeEvent(
this,
ReversibleActionObserver(viewBinding.recyclerView),
ReversibleActionObserver(viewBinding.pager),
)
viewModel.locale.observe(this) {
supportActionBar?.subtitle = it.getLocaleDisplayName(this)
supportActionBar?.subtitle = it?.toLocale().getDisplayName(this)
}
addMenuProvider(SourcesCatalogMenuProvider(this, viewModel, this))
}
@@ -65,27 +66,15 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
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
viewBinding.pager.isUserInputEnabled = false
val sq = (item.actionView as? SearchView)?.query?.trim()?.toString().orEmpty()
viewModel.performSearch(sq)
return true
@@ -93,21 +82,35 @@ class SourcesCatalogActivity : BaseActivity<ActivitySourcesCatalogBinding>(),
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
viewBinding.tabs.isVisible = true
viewBinding.pager.isUserInputEnabled = 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
private fun onHasNewSourcesChanged(hasNewSources: Boolean) {
if (hasNewSources) {
if (newSourcesSnackbar?.isShownOrQueued == true) {
return
}
val tab = tabs.newTab()
tab.setText(type.titleResId)
tab.tag = type
tabs.addTab(tab)
val snackbar = Snackbar.make(viewBinding.pager, R.string.new_sources_text, Snackbar.LENGTH_INDEFINITE)
snackbar.setAction(R.string.explore) {
NewSourcesDialogFragment.show(supportFragmentManager)
}
snackbar.addCallback(
object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (event == DISMISS_EVENT_SWIPE) {
viewModel.skipNewSources()
}
}
},
)
snackbar.show()
newSourcesSnackbar = snackbar
} else {
newSourcesSnackbar?.dismiss()
newSourcesSnackbar = null
}
tabs.addOnTabSelectedListener(this)
}
}

View File

@@ -9,7 +9,9 @@ import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getLocaleDisplayName
import org.koitharu.kotatsu.core.util.LocaleComparator
import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
class SourcesCatalogMenuProvider(
@@ -57,15 +59,17 @@ class SourcesCatalogMenuProvider(
}
private fun showLocalesMenu() {
val locales = viewModel.locales.map {
it to it.getLocaleDisplayName(activity)
val locales = viewModel.locales.mapTo(ArrayList(viewModel.locales.size)) {
it to it?.toLocale()
}
locales.sortWith(compareBy(nullsFirst(LocaleComparator())) { it.second })
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()) {
menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second)
menu.menu.add(Menu.NONE, Menu.NONE, i, lc.second.getDisplayName(activity))
}
menu.setOnMenuItemClickListener {
viewModel.setLocale(locales.getOrNull(it.order)?.first)

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.settings.sources.catalog
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
class SourcesCatalogPagerAdapter(
listener: OnListItemClickListener<SourceCatalogItem.Source>,
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
) : BaseListAdapter<SourceCatalogPage>(), TabLayoutMediator.TabConfigurationStrategy {
init {
delegatesManager.addDelegate(sourceCatalogPageAD(listener, coil, lifecycleOwner))
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = items.getOrNull(position) ?: return
tab.setText(item.type.titleResId)
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.settings.sources.catalog
import androidx.annotation.MainThread
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -8,9 +9,10 @@ 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.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -21,6 +23,8 @@ 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.EnumMap
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
@@ -28,30 +32,27 @@ import javax.inject.Inject
class SourcesCatalogViewModel @Inject constructor(
private val repository: MangaSourcesRepository,
private val listProducerFactory: SourcesCatalogListProducer.Factory,
settings: AppSettings,
private val settings: AppSettings,
) : BaseViewModel() {
private val lifecycle = RetainedLifecycleImpl()
private var searchQuery: String? = null
val onActionDone = MutableEventFlow<ReversibleAction>()
val contentType = MutableStateFlow(ContentType.entries.first())
val locales = repository.allMangaSources.mapToSet { it.locale }
val locale = MutableStateFlow(Locale.getDefault().language.takeIf { it in locales })
val isNsfwDisabled = settings.isNsfwContentDisabled
val hasNewSources = repository.observeNewSources()
.map { it.isNotEmpty() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, false)
private val listProducer: StateFlow<SourcesCatalogListProducer?> = combine(
locale,
contentType,
) { lc, type ->
listProducerFactory.create(lc, type, lifecycle).also {
it.setQuery(searchQuery)
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private val listProducers = locale.map { lc ->
createListProducers(lc)
}.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value))
val content = listProducer.flatMapLatest {
it?.list ?: emptyFlow()
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
val content: StateFlow<List<SourceCatalogPage>> = listProducers.flatMapLatest {
val flows = it.entries.map { (type, producer) -> producer.list.map { x -> SourceCatalogPage(type, x) } }
combine<SourceCatalogPage, List<SourceCatalogPage>>(flows, Array<SourceCatalogPage>::toList)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
override fun onCleared() {
super.onCleared()
@@ -60,21 +61,36 @@ class SourcesCatalogViewModel @Inject constructor(
fun performSearch(query: String?) {
searchQuery = query
listProducer.value?.setQuery(query)
listProducers.value.forEach { (_, v) -> v.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))
}
}
fun skipNewSources() {
launchJob {
repository.assimilateNewSources()
}
}
@MainThread
private fun createListProducers(lc: String?): Map<ContentType, SourcesCatalogListProducer> {
val types = EnumSet.allOf(ContentType::class.java)
if (settings.isNsfwContentDisabled) {
types.remove(ContentType.HENTAI)
}
return types.associateWithTo(EnumMap(ContentType::class.java)) { type ->
listProducerFactory.create(lc, type, lifecycle).also {
it.setQuery(searchQuery)
}
}
}
}

View File

@@ -84,14 +84,14 @@ class TrackerSettingsFragment :
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
startActivity(intent)
startActivitySafe(intent)
true
}
channels.areNotificationsDisabled -> {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", requireContext().packageName, null))
startActivity(intent)
startActivitySafe(intent)
true
}

View File

@@ -6,6 +6,7 @@ import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.CompositeMutex2
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
@@ -13,6 +14,8 @@ import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
import org.koitharu.kotatsu.tracker.work.TrackingItem
import javax.inject.Inject
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
class Tracker @Inject constructor(
private val settings: AppSettings,
@@ -77,7 +80,10 @@ class Tracker @Inject constructor(
repository.gc()
}
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates.Success {
suspend fun fetchUpdates(
track: MangaTracking,
commit: Boolean
): MangaUpdates.Success = withMangaLock(track.manga.id) {
val repo = mangaRepositoryFactory.create(track.manga.source)
require(repo is RemoteMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
val manga = repo.getDetails(track.manga, CachePolicy.WRITE_ONLY)
@@ -99,7 +105,7 @@ class Tracker @Inject constructor(
}
@VisibleForTesting
suspend fun deleteTrack(mangaId: Long) {
suspend fun deleteTrack(mangaId: Long) = withMangaLock(mangaId) {
repository.deleteTrack(mangaId)
}
@@ -137,4 +143,21 @@ class Tracker @Inject constructor(
}
}
}
private companion object {
private val mangaMutex = CompositeMutex2<Long>()
suspend inline fun <T> withMangaLock(id: Long, action: () -> T): T {
contract {
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
}
mangaMutex.lock(id)
try {
return action()
} finally {
mangaMutex.unlock(id)
}
}
}
}

View File

@@ -31,6 +31,7 @@ import javax.inject.Inject
import javax.inject.Provider
private const val NO_ID = 0L
private const val MAX_QUERY_IDS = 100
@Reusable
class TrackingRepository @Inject constructor(
@@ -65,7 +66,14 @@ class TrackingRepository @Inject constructor(
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
val ids = mangaList.mapToSet { it.id }
val tracks = db.getTracksDao().findAll(ids).groupBy { it.mangaId }
val dao = db.getTracksDao()
val tracks = if (ids.size <= MAX_QUERY_IDS) {
dao.findAll(ids)
} else {
// TODO split tracks in the worker
ids.windowed(MAX_QUERY_IDS, MAX_QUERY_IDS, true)
.flatMap { dao.findAll(it) }
}.groupBy { it.mangaId }
val idSet = HashSet<Long>()
val result = ArrayList<MangaTracking>(mangaList.size)
for (item in mangaList) {

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/m3ColorBottomMenuBackground" />
</selector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/kotatsu_onSecondaryContainer" android:state_checked="true" />
<item android:color="@color/kotatsu_secondaryContainer" android:state_checked="true" />
<item android:color="@color/kotatsu_onSurfaceVariant" android:state_checked="false" />
</selector>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/kotatsu_m3_bottom_menu" />
</selector>

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="splash"
android:width="320dp"
android:height="320dp"
android:viewportWidth="320"
android:viewportHeight="320">
<group
android:name="scaleme"
android:pivotX="160"
android:pivotY="160"
android:scaleX="1"
android:scaleY="1">
<group android:name="bg">
<path
android:name="circle"
android:pathData="M 160 110 C 146.744 110 134.018 115.271 124.645 124.645 C 115.271 134.018 110 146.744 110 160 C 110 173.256 115.271 185.982 124.645 195.355 C 134.018 204.729 146.744 210 160 210 C 173.256 210 185.982 204.729 195.355 195.355 C 204.729 185.982 210 173.256 210 160 C 210 146.744 204.729 134.018 195.355 124.645 C 185.982 115.271 173.256 110 160 110 Z"
android:fillColor="@color/m3_sys_color_dynamic_dark_primary_container"
android:fillAlpha="0"
android:strokeWidth="1" />
</group>
<group
android:name="fg"
android:pivotX="160"
android:pivotY="160"
android:scaleX="0"
android:scaleY="0">
<clip-path
android:name="mask"
android:pathData="M 160 60 C 133.489 60 108.036 70.543 89.289 89.289 C 70.543 108.036 60 133.489 60 160 C 60 186.511 70.543 211.964 89.289 230.711 C 108.036 249.457 133.489 260 160 260 C 186.511 260 211.964 249.457 230.711 230.711 C 249.457 211.964 260 186.511 260 160 C 260 133.489 249.457 108.036 230.711 89.289 C 211.964 70.543 186.511 60 160 60 Z" />
<group
android:name="totoro_group"
android:scaleX="0.73903004444"
android:scaleY="0.73903004444">
<clip-path android:pathData="M0.81,0.07h432v432h-432z" />
<path
android:name="totoro"
android:pathData="M242.26,418.58c-44.77,0 -89.54,0 -134.31,0c-6.96,0 -7.12,-0.56 -6.31,-7.57c6.75,-58.64 31.14,-110.6 61.45,-160.07c6.71,-10.95 9.45,-22.06 11.05,-34.22c1.43,-10.89 5.32,-20.99 13.4,-28.93c5.67,-5.58 9.49,-5.72 14.67,0.29c6.42,7.45 12.41,15.28 18.2,23.23c2.01,2.76 3.88,4.35 7.39,4.3c9.86,-0.15 19.72,-0.13 29.58,-0.01c3.26,0.04 5.21,-1.26 7.14,-3.9c6.02,-8.18 12.19,-16.28 18.74,-24.04c4.56,-5.41 8.36,-5.29 13.65,-0.51c8.35,7.53 11.83,17.58 13.9,28.16c1.14,5.82 1.07,11.88 2.18,17.71c0.61,3.18 2.21,6.32 3.97,9.1c19.34,30.51 35.86,62.44 48.61,96.27c8.91,23.64 15.25,47.93 18.01,73.09c0.72,6.6 0.28,7.1 -6.53,7.1C332.12,418.59 287.19,418.58 242.26,418.58zM172.46,404.96c0.23,-0.04 1.58,0.09 2.4,-0.51c8.32,-6.01 16.3,-5.48 24.88,-0.06c3.28,2.07 7.64,0.92 10.26,-2.87c2.4,-3.47 2.93,-7.99 -0.37,-10.11c-6.96,-4.48 -14.69,-7.79 -22.16,-11.45c-0.71,-0.35 -1.97,0.02 -2.8,0.42c-6.1,2.95 -12.31,5.72 -18.16,9.11c-3.47,2.01 -4.86,5.62 -3.44,9.68C164.45,403.11 167.3,405.15 172.46,404.96zM218.13,370.48c-0.72,7.32 5.26,12.42 10.7,10.65c3.5,-1.14 6.64,-3.36 10.15,-4.45c2.19,-0.68 4.93,-0.73 7.1,-0.05c3.5,1.09 6.63,3.81 10.14,4.31c2.71,0.39 6.58,-0.8 8.42,-2.72c1.69,-1.77 2.38,-5.74 1.7,-8.26c-1.66,-6.13 -21.82,-15 -27.76,-12.78c-0.3,0.11 -0.6,0.25 -0.88,0.39c-4.76,2.37 -9.72,4.42 -14.2,7.23C220.99,366.38 219.28,369.23 218.13,370.48zM312.12,405.04c5.83,-0.02 8.75,-2.02 9.96,-6c1.29,-4.24 -0.29,-7.83 -4,-9.92c-5.46,-3.07 -11.09,-5.89 -16.87,-8.3c-1.97,-0.82 -4.88,-0.73 -6.87,0.1c-5.49,2.28 -10.81,5.01 -16.03,7.87c-4.63,2.54 -6.06,7.19 -3.97,11.49c2.04,4.18 6.73,5.95 11.55,4.09c2.69,-1.04 5.36,-2.29 7.8,-3.82c3.12,-1.96 5.81,-1.75 8.95,0.07C306.06,402.61 309.82,403.99 312.12,405.04zM186.6,262.09c0.05,8.66 5.72,15.1 14.55,16.52c7.21,1.16 15.06,-3.96 17.57,-11.47c2.13,-6.36 0.68,-9.3 -5.95,-10.15c-3.72,-0.47 -4.65,-1.86 -5.04,-5.35c-0.62,-5.66 -3.07,-7 -8.67,-5.72C191.46,247.66 186.55,254.03 186.6,262.09zM282.02,278.75c7.26,0.03 14.41,-5.46 16.13,-12.38c1.46,-5.89 0.26,-8.38 -5.73,-9.23c-4.07,-0.58 -5.45,-2.48 -5.77,-6.12c-0.33,-3.76 -2.15,-6.05 -6.28,-5.61c-8.6,0.91 -15.51,8.95 -14.97,17.39C265.96,271.4 273.59,278.72 282.02,278.75zM259.29,278.52c-0.09,-5.33 -3.97,-9.16 -8.9,-9.28c-5.17,-0.13 -10.35,0 -15.52,-0.04c-3.82,-0.03 -6.54,1.76 -8.08,5.12c-1.56,3.4 -0.95,6.7 1.57,9.44c2.62,2.85 5.33,5.66 8.23,8.22c3.42,3.02 7.46,3.55 11.05,0.59c3.59,-2.97 6.8,-6.44 9.9,-9.93C258.63,281.4 258.91,279.45 259.29,278.52z"
android:fillType="evenOdd"
android:fillColor="@color/m3_sys_color_dynamic_dark_primary"
android:fillAlpha="0"
android:strokeWidth="1"
tools:targetApi="s" />
</group>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="circle">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:propertyName="pathData"
android:startOffset="200"
android:duration="300"
android:valueFrom="M 160 110 C 146.744 110 134.018 115.271 124.645 124.645 C 115.271 134.018 110 146.744 110 160 C 110 173.256 115.271 185.982 124.645 195.355 C 134.018 204.729 146.744 210 160 210 C 173.256 210 185.982 204.729 195.355 195.355 C 204.729 185.982 210 173.256 210 160 C 210 146.744 204.729 134.018 195.355 124.645 C 185.982 115.271 173.256 110 160 110 Z"
android:valueTo="M 160 60 C 133.489 60 108.036 70.543 89.289 89.289 C 70.543 108.036 60 133.489 60 160 C 60 186.511 70.543 211.964 89.289 230.711 C 108.036 249.457 133.489 260 160 260 C 186.511 260 211.964 249.457 230.711 230.711 C 249.457 211.964 260 186.511 260 160 C 260 133.489 249.457 108.036 230.711 89.289 C 211.964 70.543 186.511 60 160 60 Z"
android:valueType="pathType"
android:interpolator="@android:anim/overshoot_interpolator" />
<objectAnimator
android:propertyName="fillAlpha"
android:startOffset="200"
android:duration="100"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in" />
</set>
</aapt:attr>
</target>
<target android:name="totoro_group">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:startOffset="200"
android:duration="300"
android:propertyName="translateY"
android:valueFrom="160"
android:valueTo="0"
android:interpolator="@android:interpolator/fast_out_slow_in" />
<objectAnimator
android:startOffset="200"
android:duration="300"
android:propertyName="scaleY"
android:valueFrom="0.5"
android:valueTo="0.73903004444"
android:interpolator="@android:interpolator/fast_out_slow_in" />
</set>
</aapt:attr>
</target>
<target android:name="totoro">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:propertyName="fillAlpha"
android:startOffset="200"
android:duration="100"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in" />
</set>
</aapt:attr>
</target>
<target android:name="fg">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:propertyName="scaleX"
android:startOffset="200"
android:duration="300"
android:valueFrom="0.5"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:anim/overshoot_interpolator" />
<objectAnimator
android:propertyName="scaleY"
android:startOffset="200"
android:duration="300"
android:valueFrom="0.5"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:anim/overshoot_interpolator" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -1,64 +1,131 @@
<animated-vector
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="288dp"
android:height="288dp"
android:viewportWidth="288"
android:viewportHeight="288">
<path
android:name="path"
android:fillColor="@color/splash_primary"
android:pathData="M 144 121.499 C 121.258 121.499 102.75 141.685 102.75 166.499 C 102.75 191.314 121.258 211.499 144 211.499 C 166.741 211.499 185.25 191.314 185.25 166.499 C 185.25 141.685 166.742 121.499 144 121.499 Z M 128.604 156.927 C 127.945 158.238 126.619 159 125.25 159 C 124.686 159 124.114 158.875 123.572 158.604 L 117.749 155.689 L 111.926 158.604 C 110.066 159.534 107.825 158.772 106.894 156.927 C 105.972 155.074 106.719 152.825 108.572 151.895 L 116.072 148.145 C 117.126 147.617 118.372 147.617 119.426 148.145 L 126.926 151.895 C 128.78 152.825 129.527 155.074 128.604 156.927 Z M 154.854 145.677 C 154.195 146.988 152.869 147.75 151.5 147.75 C 150.936 147.75 150.364 147.625 149.822 147.354 L 144 144.439 L 138.177 147.354 C 136.316 148.284 134.075 147.522 133.145 145.677 C 132.222 143.824 132.969 141.575 134.822 140.645 L 142.322 136.895 C 143.377 136.367 144.622 136.367 145.677 136.895 L 153.177 140.645 C 155.03 141.575 155.777 143.824 154.854 145.677 Z M 179.427 151.895 C 181.28 152.825 182.027 155.074 181.104 156.927 C 180.445 158.238 179.119 159 177.75 159 C 177.186 159 176.615 158.875 176.072 158.604 L 170.25 155.689 L 164.427 158.604 C 162.574 159.534 160.325 158.772 159.395 156.927 C 158.472 155.074 159.219 152.825 161.072 151.895 L 168.572 148.145 C 169.627 147.617 170.872 147.617 171.927 148.145 Z"
android:strokeWidth="1" />
<path
android:name="path_1"
android:fillAlpha="0"
android:fillColor="@color/splash_primary"
android:pathData="M 192.709 104.964 C 191.142 101.857 189.595 98.926 188.115 96.247 L 184.395 89.731 C 181.336 84.548 178.879 80.775 177.742 79.082 C 177.42 59.584 169.136 54.875 168.177 54.391 C 166.734 53.681 164.984 53.952 163.848 55.094 C 158.333 60.61 154.744 66.103 153.045 68.996 L 134.954 68.996 C 133.255 66.103 129.666 60.61 124.151 55.094 C 123.016 53.952 121.272 53.681 119.822 54.391 C 118.863 54.875 110.579 59.584 110.257 79.082 C 109.12 80.775 106.663 84.547 103.605 89.731 L 99.882 96.252 C 98.401 98.933 96.853 101.864 95.285 104.974 L 91.935 111.87 C 83.959 128.994 76.5 150.313 76.5 170.248 C 76.5 201.442 102.72 228.037 103.841 229.151 C 104.251 229.561 104.757 229.876 105.306 230.059 C 105.79 230.22 117.362 233.999 136.5 233.999 C 137.496 233.999 138.448 233.604 139.151 232.901 L 141.803 230.249 L 146.197 230.249 L 148.849 232.901 C 149.552 233.604 150.504 233.999 151.5 233.999 C 170.639 233.999 182.211 230.22 182.694 230.059 C 183.244 229.876 183.749 229.561 184.159 229.151 C 185.279 228.037 211.5 201.443 211.5 170.248 C 211.5 150.313 204.041 128.994 196.064 111.87 Z M 162.75 83.999 C 164.822 83.999 164.735 87.309 164.735 89.382 C 166.808 89.382 170.25 89.427 170.25 91.5 C 170.25 95.638 166.888 98.999 162.75 98.999 C 158.612 98.999 155.25 95.638 155.25 91.5 C 155.25 87.361 158.612 83.999 162.75 83.999 Z M 136.785 97.564 C 137.364 96.165 138.734 95.25 140.25 95.25 L 147.75 95.25 C 149.266 95.25 150.635 96.165 151.214 97.564 C 151.793 98.963 151.47 100.582 150.401 101.651 L 146.651 105.401 C 145.918 106.133 144.959 106.5 143.999 106.5 C 143.04 106.5 142.08 106.133 141.348 105.401 L 137.597 101.651 C 136.529 100.582 136.207 98.963 136.785 97.564 Z M 125.25 83.999 C 127.323 83.999 127.235 87.309 127.235 89.382 C 129.308 89.382 132.75 89.427 132.75 91.5 C 132.75 95.638 129.388 98.999 125.25 98.999 C 121.112 98.999 117.75 95.637 117.75 91.5 C 117.75 87.361 121.112 83.999 125.25 83.999 Z M 144 218.999 C 117.12 218.999 95.25 195.445 95.25 166.499 C 95.25 137.554 117.12 114 144 114 C 170.88 114 192.75 137.554 192.75 166.499 C 192.75 195.445 170.88 218.999 144 218.999 Z"
android:strokeWidth="1" />
<path
android:name="path_2"
android:fillAlpha="0"
android:fillColor="#ef5350"
android:pathData="M 160.739 158.44 C 158.668 157.198 158.323 154.077 160.084 152.508 C 160.329 152.29 162.49 151.126 164.885 149.923 C 168.283 148.216 169.46 147.734 170.24 147.734 C 171.02 147.734 172.231 148.231 175.782 150.008 C 181.059 152.65 181.457 153.016 181.457 155.234 C 181.457 156.765 181.009 157.577 179.655 158.499 C 178.268 159.443 177.207 159.257 173.531 157.425 L 170.292 155.811 L 166.974 157.429 C 163.198 159.271 162.354 159.408 160.739 158.44 Z M 134.489 147.254 C 132.75 146.194 132.17 143.625 133.288 141.937 C 134.027 140.82 142.612 136.429 144.055 136.429 C 144.759 136.429 146.131 136.998 149.552 138.71 C 153.393 140.632 154.194 141.125 154.638 141.844 C 155.428 143.122 155.516 144.373 154.903 145.613 C 154.216 147.001 153.386 147.558 151.813 147.689 C 150.606 147.789 150.298 147.688 147.211 146.177 L 143.902 144.559 L 140.733 146.146 C 137.176 147.929 135.951 148.146 134.489 147.254 Z M 108.448 158.594 C 107.987 158.349 107.357 157.781 107.048 157.331 C 106.572 156.639 106.5 156.284 106.578 155.015 C 106.709 152.893 107.213 152.446 112.383 149.872 C 115.854 148.143 116.881 147.734 117.748 147.734 C 118.625 147.734 119.651 148.152 123.361 150.023 C 127.26 151.989 127.981 152.44 128.46 153.211 C 130.051 155.77 128.211 159.04 125.18 159.04 C 124.616 159.04 123.247 158.499 121.093 157.425 L 117.853 155.811 L 114.536 157.429 C 110.902 159.201 109.938 159.386 108.448 158.594 Z"
android:strokeWidth="1" />
android:name="splash"
android:width="320dp"
android:height="320dp"
android:viewportWidth="320"
android:viewportHeight="320">
<group
android:name="scaleme"
android:pivotX="160"
android:pivotY="160"
android:scaleX="1"
android:scaleY="1">
<group android:name="bg">
<path
android:name="circle"
android:pathData="M 160 110 C 146.744 110 134.018 115.271 124.645 124.645 C 115.271 134.018 110 146.744 110 160 C 110 173.256 115.271 185.982 124.645 195.355 C 134.018 204.729 146.744 210 160 210 C 173.256 210 185.982 204.729 195.355 195.355 C 204.729 185.982 210 173.256 210 160 C 210 146.744 204.729 134.018 195.355 124.645 C 185.982 115.271 173.256 110 160 110 Z"
android:fillColor="@color/m3_sys_color_dynamic_light_primary_container"
android:fillAlpha="0"
android:strokeWidth="1" />
</group>
<group
android:name="fg"
android:pivotX="160"
android:pivotY="160"
android:scaleX="0"
android:scaleY="0">
<clip-path
android:name="mask"
android:pathData="M 160 60 C 133.489 60 108.036 70.543 89.289 89.289 C 70.543 108.036 60 133.489 60 160 C 60 186.511 70.543 211.964 89.289 230.711 C 108.036 249.457 133.489 260 160 260 C 186.511 260 211.964 249.457 230.711 230.711 C 249.457 211.964 260 186.511 260 160 C 260 133.489 249.457 108.036 230.711 89.289 C 211.964 70.543 186.511 60 160 60 Z" />
<group
android:name="totoro_group"
android:scaleX="0.73903004444"
android:scaleY="0.73903004444">
<clip-path android:pathData="M0.81,0.07h432v432h-432z" />
<path
android:name="totoro"
android:pathData="M242.26,418.58c-44.77,0 -89.54,0 -134.31,0c-6.96,0 -7.12,-0.56 -6.31,-7.57c6.75,-58.64 31.14,-110.6 61.45,-160.07c6.71,-10.95 9.45,-22.06 11.05,-34.22c1.43,-10.89 5.32,-20.99 13.4,-28.93c5.67,-5.58 9.49,-5.72 14.67,0.29c6.42,7.45 12.41,15.28 18.2,23.23c2.01,2.76 3.88,4.35 7.39,4.3c9.86,-0.15 19.72,-0.13 29.58,-0.01c3.26,0.04 5.21,-1.26 7.14,-3.9c6.02,-8.18 12.19,-16.28 18.74,-24.04c4.56,-5.41 8.36,-5.29 13.65,-0.51c8.35,7.53 11.83,17.58 13.9,28.16c1.14,5.82 1.07,11.88 2.18,17.71c0.61,3.18 2.21,6.32 3.97,9.1c19.34,30.51 35.86,62.44 48.61,96.27c8.91,23.64 15.25,47.93 18.01,73.09c0.72,6.6 0.28,7.1 -6.53,7.1C332.12,418.59 287.19,418.58 242.26,418.58zM172.46,404.96c0.23,-0.04 1.58,0.09 2.4,-0.51c8.32,-6.01 16.3,-5.48 24.88,-0.06c3.28,2.07 7.64,0.92 10.26,-2.87c2.4,-3.47 2.93,-7.99 -0.37,-10.11c-6.96,-4.48 -14.69,-7.79 -22.16,-11.45c-0.71,-0.35 -1.97,0.02 -2.8,0.42c-6.1,2.95 -12.31,5.72 -18.16,9.11c-3.47,2.01 -4.86,5.62 -3.44,9.68C164.45,403.11 167.3,405.15 172.46,404.96zM218.13,370.48c-0.72,7.32 5.26,12.42 10.7,10.65c3.5,-1.14 6.64,-3.36 10.15,-4.45c2.19,-0.68 4.93,-0.73 7.1,-0.05c3.5,1.09 6.63,3.81 10.14,4.31c2.71,0.39 6.58,-0.8 8.42,-2.72c1.69,-1.77 2.38,-5.74 1.7,-8.26c-1.66,-6.13 -21.82,-15 -27.76,-12.78c-0.3,0.11 -0.6,0.25 -0.88,0.39c-4.76,2.37 -9.72,4.42 -14.2,7.23C220.99,366.38 219.28,369.23 218.13,370.48zM312.12,405.04c5.83,-0.02 8.75,-2.02 9.96,-6c1.29,-4.24 -0.29,-7.83 -4,-9.92c-5.46,-3.07 -11.09,-5.89 -16.87,-8.3c-1.97,-0.82 -4.88,-0.73 -6.87,0.1c-5.49,2.28 -10.81,5.01 -16.03,7.87c-4.63,2.54 -6.06,7.19 -3.97,11.49c2.04,4.18 6.73,5.95 11.55,4.09c2.69,-1.04 5.36,-2.29 7.8,-3.82c3.12,-1.96 5.81,-1.75 8.95,0.07C306.06,402.61 309.82,403.99 312.12,405.04zM186.6,262.09c0.05,8.66 5.72,15.1 14.55,16.52c7.21,1.16 15.06,-3.96 17.57,-11.47c2.13,-6.36 0.68,-9.3 -5.95,-10.15c-3.72,-0.47 -4.65,-1.86 -5.04,-5.35c-0.62,-5.66 -3.07,-7 -8.67,-5.72C191.46,247.66 186.55,254.03 186.6,262.09zM282.02,278.75c7.26,0.03 14.41,-5.46 16.13,-12.38c1.46,-5.89 0.26,-8.38 -5.73,-9.23c-4.07,-0.58 -5.45,-2.48 -5.77,-6.12c-0.33,-3.76 -2.15,-6.05 -6.28,-5.61c-8.6,0.91 -15.51,8.95 -14.97,17.39C265.96,271.4 273.59,278.72 282.02,278.75zM259.29,278.52c-0.09,-5.33 -3.97,-9.16 -8.9,-9.28c-5.17,-0.13 -10.35,0 -15.52,-0.04c-3.82,-0.03 -6.54,1.76 -8.08,5.12c-1.56,3.4 -0.95,6.7 1.57,9.44c2.62,2.85 5.33,5.66 8.23,8.22c3.42,3.02 7.46,3.55 11.05,0.59c3.59,-2.97 6.8,-6.44 9.9,-9.93C258.63,281.4 258.91,279.45 259.29,278.52z"
android:fillType="evenOdd"
android:fillColor="@color/m3_sys_color_dynamic_light_primary"
android:fillAlpha="0"
android:strokeWidth="1"
tools:targetApi="s" />
</group>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path_2">
<target android:name="circle">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="300"
android:interpolator="@android:anim/decelerate_interpolator"
android:propertyName="fillAlpha"
android:startOffset="500"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<set>
<objectAnimator
android:propertyName="pathData"
android:startOffset="200"
android:duration="300"
android:valueFrom="M 160 110 C 146.744 110 134.018 115.271 124.645 124.645 C 115.271 134.018 110 146.744 110 160 C 110 173.256 115.271 185.982 124.645 195.355 C 134.018 204.729 146.744 210 160 210 C 173.256 210 185.982 204.729 195.355 195.355 C 204.729 185.982 210 173.256 210 160 C 210 146.744 204.729 134.018 195.355 124.645 C 185.982 115.271 173.256 110 160 110 Z"
android:valueTo="M 160 60 C 133.489 60 108.036 70.543 89.289 89.289 C 70.543 108.036 60 133.489 60 160 C 60 186.511 70.543 211.964 89.289 230.711 C 108.036 249.457 133.489 260 160 260 C 186.511 260 211.964 249.457 230.711 230.711 C 249.457 211.964 260 186.511 260 160 C 260 133.489 249.457 108.036 230.711 89.289 C 211.964 70.543 186.511 60 160 60 Z"
android:valueType="pathType"
android:interpolator="@android:anim/overshoot_interpolator" />
<objectAnimator
android:propertyName="fillAlpha"
android:startOffset="200"
android:duration="100"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in" />
</set>
</aapt:attr>
</target>
<target android:name="path_1">
<target android:name="totoro_group">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="500"
android:interpolator="@android:anim/accelerate_interpolator"
android:propertyName="fillAlpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<set>
<objectAnimator
android:startOffset="200"
android:duration="300"
android:propertyName="translateY"
android:valueFrom="160"
android:valueTo="0"
android:interpolator="@android:interpolator/fast_out_slow_in" />
<objectAnimator
android:startOffset="200"
android:duration="300"
android:propertyName="scaleY"
android:valueFrom="0.5"
android:valueTo="0.73903004444"
android:interpolator="@android:interpolator/fast_out_slow_in" />
</set>
</aapt:attr>
</target>
<target android:name="path">
<target android:name="totoro">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="800"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="pathData"
android:valueFrom="M 144 165.978 C 143.736 165.978 143.522 166.212 143.522 166.499 C 143.522 166.787 143.736 167.021 144 167.021 C 144.263 167.021 144.478 166.787 144.478 166.499 C 144.478 166.212 144.263 165.978 144 165.978 Z M 143.821 166.389 C 143.813 166.404 143.798 166.413 143.783 166.413 C 143.776 166.413 143.769 166.412 143.763 166.408 L 143.696 166.374 L 143.628 166.408 C 143.607 166.418 143.581 166.41 143.57 166.388 C 143.559 166.367 143.568 166.341 143.589 166.33 L 143.676 166.286 C 143.688 166.28 143.703 166.28 143.715 166.286 L 143.802 166.33 C 143.823 166.341 143.832 166.367 143.821 166.388 Z M 144.126 166.258 C 144.118 166.273 144.103 166.282 144.087 166.282 C 144.08 166.282 144.074 166.281 144.067 166.277 L 144 166.244 L 143.932 166.277 C 143.911 166.288 143.885 166.279 143.874 166.258 C 143.863 166.236 143.872 166.21 143.893 166.2 L 143.98 166.156 C 143.992 166.15 144.007 166.15 144.019 166.156 L 144.106 166.2 C 144.128 166.21 144.136 166.236 144.125 166.258 Z M 144.41 166.33 C 144.432 166.341 144.44 166.367 144.43 166.389 C 144.422 166.404 144.407 166.413 144.391 166.413 C 144.384 166.413 144.378 166.412 144.371 166.408 L 144.304 166.374 L 144.236 166.408 C 144.215 166.418 144.189 166.41 144.178 166.388 C 144.167 166.367 144.176 166.341 144.197 166.33 L 144.284 166.286 C 144.297 166.28 144.311 166.28 144.323 166.286 Z"
android:valueTo="M 144 121.499 C 121.258 121.499 102.75 141.685 102.75 166.499 C 102.75 191.314 121.258 211.499 144 211.499 C 166.741 211.499 185.25 191.314 185.25 166.499 C 185.25 141.685 166.742 121.499 144 121.499 Z M 128.604 156.927 C 127.945 158.238 126.619 159 125.25 159 C 124.686 159 124.114 158.875 123.572 158.604 L 117.749 155.689 L 111.926 158.604 C 110.066 159.534 107.825 158.772 106.894 156.927 C 105.972 155.074 106.719 152.825 108.572 151.895 L 116.072 148.145 C 117.126 147.617 118.372 147.617 119.426 148.145 L 126.926 151.895 C 128.78 152.825 129.527 155.074 128.604 156.927 Z M 154.854 145.677 C 154.195 146.988 152.869 147.75 151.5 147.75 C 150.936 147.75 150.364 147.625 149.822 147.354 L 144 144.439 L 138.177 147.354 C 136.316 148.284 134.075 147.522 133.145 145.677 C 132.222 143.824 132.969 141.575 134.822 140.645 L 142.322 136.895 C 143.377 136.367 144.622 136.367 145.677 136.895 L 153.177 140.645 C 155.03 141.575 155.777 143.824 154.854 145.677 Z M 179.427 151.895 C 181.28 152.825 182.027 155.074 181.104 156.927 C 180.445 158.238 179.119 159 177.75 159 C 177.186 159 176.615 158.875 176.072 158.604 L 170.25 155.689 L 164.427 158.604 C 162.574 159.534 160.325 158.772 159.395 156.927 C 158.472 155.074 159.219 152.825 161.072 151.895 L 168.572 148.145 C 169.627 147.617 170.872 147.617 171.927 148.145 Z"
android:valueType="pathType" />
<set>
<objectAnimator
android:propertyName="fillAlpha"
android:startOffset="200"
android:duration="100"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:interpolator/fast_out_slow_in" />
</set>
</aapt:attr>
</target>
<target android:name="fg">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:propertyName="scaleX"
android:startOffset="200"
android:duration="300"
android:valueFrom="0.5"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:anim/overshoot_interpolator" />
<objectAnimator
android:propertyName="scaleY"
android:startOffset="200"
android:duration="300"
android:valueFrom="0.5"
android:valueTo="1"
android:valueType="floatType"
android:interpolator="@android:anim/overshoot_interpolator" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M12 2C13.1 2 14 2.9 14 4S13.1 6 12 6 10 5.1 10 4 10.9 2 12 2M15.9 8.1C15.5 7.7 14.8 7 13.5 7H11C8.2 7 6 4.8 6 2H4C4 5.2 6.1 7.8 9 8.7V22H11V16H13V22H15V10.1L19 14L20.4 12.6L15.9 8.1Z" />
</vector>

View File

@@ -11,7 +11,8 @@
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
@@ -27,21 +28,10 @@
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/layout_list"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
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>
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,78 @@
<?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:gravity="center_vertical"
android:orientation="vertical"
android:paddingVertical="?dialogPreferredPadding">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding"
android:paddingBottom="@dimen/margin_normal"
android:text="@string/restore_backup"
android:textAppearance="?textAppearanceTitleLarge" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding"
android:indeterminate="true"
android:max="100"
android:visibility="gone"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollIndicators="top|bottom"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="6"
tools:listitem="@layout/item_checkable_multiple"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?android:textColorSecondary"
tools:text="@tools:sample/lorem[10]" />
<LinearLayout
style="?buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding"
android:layout_marginTop="@dimen/margin_normal"
android:gravity="end"
android:orientation="horizontal">
<Button
android:id="@+id/button_cancel"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel" />
<Button
android:id="@+id/button_restore"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/restore" />
</LinearLayout>
</LinearLayout>

View File

@@ -1,165 +1,197 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
<FrameLayout
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">
<androidx.constraintlayout.widget.ConstraintLayout
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:scrollIndicators="bottom">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:background="?colorSecondaryContainer"
android:foreground="?selectableItemBackground"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="H,13:18"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:background="@tools:sample/backgrounds/scenic[5]"
tools:ignore="ContentDescription,UnusedAttribute" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:ellipsize="end"
android:maxLines="5"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
app:layout_constraintEnd_toStartOf="@id/button_open"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginEnd="16dp"
tools:text="@tools:sample/lorem" />
android:paddingBottom="?actionBarSize">
<ImageButton
android:id="@+id/button_open"
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:background="?colorSecondaryContainer"
android:foreground="?selectableItemBackground"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="H,13:18"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.3"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
tools:background="@tools:sample/backgrounds/scenic[5]"
tools:ignore="ContentDescription,UnusedAttribute" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:ellipsize="end"
android:maxLines="5"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
app:layout_constraintEnd_toStartOf="@id/button_open"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginEnd="16dp"
tools:text="@tools:sample/lorem" />
<ImageButton
android:id="@+id/button_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/details"
android:padding="12dp"
app:layout_constraintEnd_toStartOf="@id/button_close"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_expand" />
<ImageButton
android:id="@+id/button_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/close"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="?actionModeCloseDrawable"
app:tint="?colorControlNormal" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="3"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:text="@tools:sample/lorem[12]" />
<TextView
android:id="@+id/textView_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:background="@drawable/list_selector"
android:padding="2dp"
android:singleLine="true"
android:textColor="?attr/colorTertiary"
android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
tools:text="@tools:sample/full_names" />
<RatingBar
android:id="@+id/rating_bar"
style="?ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:isIndicator="true"
android:max="1"
android:numStars="5"
android:stepSize="0.5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_author"
tools:rating="4" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:barrierMargin="8dp"
app:constraint_referenced_ids="imageView_cover,rating_bar" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier_header" />
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
android:id="@+id/textView_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_normal"
android:lineSpacingMultiplier="1.2"
android:paddingBottom="@dimen/margin_normal"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chips_tags"
tools:ignore="UnusedAttribute"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?colorBackgroundFloating">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/details"
android:padding="12dp"
app:layout_constraintEnd_toStartOf="@id/button_close"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_expand" />
android:layout_gravity="end|center_vertical"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:enabled="false"
android:text="@string/read"
android:textAllCaps="false"
app:iconGravity="textStart"
app:iconPadding="8dp"
tools:enabled="true"
tools:icon="@drawable/ic_read" />
<ImageButton
android:id="@+id/button_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/close"
android:padding="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="?actionModeCloseDrawable"
app:tint="?colorControlNormal" />
</com.google.android.material.appbar.MaterialToolbar>
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="3"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:text="@tools:sample/lorem[12]" />
<TextView
android:id="@+id/textView_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:background="@drawable/list_selector"
android:padding="2dp"
android:singleLine="true"
android:textColor="?attr/colorTertiary"
android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
tools:text="@tools:sample/full_names" />
<RatingBar
android:id="@+id/rating_bar"
style="?ratingBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:isIndicator="true"
android:max="1"
android:numStars="5"
android:stepSize="0.5"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_author"
tools:rating="4" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:barrierMargin="8dp"
app:constraint_referenced_ids="imageView_cover,rating_bar" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_tags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:chipSpacingHorizontal="6dp"
app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/barrier_header" />
<org.koitharu.kotatsu.core.ui.widgets.SelectableTextView
android:id="@+id/textView_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_normal"
android:lineSpacingMultiplier="1.2"
android:paddingBottom="@dimen/margin_normal"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textIsSelectable="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/chips_tags"
tools:ignore="UnusedAttribute"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</FrameLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_list"
android:layout_width="match_parent"
android:layout_height="match_parent">
<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>

View File

@@ -1,34 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<HorizontalScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
android:scrollbars="none">
<Button
android:id="@+id/button_create"
style="@style/Widget.Material3.Button.OutlinedButton"
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/create_category"
app:icon="@drawable/ic_add" />
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
app:singleLine="true">
<Button
android:id="@+id/button_manage"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/manage"
app:icon="@drawable/ic_edit" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_create"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/create_category"
app:chipIcon="@drawable/ic_add" />
</LinearLayout>
<com.google.android.material.chip.Chip
android:id="@+id/chip_manage"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/manage_categories"
app:chipIcon="@drawable/ic_edit" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>

View File

@@ -5,6 +5,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:gravity="center_vertical"
android:paddingStart="?android:listPreferredItemPaddingStart"

View File

@@ -5,6 +5,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:gravity="center_vertical"
android:paddingStart="?android:listPreferredItemPaddingStart"

View File

@@ -1,9 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
<HorizontalScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="4dp"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingBottom="6dp" />
android:scrollbars="none">
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_genres"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
app:singleLine="true" />
</HorizontalScrollView>

View File

@@ -20,6 +20,7 @@
android:scrollIndicators="top">
<LinearLayout
android:id="@+id/layout_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
@@ -114,10 +115,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingVertical="8dp"
android:drawablePadding="16dp"
android:paddingHorizontal="@dimen/margin_normal"
android:gravity="center_vertical"
android:paddingHorizontal="@dimen/margin_normal"
android:paddingVertical="8dp"
android:textAppearance="?textAppearanceBodySmall"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_error_small"

View File

@@ -17,18 +17,19 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_input_layout"
style="?textInputFilledDenseStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:startIconDrawable="?android:actionModeWebSearchDrawable"
app:endIconMode="clear_text"
app:endIconDrawable="@drawable/abc_ic_clear_material"
android:hint="@string/genres_search_hint"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="4dp"
android:hint="@string/genres_search_hint"
app:endIconDrawable="@drawable/abc_ic_clear_material"
app:endIconMode="clear_text"
app:layout_constraintBottom_toTopOf="@id/recyclerView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headerBar"
app:layout_constraintBottom_toTopOf="@id/recyclerView">
app:startIconDrawable="?android:actionModeWebSearchDrawable">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_search"
@@ -36,7 +37,7 @@
android:layout_height="wrap_content"
android:imeOptions="actionSearch|flagNoFullscreen"
android:importantForAutofill="no"
android:inputType="text"/>
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>

View File

@@ -0,0 +1,117 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
android:id="@+id/headerBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/welcome" />
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollIndicators="top">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/margin_normal">
<TextView
android:id="@+id/textView_welcome_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/welcome"
android:textAppearance="?textAppearanceTitleLarge"
android:drawablePadding="16dp"
app:drawableStartCompat="@drawable/ic_welcome" />
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/margin_normal"
app:singleLine="true">
<com.google.android.material.chip.Chip
android:id="@+id/chip_backup"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/restore_backup"
app:chipIcon="@drawable/ic_backup_restore" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_sync"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sync_auth"
app:chipIcon="@drawable/ic_sync" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<TextView
android:id="@+id/textView_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:paddingHorizontal="@dimen/margin_normal"
android:text="@string/welcome_text"
android:textAppearance="?textAppearanceBodyMedium" />
<TextView
android:id="@+id/textView_locales_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/languages"
android:textAppearance="?textAppearanceTitleSmall" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_locales"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:paddingHorizontal="@dimen/margin_normal"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
<TextView
android:id="@+id/textView_type_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/type"
android:textAppearance="?textAppearanceTitleSmall" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:paddingHorizontal="@dimen/margin_normal"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -178,4 +178,7 @@
<string name="system_default">الافتراضي</string>
<string name="sign_in">تسجبل الدخول</string>
<string name="domain">المجال</string>
<string name="text_history_holder_primary">كل ما تقرأه سيعرض هنا</string>
<string name="text_search_holder_secondary">حاول إعادة صياغة الكلمات.</string>
<string name="text_empty_holder_primary">يبدو أنه فارغ…</string>
</resources>

View File

@@ -538,4 +538,9 @@
<string name="skip">Прапусціць</string>
<string name="color_correction_apply_text">Гэтыя налады могуць прымяняцца глабальна або толькі да бягучай мангі. Пры глабальным прымяненні індывідуальныя налады не будуць перавызначаны.</string>
<string name="grayscale">Адценні шэрага</string>
<string name="disable_battery_optimization_summary_downloads">Можа дапамагчы з пачаткам загрузкі, калі ў вас узнікаюць з ёй праблемы</string>
<string name="welcome_text">Выберыце, якія крыніцы змесціва вы хочаце ўключыць. Гэта таксама можна наладзіць пазней у наладах</string>
<string name="restore">Аднавіць</string>
<string name="backup_date_">Дата стварэння рэзервовай копіі: %s</string>
<string name="sync_auth">Увайдзіце, каб сінхранізаваць уліковы запіс</string>
</resources>

View File

@@ -538,4 +538,11 @@
<string name="error_filter_states_genre_not_supported">Esta fuente no permite filtrar por géneros ni por estados</string>
<string name="error_filter_locale_genre_not_supported">Esta fuente no permite filtrar por géneros ni por configuración regional</string>
<string name="genres_search_hint">Empieza a escribir el nombre del género</string>
<string name="disable_battery_optimization_summary_downloads">Puede que te ayude a iniciar la descarga si tienes algún problema</string>
<string name="welcome_text">Por favor, seleccione las fuentes de contenido que desea habilitar. Esto también se puede configurar más tarde en la configuración</string>
<string name="restore">Restaurar</string>
<string name="backup_date_">Fecha de la copia de seguridad: %s</string>
<string name="sync_auth">Iniciar sesión en la cuenta de sincronización</string>
<string name="by_name_reverse">Nombre invertido</string>
<string name="state_upcoming">Próximamente</string>
</resources>

View File

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

View File

@@ -0,0 +1,413 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="updates">Uuendused</string>
<string name="languages">Keel</string>
<string name="status_re_reading">Uuesti loen</string>
<string name="add_to_favourites">Lisa lemmikuks</string>
<string name="detect_reader_mode">Automaatne lugeja mood</string>
<string name="restore_backup_description">Impordi varem tehtud tagavara oma kasutaja andmetest</string>
<string name="manga_shelf">Riiul</string>
<string name="download_started">Allalaadimine alustatud</string>
<string name="tracking">Jälgimine</string>
<string name="close">Sulge</string>
<string name="progress">Progress</string>
<string name="text_history_holder_secondary">Leia mida lugeda «Seiklemis» osakonnas</string>
<string name="cancel_all">Tühista kõik</string>
<string name="sync_host_description">Saate kasutada ise korraldatud sünkroonimisserverit või tava sünkroonimisserverit. Ärge muutke seda, kui te ei ole kindel, mida teete.</string>
<string name="all_favourites">Kõik lemmikud</string>
<string name="email_enter_hint">Sisesta oma emaili aadress et jätkata</string>
<string name="save">Salvesta</string>
<string name="light_indicator">LED indikaator</string>
<string name="no_chapters">Pole peatükke</string>
<string name="related_manga_summary">Näita seotud mangade nimekirja. Mõnel juhul võib see olla ebatäpne või puududa</string>
<string name="theme_name_dynamic">Dünaamiline</string>
<string name="text_clear_cookies_prompt">Teid logitakse välja kõikidest allikatest</string>
<string name="clear_cookies">Tühjenda küpsisesd</string>
<string name="favourites_categories">Lemmikud kategooriad</string>
<string name="pages_cache">Lehekülgede vahemälu</string>
<string name="local_storage">Kohalik salvestamine</string>
<string name="filter">Filterid</string>
<string name="enabled_sources">Kasutatud allikad</string>
<string name="reset">Nulli</string>
<string name="disable_all">Lülita kõik välja</string>
<string name="error_occurred">Tekkis viga</string>
<string name="gestures_only">Ainult žestid</string>
<string name="order_added">Lisatud</string>
<string name="enable_logging">Lülita sisse loggig</string>
<string name="clear_thumbs_cache">Tühjenda pisipildi vahemälu</string>
<string name="taps_on_edges">Külje puudutused</string>
<string name="switch_pages">Vaheta lehekülge</string>
<string name="settings_apply_restart_required">Muudatuste rakendamiseks käivitage rakendus uuesti</string>
<string name="rotate_screen">Keera ekraani</string>
<string name="text_clear_updates_feed_prompt">Tühjenda kõik uuenduste ajalugu?</string>
<string name="source_disabled">Allikas välja lülitatud</string>
<string name="clear_cookies_summary">Võib aidata mõndade probleemidega. Kõik autoriseerimine saab invalideeritud</string>
<string name="enable_logging_summary">Salvesta mõned tegevused silumiseks. Ärge lülitage seda sisse, kui te ei ole kindel, mida teete</string>
<string name="suggestions_enable">Lülita sisse soovitused</string>
<string name="clear_feed">Tühjenda voog</string>
<string name="welcome">Tere tulemast</string>
<string name="no_description">Kirjeldus puudub</string>
<string name="about_app_translation_summary">Tõlgi see äppi</string>
<string name="text_shelf_holder_secondary">Leia mida lugeda «Seiklemis» osakonnas</string>
<string name="chapters_empty">Selles mangas ei ole peatükke</string>
<string name="remove">Eemalda</string>
<string name="vibration">Värisemine</string>
<string name="no_update_available">Uuendusi pole saadaval</string>
<string name="remove_category">Eemalda</string>
<string name="clear_all_history">Tühjenda kõik ajalugu</string>
<string name="preload_pages">Lehekülgede eellaadimine</string>
<string name="data_deletion">Andmete kustutamine</string>
<string name="favourites">Lemmikud</string>
<string name="history_shortcuts">Näita hiljutise manga lühitee</string>
<string name="downloads_wifi_only_summary">Peata allalaadimised kui vahetad mobiilsele ühendusele</string>
<string name="read_mode">Lugemis mood</string>
<string name="internal_storage">Sisemine mälu</string>
<string name="import_completed">Importitud</string>
<string name="different_languages">Erinevad keeled</string>
<string name="show_reading_indicators">Näita lugemis progressi näitaja</string>
<string name="read_later">Loe hiljem</string>
<string name="backup_saved">Tagavara salvestatud</string>
<string name="local_manga_processing">Töötlen salvestatud manga</string>
<string name="user_agent">UserAgent pealkiri</string>
<string name="cannot_find_available_storage">Puudub olemasolevat salvestamisruumi</string>
<string name="create_backup">Tee andmete tagavara</string>
<string name="detailed_list">Detailitud loetelu</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">sisse lülitatud %1$d / %2$d</string>
<string name="tap_to_try_again">Vajuta, et uuesti proovida</string>
<string name="ignore_ssl_errors">Ignoreeri SSL vigasi</string>
<string name="auth_required">Selle sisu vaatamiseks logi sisse</string>
<string name="search_manga">Otsi mangat</string>
<string name="next">Järgmine</string>
<string name="restore_backup">Taasta andmete tagavarast</string>
<string name="reader_info_bar">Näita inforiba lugejas</string>
<string name="password_length_hint">Parool peab olema 4 tähemärki või veel</string>
<string name="server_address">Sereri aadress</string>
<string name="text_feed_holder">Uued peatükid mangast mis sa loed näidatakse siin</string>
<string name="text_suggestion_holder">Hakka lugema mangat ja sa saad isikupärastatud soovitusi</string>
<string name="explore">Seikle</string>
<string name="find_similar">Leija samasuguseid</string>
<string name="show_notification_new_chapters_off">Sa ei saa teateid uuenduste kohta, aga uued peatükkid on esiletoodud nimekirjas</string>
<string name="storage_usage">Ruumi kasutatud</string>
<string name="data_restored">Taastatud</string>
<string name="favourites_category_empty">Tühi kategooria</string>
<string name="protect_application_subtitle">Sisesta parool millega startida äppi</string>
<string name="remote_sources">Manga allikad</string>
<string name="theme_name_sakura">Sakura</string>
<string name="clear">Tühjenda</string>
<string name="view_list">Vaata nimekirja</string>
<string name="by_rating">Hinnang</string>
<string name="unknown">Mitte teatud</string>
<string name="newest">Uusimad</string>
<string name="suggestions">Soovitused</string>
<string name="sort_order">Sorteerimisjärjekord</string>
<string name="clear_history">Kustuta ajalugu</string>
<string name="show_notification_new_chapters_on">Sa saad manga kohta mis sa loed teateid uuenduste kohta</string>
<string name="importing_manga">Impordi manga</string>
<string name="enabled">Sisse lülitatud</string>
<string name="pause">Paus</string>
<string name="clear_new_chapters_counters">Ja tühjenda informatsioon uute peatükkide kohta</string>
<string name="text_clear_search_history_prompt">Eemaldada kõik hiljutised otsingupäringud?</string>
<string name="nothing_here">Midagi pole siin</string>
<string name="remove_completed">Eemalda lõpetatud</string>
<string name="manga_save_location">Kaust allalaadimiseks</string>
<string name="status_reading">Loen</string>
<string name="auth_complete">Lubatud</string>
<string name="loading_">Laadimine…</string>
<string name="suggestions_notifications_summary">Vahepeal näita teateid koos soovitatud mangaga</string>
<string name="updates_feed_cleared">Tühjendatud</string>
<string name="webtoon_zoom">Webtooni zoom</string>
<string name="various_languages">Erinevad keeled</string>
<string name="list_mode">Loetelu mood</string>
<string name="removal_completed">Eemalda lõppetatud</string>
<string name="download_complete">Alla laetud</string>
<string name="theme_name_miku">Miku</string>
<string name="edit">Muuda</string>
<string name="captcha_required">CAPTCHA vajatud</string>
<string name="filter_load_error">Ei saa laadida žanrite nimekirja</string>
<string name="popular">Populaarsed</string>
<string name="manage_categories">Halda kategoorjaid</string>
<string name="update">Uuenda</string>
<string name="scrobbling_empty_hint">Lugemise edenemise jälgimiseks valige manga üksikasjade ekraanil menüü → Jälgimine.</string>
<string name="removed_from_history">Eemaldatud ajaloost</string>
<string name="crash_text">Midagi läks valesti. Palun esitage arendajatele veateade, et aidata meil seda parandada.</string>
<string name="theme">Teema</string>
<string name="detect_reader_mode_summary">Automaatselt tuvastada, kas manga on webtoon</string>
<string name="color_correction_hint">Valitud värvi seaded jäetakse meelde selle manga jaoks</string>
<string name="not_found_404">Sisu mitte saadaval või eemaldatud</string>
<string name="feed_will_update_soon">Voogu uuendused algavad varsti</string>
<string name="appwidget_recent_description">Sinu hiljuti loetud manga</string>
<string name="got_it">Saadud</string>
<string name="chapters">Peatükid</string>
<string name="appearance">Välimus</string>
<string name="bookmark_remove">Eemalda järjehoidja</string>
<string name="search_hint">Sisestage manga pealkiri, žanr või allika nimi</string>
<string name="disable_battery_optimization_summary">Aitab tausta uuenduste kontrollimisega</string>
<string name="downloads">Alla laadimised</string>
<string name="auth_not_supported_by">Sisselogimine %s ei ole toetatud</string>
<string name="app_update_available">Uus äppi versioon on olemas</string>
<string name="status_on_hold">Ootel</string>
<string name="last_2_hours">Viimased 2 tundi</string>
<string name="name">Nimi</string>
<string name="sources_reorder_tip">Vajuta ja hoija asjal, et uuesti reastada neid</string>
<string name="edit_category">Muuda kategooriat</string>
<string name="resume">Jätka</string>
<string name="server_error">Serveri poolene viga (%1$d). Palun proovi uuessti hiljem</string>
<string name="bookmark_removed">Järjehoidja eemaldatud</string>
<string name="network_unavailable_hint">Lülita sisse Wi-Fi või mobile data, et lugeda mangat online</string>
<string name="you_have_not_favourites_yet">Pole lemmikuid veel</string>
<string name="select_range">Valige vahemik</string>
<string name="tracker_warning">Mõnedel seadmetel on erinev süsteemikäitumine, mis võib katkestada taustatööd.</string>
<string name="network_error">Võrguviga</string>
<string name="new_version_s">Uus versioon: %s</string>
<string name="no_manga_sources">Pole manga allikaid</string>
<string name="light">Valge</string>
<string name="suggestions_excluded_genres_summary">Täpsusta žanrid, mida te ei soovi ettepanekutes näha</string>
<string name="text_delete_local_manga">Kustuta \"%s\" seadelt?</string>
<string name="prefetch_content">Sisu eellaeb</string>
<string name="confirm_exit">Vajuta Tagasi, et lahkuda</string>
<string name="scale_mode">skaala mood</string>
<string name="advanced">Täiustatud</string>
<string name="only_using_wifi">Ainult Wi-Fi-l</string>
<string name="automatic">Jälgne süsteemile</string>
<string name="sync_settings">Sünkroniseeri seadeid</string>
<string name="black_dark_theme">Must</string>
<string name="text_history_holder_primary">Mis sa loed näidatakse siin</string>
<string name="back">Must</string>
<string name="delete_manga">Kustuta manga</string>
<string name="create_category">Uus kategooria</string>
<string name="delete">Kustuta</string>
<string name="notification_sound">Teate hääl</string>
<string name="screenshots_allow">Luba</string>
<string name="backup_restore">Varundamine ja taastamine</string>
<string name="other_cache">Teised vahemälud</string>
<string name="dns_over_https">DNS üle HTTPS</string>
<string name="show_suspicious_content">Näita kahtlast sisu</string>
<string name="show_pages_numbers">nummerda leheküljed</string>
<string name="allow_unstable_updates_summary">Saa teateid ebastabiilsete uuenduste kohta</string>
<string name="sync_title">Sünkroniseeri oma andmed</string>
<string name="appwidget_shelf_description">Manga sinu lemmikutest</string>
<string name="comics_archive_import_description">Sa võid valida üks või veel .cbz või .zip faile, iga fail võetakse kui erinev manga.</string>
<string name="search_history_cleared">Tühjendatud</string>
<string name="_s_deleted_from_local_storage">\"%s\" eemaldatud lokaasest salvestusruumist</string>
<string name="too_many_requests_message">Liiga palju taotlusi. Proovige hiljem uuesti</string>
<string name="downloads_wifi_only">Allalaadi ainult üle Wi-Fi</string>
<string name="send">Saada</string>
<string name="bookmark_add">Lisa järjehoidja</string>
<string name="discard">Unusta</string>
<string name="saved_manga">Salvestatud manga</string>
<string name="manga_downloading_">Laadin alla…</string>
<string name="screenshots_block_all">Alati blokeeri</string>
<string name="new_sources_text">Uued manga allikad on saadaval</string>
<string name="manga_error_description_pattern">Vea detailid:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Proovi &lt;a href=%2$s&gt;avada mangat oma browseris&lt;/a&gt; et olla kindel et see manga on saadaval allikast &lt;br&gt;2. tee kindlaks et sa kasutad &lt;a href=kotatsu://about&gt;kõige uuemat versiooni Kotatsut&lt;/a&gt;&lt;br&gt;3. Kui see on saadaval, saada vea raport arendajatele.</string>
<string name="open_in_browser">Ava veebibrauser</string>
<string name="about_app_translation">Tõlge</string>
<string name="zoom_mode_fit_height">Sobita kõrgusele</string>
<string name="notifications">Teated</string>
<string name="not_available">Mitte saadaval</string>
<string name="check_new_chapters_title">Otsi uusi peatükke ja tevita sellest</string>
<string name="history_shortcuts_summary">Tee hiljutine manga saadaval hoides pikkalt applikatsiooni ikoonil</string>
<string name="automatic_scroll">Automaatiline scroll</string>
<string name="reverse">Tagurpidi</string>
<string name="track_sources">Otsi uusi uuendusi</string>
<string name="paused">Pausis</string>
<string name="clear_search_history">Tühjenda otsingute ajalugu</string>
<string name="wrong_password">Vale parool</string>
<string name="group">Grupp</string>
<string name="_import">Impordi</string>
<string name="color_correction">Värvi korektsioon</string>
<string name="just_now">Just nüüd</string>
<string name="text_file_not_supported">Vali kas ZIP või CBZ fail.</string>
<string name="download">Lae alla</string>
<string name="chapter_is_missing">See peatükk puudub</string>
<string name="categories_delete_confirm">Kas sa oled kindel, et tahad kustutada valitud lemmikud kategooriad?
\nKõik manga nende sees kaob ja seda tagasi ei saa.</string>
<string name="logged_in_as">Logitud sisse kui %s</string>
<string name="suggestions_info">Kõik andmed analüüsitakse ainult lokaalselt selles seadmes ja neid ei saadeta kunagi kuhugi.</string>
<string name="reader_info_pattern">Pt. %1$d/%2$d Lk. %3$d/%4$d</string>
<string name="contrast">Kontrast</string>
<string name="history_cleared">Ajalugu tühjendatud</string>
<string name="size_s">Suurus: %s</string>
<string name="reader_slider">Näita lehekülje vahetamise liugurit</string>
<string name="no_manga_sources_text">Lülita sisse manga allikaid, et lugeda mangat online</string>
<string name="about">Kohta</string>
<string name="undo">Tühista</string>
<string name="zoom_mode_fit_center">Sobita keskele</string>
<string name="options">Valikud</string>
<string name="services">Teenused</string>
<string name="exclude_nsfw_from_history">Jäta NSFW manga ajaloost välja</string>
<string name="clear_pages_cache">Tühjenda lehekülje vahemälu</string>
<string name="download_slowdown_summary">Aitab vältida teie IP-aadressi blokeerimist</string>
<string name="check_for_new_chapters">Otsi uusi peatükke</string>
<string name="read">Loe</string>
<string name="text_delete_local_manga_batch">Kustuta valitud asjad seadmelt?</string>
<string name="queued">Järjekorras</string>
<string name="text_clear_history_prompt">Tühjenda kõik lugemis ajalugu?</string>
<string name="text_shelf_holder_primary">Sinu mangat näidatakse siin</string>
<string name="report">Raporteeri</string>
<string name="captcha_solve">Lahenda</string>
<string name="download_slowdown">Aeglusta alla laadimised</string>
<string name="bookmark_added">Järjehoidja lisatud</string>
<string name="data_restored_with_errors">Andmed on taastatud, aga on vigasi</string>
<string name="details">Detailid</string>
<string name="sync">Sünkronisatsioon</string>
<string name="new_chapters">Uued peatükid</string>
<string name="search_chapters">Leia peatükk</string>
<string name="exit_confirmation">Lahkumise kinnitus</string>
<string name="comics_archive">Koomikute arhiiv</string>
<string name="more">Rohkem</string>
<string name="theme_name_asuka">Asuka</string>
<string name="history_is_empty">Pole mingit ajalugu veel</string>
<string name="always">Alati</string>
<string name="import_will_start_soon">Import algab varsti</string>
<string name="try_again">Proovi uuesti</string>
<string name="compact">Kompaktne</string>
<string name="protect_application">Kaitse seda äppi</string>
<string name="folder_with_images_import_description">Sa võid valida kausta arhiivi või piltidega. Iga arhiiv (või alamkaust) võetakse kui erinev peatükk.</string>
<string name="reorder">ümberjärjestamine</string>
<string name="share_s">Jaga %s</string>
<string name="suggestions_excluded_genres">Välista žanrid</string>
<string name="chapter_d_of_d">Peatükid %1$d %2$d-st</string>
<string name="canceled">Peatatud</string>
<string name="feed">Voog</string>
<string name="account_already_exists">Kasutaja juba eksisteerib</string>
<string name="dark">Must</string>
<string name="hide">Peida</string>
<string name="passwords_mismatch">Ebavõrdsed paroolid</string>
<string name="by_name">Nimi</string>
<string name="yesterday">Eile</string>
<string name="check_for_updates">Otsi uuendusi</string>
<string name="exclude_nsfw_from_history_summary">Manga märkitud kui NSFW kungai ei lisata sinu ajalukku ja progressi ei salvestata</string>
<string name="mark_as_current">Margi kui präegune</string>
<string name="protect_application_summary">Kusi parooli avades Kotatsut</string>
<string name="right_to_left">Paremalt-vasakule</string>
<string name="show_reading_indicators_summary">Näita mitu protsenti on loetud ajaloos ja lemmikutes</string>
<string name="random">Suvakas</string>
<string name="mirror_switching">Vali peegeldus automaatselt</string>
<string name="use_fingerprint">Kasuta näppujälge, kui see on olemas</string>
<string name="standard">Standard</string>
<string name="history">Ajalugu</string>
<string name="nothing_found">Mitte midagi leitud</string>
<string name="onboard_text">Väli keeled milles sa tahad mangat lugeda. Seda võib hiljem seadetes muuta.</string>
<string name="show_in_grid_view">Näita ruudustiku vaates</string>
<string name="reader_mode_hint">Valitud konfiguratsioon jäetakse meelde selle manga jaoks</string>
<string name="default_s">Default: %s</string>
<string name="volume_buttons">Helitugevusnupud</string>
<string name="confirm">Kinnita</string>
<string name="suggestions_updating">Soovitused uuendavad</string>
<string name="add_new_category">Uus kategooria</string>
<string name="clear_updates_feed">Tühjenda uuenduste voog</string>
<string name="enable">Lülita sisse</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="chapters_will_removed_background">Peatükid eemaldatakse taustal</string>
<string name="import_completed_hint">Ruumi säästmiseks saad originaalfaili salvestusruumist kustutada</string>
<string name="theme_name_rikka">Rikka</string>
<string name="share_image">Jaga pilt</string>
<string name="disabled">Välja lülitatud</string>
<string name="long_ago">Kaua aega tagasi</string>
<string name="reader_control_ltr_summary">Vajutades paremal äärele või paremale nuppule alati vahetub järgmisele leheküljele</string>
<string name="list">Loetelu</string>
<string name="notifications_settings">Teate seaded</string>
<string name="language">Keel</string>
<string name="domain">Domeen</string>
<string name="save_manga">Salvesta</string>
<string name="incognito_mode">Inkognito mood</string>
<string name="no_bookmarks_summary">Sa võid teha järjehoidjaid mangat lugedes</string>
<string name="large_manga_save_confirm">Sellel mangal on %s. Salvesta kõik?</string>
<string name="search">Otsi</string>
<string name="default_mode">Tava mood</string>
<string name="reader_settings">Lugeja seaded</string>
<string name="theme_name_mamimi">Mamimi</string>
<string name="text_search_holder_secondary">Proovi otsingut teiste sõnadega.</string>
<string name="error">Viga</string>
<string name="grid_size">Ruudustiku suurus</string>
<string name="manage">Halda</string>
<string name="logout">Logi välja</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="status_completed">Lõpetatud</string>
<string name="_continue">Jätka</string>
<string name="grid">Ruudustik</string>
<string name="recent_manga">Hiljutised</string>
<string name="operation_not_supported">See operatsioon ei ole toetatud</string>
<string name="reader_control_ltr">Ergonoomiline lügeja kontroll</string>
<string name="dont_check">Ära kontrolli</string>
<string name="save_page">Salvesta lehekülg</string>
<string name="zoom_mode_fit_width">Sobita laijusele</string>
<string name="read_more">Loe veel</string>
<string name="reset_filter">Nulli filter</string>
<string name="search_on_s">Otsi %s-st</string>
<string name="status_dropped">Kukkutatud</string>
<string name="nsfw">18+</string>
<string name="disable_nsfw">Lülita välja NSFW</string>
<string name="processing_">Töötlen…</string>
<string name="black_dark_theme_summary">Kasuta vähem elektrit AMOLED ekraanidel</string>
<string name="available">Saadaval</string>
<string name="notifications_enable">Lülita sisse notifikatsioonid</string>
<string name="network_unavailable">Võrk ei ole saadaval</string>
<string name="search_results">Otsingutulemused</string>
<string name="empty">Tühi</string>
<string name="webtoon">Webtoon</string>
<string name="exclude_nsfw_from_suggestions">Ära soovita NSFW mangat</string>
<string name="file_not_found">Fail mitte leitud</string>
<string name="never">Mitte kungai</string>
<string name="text_unsaved_changes_prompt">Salvesta või unusta mitte salvestatud muutused?</string>
<string name="settings">Seaded</string>
<string name="folder_with_images">Kaustad piltitega</string>
<string name="pages">Leheküljed</string>
<string name="app_version">Versioon %s</string>
<string name="cookies_cleared">Kõik küpsised on eemaldatud</string>
<string name="disable_battery_optimization">Lülita välja aku optimeerimine</string>
<string name="status_planned">Planeeritud</string>
<string name="pages_animation">Lehekülje animatsioon</string>
<string name="state_finished">Lõppetatud</string>
<string name="state_ongoing">Käimasolev</string>
<string name="suggestions_summary">Soovita manga vastavalt oma eelistustele</string>
<string name="show">Näita</string>
<string name="genres">Žanrid</string>
<string name="show_on_shelf">Näita riiulil</string>
<string name="allow_unstable_updates">Luba ebastabiilseid uuendusi</string>
<string name="sync_auth_hint">Sa võid sisse logida olemasolevasse kasutajasse või teha uue</string>
<string name="create_shortcut">Tee otsetee…</string>
<string name="theme_name_kanade">Kanade</string>
<string name="other_storage">Teine salvestamisruum</string>
<string name="updated">Uuendatud</string>
<string name="color_theme">Värvi skeema</string>
<string name="brightness">Helendus</string>
<string name="memory_usage_pattern">%s - %s</string>
<string name="preparing_">Valmistan…</string>
<string name="share">Jaga</string>
<string name="computing_">Arvutan…</string>
<string name="add">Lisa</string>
<string name="exit_confirmation_summary">Vajuta Tagasi kaks korda, et lahkuda äppist</string>
<string name="bookmarks_removed">Järjehoidjad eemaldatud</string>
<string name="screenshots_block_nsfw">Blokeeri kui NSFW</string>
<string name="enter_password">Sisesta parool</string>
<string name="repeat_password">Korda parooli</string>
<string name="data_restored_success">Kõik andmed on taastatud</string>
<string name="theme_name_mion">Mion</string>
<string name="mirror_switching_summary">Automaatselt vaheta domeene manga allika jaoks veaga kui peegeldused on saadaval</string>
<string name="no_bookmarks_yet">Pole järjehoidjaid veel</string>
<string name="no_thanks">Tänan ei</string>
<string name="backup_information">Sa saad teha tagavara enda ajaloost, lemmikutest ja siis taastada need</string>
<string name="available_sources">Kättesaadavad allikad</string>
<string name="share_logs">Jaga looge</string>
<string name="zoom_mode_keep_start">Hoija stardis</string>
<string name="external_storage">Väline mälu</string>
<string name="text_local_holder_secondary">Salvesta see interneti allikast või impordidtud failist.</string>
<string name="speed">Kiirus</string>
<string name="silent">Vaikne</string>
<string name="text_local_holder_primary">Salvesta midagi esimesena</string>
<string name="suggestion_manga">Soovitused: %s</string>
<string name="removed_from_favourites">Eemaldatud lemmikutest</string>
<string name="bookmarks">Järjehoidja</string>
<string name="show_all">Näita kõik</string>
<string name="page_saved">Salvestatud</string>
<string name="today">Täna</string>
<string name="empty_favourite_categories">Pole lemmikkategooriaid</string>
<string name="invalid_domain_message">Kehtetu domeen</string>
<string name="system_default">Tava</string>
<string name="text_empty_holder_primary">On üsna tühi siin…</string>
<string name="screenshots_policy">Ekraanipiltide poliitika</string>
<string name="done">Tehtud</string>
<string name="error_no_space_left">Ruumi pole ülejäänud seadmel</string>
<string name="sign_in">Logi sisse</string>
</resources>

View File

@@ -150,7 +150,7 @@
<string name="favourites_categories">Mga paboritong kategorya</string>
<string name="remove_category">Tanggalin</string>
<string name="text_empty_holder_primary">Parang walang laman dito…</string>
<string name="text_history_holder_secondary">Hanapin kung ano ang babasahin sa side menu.</string>
<string name="text_history_holder_secondary">Hanapin kung anong pwedeng basahin sa «Explore» section</string>
<string name="text_shelf_holder_secondary">Hanapin kung ano ang babasahin sa seksyong «Mag-explore»</string>
<string name="recent_manga">Kamakailan</string>
<string name="manga_save_location">Folder para sa mga download</string>
@@ -267,7 +267,7 @@
<string name="incognito_mode">Incognito mode</string>
<string name="no_chapters">Walang mga kabanata</string>
<string name="automatic_scroll">Awtomatikong pag-scroll</string>
<string name="reader_info_pattern">Ch. %1$d/%2$d Pg. %3$d/%4$d</string>
<string name="reader_info_pattern">Ka. %1$d/%2$d Pp. %3$d/%4$d</string>
<string name="reader_info_bar">Ipakita ang information bar sa pagbasa</string>
<string name="comics_archive">Archive ng mga comics</string>
<string name="folder_with_images">Folder na may mga larawan</string>
@@ -303,8 +303,8 @@
<string name="nothing_here">Wala naman dito</string>
<string name="services">Mga serbisyo</string>
<string name="allow_unstable_updates">Payagan ang mga hindi stable na update</string>
<string name="show_reading_indicators">Ipakita ang mga tagapagpahiwatig ng progress ng pagbabasa</string>
<string name="exclude_nsfw_from_history_summary">Manga na minarkahan bilang NSFW ay hindi kailanman idadagdag sa kasaysayan at ang iyong progress ay hindi mase-save</string>
<string name="show_reading_indicators">Ipakita ang pang-hiwatig ng progreso</string>
<string name="exclude_nsfw_from_history_summary">Ang mga Manga na naka-mark na NSFW ay hindi madadagdag sa history at yung progreso mo ay hindi ma-sasave</string>
<string name="clear_cookies_summary">Maaaring makatulong sa kaso ng ilang mga isyu. Ang lahat ng pahintulot ay mawawalan ng bisa</string>
<string name="invalid_domain_message">Imbalidong domain</string>
<string name="last_2_hours">Huling 2 oras</string>
@@ -313,7 +313,7 @@
<string name="no_bookmarks_yet">Wala pang bookmark</string>
<string name="nsfw">18+</string>
<string name="not_found_404">Hindi natagpuan o inalis ang nilalaman</string>
<string name="enable_logging_summary">Magtala ng ilang pagkilos para sa mga layunin ng pag-debug</string>
<string name="enable_logging_summary">Magtala ng ilang pagkilos para sa mga layunin ng pag-debug. Huwag buksan kung hindi mo alam kung ano ang ginagawa mo</string>
<string name="text_delete_local_manga_batch">Permanenteng tanggalin ang mga napiling item sa device\?</string>
<string name="chapters_empty">Walang mga kabanata sa manga na ito</string>
<string name="suggestions_updating">Nag-a-update ang mga mungkahi</string>
@@ -343,7 +343,7 @@
<string name="use_fingerprint">Gumamit ng fingerprint kung magagamit</string>
<string name="report">Ulat</string>
<string name="reset">I-reset</string>
<string name="allow_unstable_updates_summary">Magmungkahi ng mga update sa mga beta na bersyon ng app</string>
<string name="allow_unstable_updates_summary">Makakuha ng paunawa tungkol sa mga unstable build</string>
<string name="network_unavailable">Hindi magagamit ang network</string>
<string name="network_unavailable_hint">I-on ang Wi-Fi o mobile network para magbasa ng manga online</string>
<string name="reader_control_ltr_summary">Mag-tap sa kanang gilid o ang pagpindot sa kanang key ay palaging lilipat sa susunod na pahina</string>
@@ -361,16 +361,16 @@
<string name="theme_name_miku">Miku</string>
<string name="user_agent">Header ng UserAgent</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">Standard</string>
<string name="standard">Pamantayan</string>
<string name="webtoon">Webtoon</string>
<string name="read_mode">Read mode</string>
<string name="error">Error</string>
<string name="internal_storage">Internal storage</string>
<string name="external_storage">External storage</string>
<string name="read_mode">Mode na pang-basa</string>
<string name="error">May Mali</string>
<string name="internal_storage">Pangkaloobang storage</string>
<string name="external_storage">Panlabasang storage</string>
<string name="vibration">Vibration</string>
<string name="domain">Domain</string>
<string name="light_indicator">LED indicator</string>
<string name="settings_apply_restart_required">Mangyaring i-restart ang application upang ilapat ang mga pagbabagong ito</string>
<string name="settings_apply_restart_required">I-restart ang application para ma-ilapat ang mga pagbabagong ito</string>
<string name="got_it">Nakuha ko</string>
<string name="sources_reorder_tip">I-tap at hawakan ang isang aytem upang muling ayusin ang mga ito</string>
<string name="restore_backup_description">Mag-import ng dating ginawa na backup ng data ng user</string>
@@ -389,7 +389,7 @@
<string name="sync_host_description">Maaari kang gumamit ng self-hosted synchronization server o isang default. Huwag baguhin ito kung hindi ka sigurado sa iyong ginagawa.</string>
<string name="ignore_ssl_errors">Huwag pansinin ang mga error sa SSL</string>
<string name="mirror_switching">Awtomatikong pumili ng mirror</string>
<string name="mirror_switching_summary">Awtomatikong lumipat ng mga domain para sa mga remote source sa mga error kung available ang mga mirror</string>
<string name="mirror_switching_summary">Kusang lumipat ng domain para sa mga manga source na may mali kung mayroong mga mirror</string>
<string name="cancel_all">Kanselahin lahat</string>
<string name="downloads_wifi_only">Mag-download lamang sa pamamagitan ng Wi-Fi</string>
<string name="downloads_wifi_only_summary">Itigil ang pag-download kapag lumipat sa isang mobile network</string>
@@ -423,7 +423,7 @@
<string name="data_and_privacy">Data at privacy</string>
<string name="restore_summary">Ibalik ang dating ginawang backup</string>
<string name="webtoon_zoom_summary">Payagan ang pag-zoom in na gesture sa webtoon mode</string>
<string name="reader_info_bar_summary">Ipakita ang kasalukuyang oras at pagbabasa ng progreso sa tuktok ng screen</string>
<string name="reader_info_bar_summary">Ipakita ang kasalukuyang oras at progreso ng pagbabasa sa tuktok ng screen</string>
<string name="show_pages_numbers_summary">Ipakita ang mga numero ng pahina sa ibabang gilid</string>
<string name="pages_animation_summary">I-animate ang paglipat ng pahina</string>
<string name="invalid_port_number">Imbalidong numero ng port</string>
@@ -437,8 +437,8 @@
<string name="download_option_all_unread_b">Lahat ng di-nababasang kabanata (%s)</string>
<string name="download_option_manual_selection">Manu-manong pumili ng mga kabanata</string>
<string name="invert_colors">Baliktarin ang mga kulay</string>
<string name="custom_directory">Pasadyang direktoryo</string>
<string name="pick_custom_directory">Pumili ng pasadyang direktoryo</string>
<string name="custom_directory">Custom na direktoryo</string>
<string name="pick_custom_directory">Pumili ng Custom na direktoryo</string>
<string name="no_access_to_file">Wala kang access sa file o direktoryo na ito</string>
<string name="local_manga_directories">Mga lokal na direktoryo ng manga</string>
<string name="password">Password</string>
@@ -464,17 +464,17 @@
<string name="manage_categories">Ayusin ang mga kategorya</string>
<string name="suggestions_wifi_only_summary">Huwag i-update ang mga mungkahi gamit ang mga naka-metrong network na koneksyon</string>
<string name="show">Ipakita</string>
<string name="unknown">Di-alam</string>
<string name="unknown">Di-kilala</string>
<string name="in_progress">Isinasagawa</string>
<string name="disable_nsfw">I-disable ang NSFW</string>
<string name="error_corrupted_file">Ang hindi wastong data ay ibinalik nito o ang file ay nasira</string>
<string name="error_corrupted_file">Nabalik ang di-wastong data o na-corrupt ang file</string>
<string name="related_manga_summary">Magpakita ng listahan ng mga kaugnay na manga. Sa ilang mga kaso, ito ay maaaring may mali o nawawala</string>
<string name="advanced">Advanced</string>
<string name="too_many_requests_message">Masyadong maraming request. Subukang ulit mamaya</string>
<string name="default_section">Default na seksyon</string>
<string name="manga_list">Listahan ng Manga</string>
<string name="zoom_in">Mag-zoom paloob</string>
<string name="reader_zoom_buttons_summary">Kung magpapakita ng mga button ng kontrol ng zoom sa kanang sulok sa ibaba</string>
<string name="reader_zoom_buttons_summary">Kung magpapakita ba ng mga button ng pang-zoom sa kanang sulok sa ibaba</string>
<string name="on_device">Sa device</string>
<string name="moved_to_top">Nailipat sa itaas</string>
<string name="items_limit_exceeded">Wala nang mga aytem na pwedeng idagdag</string>
@@ -487,13 +487,13 @@
<string name="enhanced_colors_summary">Binabawasan ang banding, ngunit maaaring makaapekto ito sa performance</string>
<string name="state_abandoned">Na-drop</string>
<string name="keep_screen_on">Panatilihing naka-on ang screen</string>
<string name="enhanced_colors">32-bit color mode</string>
<string name="enhanced_colors">32-bit na color mode</string>
<string name="keep_screen_on_summary">Huwag I-off ang screen habang nagbabasa ng manga</string>
<string name="suggest_new_sources_summary">I-prompt na paganahin ang mga bagong idinagdag na source pagkatapos i-update ang aplikasyon</string>
<string name="categories">Mga Kategorya</string>
<string name="list_options">Opsyon sa Listahan</string>
<string name="by_relevance">Kaugnayan</string>
<string name="online_variant">Online na baryante</string>
<string name="online_variant">Online na variant</string>
<string name="frequency_every_day">Araw araw</string>
<string name="backup_frequency">Dalas ng paglikha ng backup</string>
<string name="periodic_backups_enable">Paganahin ang periodic na pag-backup</string>
@@ -517,7 +517,8 @@
<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="no_manga_sources_catalog_text">Walang mga sources na maaring gamitin sa seksyong ito, o maaring na-add na lahat.
\nManatiling nakatutok para sa iba pang mga source</string>
<string name="available_d">Available: %1$d</string>
<string name="content_type_other">Iba pa</string>
<string name="error_multiple_states_not_supported">Ang pag-filter ayon sa maraming estado ay hindi sinusuportahan ng manga source na ito</string>
@@ -532,9 +533,14 @@
<string name="apply">Gamitin</string>
<string name="genres_search_hint">Simulan ang pag-type ng pangalan ng genre</string>
<string name="globally">Pangkahalatan</string>
<string name="downloads_settings_info">Maaari mong paganahin ang pabagal na pag-download para sa bawat source ng manga nang paisa-isa sa mga setting ng source kung nagkakaroon ka ng mga problema sa pagharang sa server-side</string>
<string name="downloads_settings_info">Maaari mong paganahin ang pag-bagal ng pag-download para sa bawat source ng manga nang paisa-isa sa mga setting ng source kung nagkakaroon ka ng mga problema sa pagharang na server-side</string>
<string name="this_manga">Itong manga na ito</string>
<string name="skip">Laktawan</string>
<string name="color_correction_apply_text">Ang mga setting na ito ay maaaring ilapat sa pangkahalatan o sa kasalukuyang manga lamang. Kung inilapat sa pangkahalatan, hindi ma-override ang mga indibidwal na setting.</string>
<string name="grayscale">Grayscale</string>
<string name="grayscale">Walang kulay</string>
<string name="welcome_text">Paki-select kung anong mga content source ang gusto mo i-enable. Pwede rin ito i-configure mamaya sa settings</string>
<string name="restore">I-balik</string>
<string name="disable_battery_optimization_summary_downloads">Baka makatulong sa pag-simula ng download kung mayroong isyu</string>
<string name="backup_date_">Petsa ng pag-backup: %s</string>
<string name="sync_auth">Mag-login para i-sync ang account</string>
</resources>

View File

@@ -268,7 +268,7 @@
<string name="show_reading_indicators">Mostrare gli indicatori di progresso della lettura</string>
<string name="data_deletion">Eliminazione dei dati</string>
<string name="show_reading_indicators_summary">Mostra la percentuale di lettura nella cronologia e nei preferiti</string>
<string name="exclude_nsfw_from_history_summary">I manga contrassegnati come per adulti non verranno mai aggiunti alla cronologia e i vostri progressi non verranno salvati</string>
<string name="exclude_nsfw_from_history_summary">I manga contrassegnati come per adulti non verranno mai aggiunti alla cronologia e i progressi non saranno salvati</string>
<string name="clear_cookies_summary">Può aiutare in caso di problemi. Tutte le autorizzazioni saranno invalidate</string>
<string name="show_all">Mostra tutto</string>
<string name="logout">Esci</string>
@@ -345,12 +345,12 @@
\n Tutti i manga in esso contenuti andranno persi e questo non può essere annullato.</string>
<string name="history_shortcuts_summary">Rendere disponibili i manga recenti premendo a lungo sull\'icona dell\'applicazione</string>
<string name="reader_info_bar">Mostra la barra delle informazioni nel lettore</string>
<string name="manga_error_description_pattern">Dettagli dell\'errore:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;1. Prova ad &lt;a href=%2$s&gt;aprire il manga in un browser web&lt;/a&gt; per assicurarsi che sia disponibile sulla sua fonte&lt;br&gt;2. Se è disponibile, inviare una segnalazione di errore agli sviluppatori.</string>
<string name="manga_error_description_pattern">Dettagli dell\'errore:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Prova ad &lt;a href=%2$s&gt;aprire il manga in un browser web&lt;/a&gt; per assicurarsi che sia disponibile sulla sua fonte&lt;br&gt;2. Controllare di stare usando la &lt;a href=kotatsu://about&gt;versione più recente di Kotatsu&lt;/a&gt;&lt;br&gt;3. Se è disponibile, inviare una segnalazione di errore agli sviluppatori.</string>
<string name="text_unsaved_changes_prompt">Salvare o eliminare le modifiche non salvate\?</string>
<string name="discard">Abbandona</string>
<string name="language">Lingua</string>
<string name="share_logs">Condividi i registri</string>
<string name="enable_logging_summary">Registra alcune azioni a scopo di debug</string>
<string name="enable_logging_summary">Registra alcune azioni a scopo di debug. Non attivare se non si è sicuri di cosa si stia facendo</string>
<string name="enable_logging">Abilita la registrazione</string>
<string name="show_suspicious_content">Mostra il contenuto sospetto</string>
<string name="theme_name_dynamic">Dinamico</string>
@@ -366,7 +366,181 @@
<string name="theme_name_rikka">Rikka</string>
<string name="theme_name_miku">Miku</string>
<string name="allow_unstable_updates">Permetti aggiornamenti instabili</string>
<string name="allow_unstable_updates_summary">Proponi aggiornamenti alle versioni beta dell\'app</string>
<string name="allow_unstable_updates_summary">Ricevi notifiche riguardo versioni instabili</string>
<string name="download_started">Scaricamento iniziato</string>
<string name="theme_name_sakura">Sakura</string>
<string name="languages">Lingue</string>
<string name="zoom_in">Ingrandire</string>
<string name="captcha_required_summary">%s richiede il completamento di un captcha per funzionare correttamente</string>
<string name="sources_catalog">Catalogo fonti</string>
<string name="download_option_all_unread">Tutti i capitoli non letti</string>
<string name="frequency_every_day">Ogni giorno</string>
<string name="categories">Categorie</string>
<string name="progress">Progresso</string>
<string name="cancel_all">Annulla tutti</string>
<string name="sync_host_description">Puoi usare un server di sincronizzazione self-hosted oppure uno di default. Non cambiare se non si è sicuri di cosa si stia facendo.</string>
<string name="error_corrupted_file">Dati non validi in ritorno o file corrotto</string>
<string name="pick_custom_directory">Seleziona cartella personalizzata</string>
<string name="list_options">Mostra lista opzioni</string>
<string name="related_manga_summary">Mostra una lista di manga correlati. In alcuni casi potrebbe essere imprecisa o mancante</string>
<string name="remove_completed_downloads_confirm">La tua cronologia dei download sarà permanentemente cancellata</string>
<string name="reader_zoom_buttons_summary">Se mostrare o no i tasti di controllo dello zoom in basso a destra</string>
<string name="content_type_manga">Manga</string>
<string name="tracker_wifi_only_summary">Non controllare nuovi capitoli mentre è in uso una connessione a consumo</string>
<string name="error_multiple_states_not_supported">Questa fonte di manga non supporta il filtraggio per multipli stati</string>
<string name="order_added">Aggiunto</string>
<string name="source_summary_pattern">%1$s, %2$s</string>
<string name="on_device">Su dispositivo</string>
<string name="password">Password</string>
<string name="download_option_whole_manga">L\'intero manga</string>
<string name="settings_apply_restart_required">Riavviare l\'applicazione per applicare queste modifiche</string>
<string name="backup_frequency">Frequenza creazione backup</string>
<string name="data_and_privacy">Dati e privacy</string>
<string name="content_type_hentai">Hentai</string>
<string name="clear_source_cookies_summary">Pulisci cookies solo per dominio specifico. Nella maggior parte dei casi invaliderà l\'autorizzazione</string>
<string name="downloads_wifi_only_summary">Smetti di scaricare quando si passa a rete mobile</string>
<string name="suggest_new_sources">Suggerisci nuove fonti dopo aggiornamento dell\'app</string>
<string name="user_agent">Intestazione UserAgent</string>
<string name="error_filter_states_genre_not_supported">Questa fonte non supporta il filtraggio per sia genere che stato</string>
<string name="ignore_ssl_errors">Ignora errori SSL</string>
<string name="error_filter_locale_genre_not_supported">Questa fonte non supporta il filtraggio per sia genere che localizzazione</string>
<string name="periodic_backups_enable">Abilita backup periodici</string>
<string name="server_address">Indirizzo server</string>
<string name="content_type_comics">Fumetti</string>
<string name="moved_to_top">Spostato in cima</string>
<string name="find_similar">Trova simili</string>
<string name="data_not_restored_text">Assicurati di aver selezionato il file di backup corretto</string>
<string name="catalog">Catalogo</string>
<string name="view_list">Vedi lista</string>
<string name="welcome_text">Selezionare le fonti di contenuti da abilitare. Si può configurare anche successivamente nelle impostazioni</string>
<string name="unknown">Sconosciuto</string>
<string name="in_progress">In progresso</string>
<string name="download_option_manual_selection">Selezione manuale capitoli</string>
<string name="enhanced_colors_summary">Riduce il banding, ma può impattare la performance</string>
<string name="pause">Pausa</string>
<string name="remove_completed">Rimuovi completati</string>
<string name="items_limit_exceeded">Impossibile aggiungere altri oggetti</string>
<string name="frequency_every_2_days">Ogni 2 giorni</string>
<string name="suggestions_notifications_summary">A volte mostra le notifiche sui manga suggeriti</string>
<string name="invalid_value_message">Valore non valido</string>
<string name="downloads_cancelled">I download sono stati annullati</string>
<string name="reader_optimize">Riduci consumo di memoria (beta)</string>
<string name="apply">Applica</string>
<string name="restore">Ripristina</string>
<string name="data_not_restored">Dati non ripristinati</string>
<string name="manage_sources">Gestisci fonti</string>
<string name="directories">Cartelle</string>
<string name="local_manga_directories">Cartelle manga locali</string>
<string name="manage_categories">Gestisci categorie</string>
<string name="no_manga_sources_found">Nessuna fonte manga trovata in base alla tua query</string>
<string name="color_light">Chiaro</string>
<string name="web_view_unavailable">WebView non disponibile: controlla se è installato un provider di WebView</string>
<string name="genres_search_hint">Inizia a scrivere il nome del genere</string>
<string name="port">Porta</string>
<string name="type">Tipo</string>
<string name="search_hint">Inserisci titolo manga, genere o nome fonte</string>
<string name="frequency_once_per_week">Una volta a settimana</string>
<string name="description">Descrizione</string>
<string name="periodic_backups">Backup periodici</string>
<string name="reader_zoom_buttons">Mostra tasti zoom</string>
<string name="sources_reorder_tip">Tocca e tieni premuto su un elemento per riordinarlo</string>
<string name="globally">Globalmente</string>
<string name="resume">Riprendi</string>
<string name="images_proxy_title">Proxy ottimizzazione immagini</string>
<string name="username">Nome utente</string>
<string name="frequency_twice_per_month">Due volte al mese</string>
<string name="main_screen_sections">Sezioni principali schermo</string>
<string name="advanced">Avanzate</string>
<string name="downloads_settings_info">Se stai avendo problemi di blocco dal server, puoi abilitare il rallentamento dei download per ogni fonte di manga nelle impostazioni delle fonti</string>
<string name="sync_settings">Impostazioni di sincronizzazione</string>
<string name="online_variant">Variante online</string>
<string name="disable_battery_optimization_summary_downloads">Potrebbe aiutare a far iniziare il download in caso di problemi</string>
<string name="error_multiple_genres_not_supported">Questa fonte di manga non supporta il filtraggio per multipli generi</string>
<string name="download_option_all_unread_b">Tutti i capitoli non letti (%s)</string>
<string name="authorization_optional">Autorizzazione (opzionale)</string>
<string name="color_dark">Scuro</string>
<string name="this_manga">Questo manga</string>
<string name="reader_info_bar_summary">Mostra l\'orario attuale e il progresso di lettura in cima allo schermo</string>
<string name="translations">Traduzioni</string>
<string name="downloads_paused">I download sono stati messi in pausa</string>
<string name="too_many_requests_message">Troppe richieste. Riprova più tardi</string>
<string name="downloads_wifi_only">Scarica solo via Wi-Fi</string>
<string name="lock_screen_rotation">Blocca rotazione schermo</string>
<string name="cancel_all_downloads_confirm">Tutti i download attivi saranno annullati, i dati scaricati parzialmente saranno persi</string>
<string name="by_relevance">Rilevanza</string>
<string name="related_manga">Manga correlati</string>
<string name="state_abandoned">Abbandonato</string>
<string name="download_option_first_n_chapters">Primo %s</string>
<string name="keep_screen_on">Tieni schermo acceso</string>
<string name="paused">In pausa</string>
<string name="text_downloads_list_holder">Non hai alcun download</string>
<string name="skip">Salta</string>
<string name="error_search_not_supported">Questa fonte di manga non supporta la ricerca</string>
<string name="invalid_port_number">Numero di porta non valido</string>
<string name="suggestions_wifi_only_summary">Non aggiornare suggerimenti mentre è in uso una connessione a consumo</string>
<string name="webtoon_zoom_summary">Consenti gesto di zoom in modalità webtoon</string>
<string name="frequency_once_per_month">Una volta al mese</string>
<string name="network">Rete</string>
<string name="downloaded">Scaricato</string>
<string name="suggestions_enable_prompt">Vuoi ricevere suggerimenti personalizzati di manga?</string>
<string name="manual">Manuale</string>
<string name="custom_directory">Cartella personalizzata</string>
<string name="more">Di più</string>
<string name="reader_optimize_summary">Riduci la qualità delle pagine fuori schermo per usare meno memoria</string>
<string name="address">Indirizzo</string>
<string name="color_correction_apply_text">Queste impostazioni possono essere applicate globalmente o per solo questo manga. Le applicazioni globali non sovrascrivono quelle individuali.</string>
<string name="source_enabled">Fonte abilitata</string>
<string name="enhanced_colors">Modalità colori 32-bit</string>
<string name="folder_with_images_import_description">Puoi selezionare una cartella contenente archivi o immagini. Ogni archivio (o sottocartella) sarà riconosciuto come singolo capitolo.</string>
<string name="default_section">Sezione default</string>
<string name="background">Sfondo</string>
<string name="disable_nsfw_summary">Disabilita fonti per adulti e nascondi manga per adulti dalla lista se possibile</string>
<string name="speed_value">x%.1f</string>
<string name="downloads_removed">I download sono stati rimossi</string>
<string name="pages_animation_summary">Animazioni cambio pagina</string>
<string name="no_access_to_file">Non hai accesso a questo file o cartella</string>
<string name="mirror_switching">Scegli mirror automaticamente</string>
<string name="restore_summary">Ripristina backup creato in precedenza</string>
<string name="show_pages_numbers_summary">Mostra numeri di pagina nell\'angolo inferiore</string>
<string name="zoom_out">Rimpicciolire</string>
<string name="keep_screen_on_summary">Non spegnere lo schermo durante la lettura di manga</string>
<string name="download_option_next_unread_n_chapters">Prossimo %s non letto</string>
<string name="clear_network_cache">Pulisci cache di rete</string>
<string name="voice_search">Ricerca vocale</string>
<string name="enable">Abilita</string>
<string name="backup_date_">Data del backup: %s</string>
<string name="images_procy_description">Usa il servizio wsrv.nl per ridurre i consumi di rete e velocizzare il caricamento delle immagini, se possibile</string>
<string name="no_manga_sources_catalog_text">Non sono disponibili fonti in questa sezione, o potrebbero essere state aggiunte tutte.
\nRimani sintonizzato</string>
<string name="available_d">Disponibile: %1$d</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="state">Stato</string>
<string name="manga_list">Lista manga</string>
<string name="grayscale">Bianco e nero</string>
<string name="disable_nsfw">Disabilita contenuti per adulti</string>
<string name="last_successful_backup">Ultimo backup completato: %s</string>
<string name="color_white">Bianco</string>
<string name="downloads_resumed">I download sono stati ripresi</string>
<string name="details_button_tip">Tocca e tieni premuto il tasto Leggi per vedere più opzioni</string>
<string name="state_paused">In pausa</string>
<string name="to_top">Va in cima</string>
<string name="show">Mostra</string>
<string name="sync_auth_hint">Puoi accedere ad un account esistente o crearne uno nuovo</string>
<string name="backups_output_directory">Cartella salvataggio backup</string>
<string name="invert_colors">Inverti colori</string>
<string name="mirror_switching_summary">Cambiare dominio di fonte del manga automaticamente in caso di errore se sono disponibili mirror</string>
<string name="no_thanks">No grazie</string>
<string name="suggest_new_sources_summary">Chiedi se aggiungere nuove fonti dopo aggiornamento dell\'applicazione</string>
<string name="speed">Velocità</string>
<string name="download_option_all_chapters">Tutti i capitoli con traduzioni %s</string>
<string name="content_type_other">Altro</string>
<string name="suggestion_manga">Consigliato: %s</string>
<string name="color_black">Nero</string>
<string name="this_month">Questo mese</string>
<string name="sync_auth">Accedi per sincronizzare l\'account</string>
<string name="proxy">Proxy</string>
<string name="restore_backup_description">Importa un backup di dati utente creato precedentemente</string>
<string name="got_it">Ho capito</string>
<string name="comics_archive_import_description">Puoi selezionare uno o più file .cbz o .zip, ogni file sarà riconosciuto come un manga a parte.</string>
<string name="show_on_shelf">Mostra sullo Scaffale</string>
</resources>

View File

@@ -3,12 +3,13 @@
<style name="Base.V31.Kotatsu" parent="Base.V27.Kotatsu">
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_splash</item>
<item name="android:windowSplashScreenBackground">@android:color/system_neutral2_900</item>
<item name="android:windowSplashScreenAnimationDuration">800</item>
<item name="android:windowSplashScreenAnimationDuration">@integer/splash_screen_duration</item>
<item name="android:windowSplashScreenBackground">@color/m3_sys_color_dynamic_dark_surface</item>
</style>
<!-- From ThemeOverlay.Material3.DynamicColors.Dark -->
<style name="Theme.Kotatsu.Monet">
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="isMaterial3DynamicColorApplied">true</item>
<!-- Color palettes -->
<item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item>
@@ -61,11 +62,11 @@
<item name="colorOnErrorContainer">@color/m3_sys_color_dark_on_error_container</item>
<item name="colorOutline">@color/m3_sys_color_dynamic_dark_outline</item>
<item name="colorOutlineVariant">@color/m3_sys_color_dynamic_dark_outline_variant</item>
<item name="android:colorBackground">@android:color/system_neutral2_900</item>
<item name="m3ColorBackground">@android:color/system_neutral2_900</item>
<item name="m3ColorExploreButton">@android:color/system_neutral2_800</item>
<item name="m3ColorCardBackground">@android:color/system_neutral2_700</item>
<item name="m3ColorBottomMenuBackground">@android:color/system_neutral2_800</item>
<item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_surface</item>
<item name="m3ColorBackground">@color/m3_sys_color_dynamic_dark_surface</item>
<item name="m3ColorExploreButton">@color/m3_sys_color_dynamic_dark_surface_container_low</item>
<item name="m3ColorCardBackground">@color/m3_sys_color_dynamic_dark_surface_container_low</item>
<item name="m3ColorBottomMenuBackground">@color/m3_sys_color_dynamic_dark_surface_container_low</item>
<!-- Default Framework Text Colors. -->
<item name="android:textColorPrimary">@color/m3_dynamic_dark_default_color_primary_text
</item>

View File

@@ -11,7 +11,6 @@
<color name="status_bar_incognito">#66311B92</color>
<color name="warning">#FB8C00</color>
<color name="launcher_background">#222222</color>
<color name="splash_primary">#0C4091</color>
<!-- Color schemes colors -->
<color name="background_miku">#191C1C</color>

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ThemeOverlay.Kotatsu" parent="ThemeOverlay.Material3.Dark" />
<style name="ThemeOverlay.Kotatsu" parent="ThemeOverlay.Material3.Dark">
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="ThemeOverlay.Kotatsu.Amoled" parent="">
<item name="colorSurface">@color/surface_amoled</item>

View File

@@ -2,31 +2,37 @@
<resources>
<plurals name="items">
<item quantity="one">%1$d item</item>
<item quantity="many">%1$d itens</item>
<item quantity="other">%1$d itens</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d capítulo</item>
<item quantity="many">%1$d capítulos</item>
<item quantity="other">%1$d capítulos</item>
</plurals>
<plurals name="new_chapters">
<item quantity="one">%1$d novo capítulo</item>
<item quantity="other">%1$d novos capítulos</item>
<item quantity="one">%1$d capítulo novo</item>
<item quantity="many">%1$d capítulos novos</item>
<item quantity="other">%1$d capítulos novos</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d minuto atrás</item>
<item quantity="many">%1$d minutos atrás</item>
<item quantity="other">%1$d minutos atrás</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d hora atrás</item>
<item quantity="many">%1$d horas atrás</item>
<item quantity="other">%1$d horas atrás</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d dia atrás</item>
<item quantity="many">%1$d dias atrás</item>
<item quantity="other">%1$d dias atrás</item>
</plurals>
<plurals name="months_ago">
<item quantity="one">%1$d mês atrás</item>
<item quantity="many">%1$d meses atrás</item>
<item quantity="other"></item>
<item quantity="other">%1$d meses atrás</item>
</plurals>
</resources>

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