Compare commits

...

56 Commits

Author SHA1 Message Date
Koitharu 34f6e5232b
Update readme 6 months ago
Koitharu f205c1b3dc
Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 6 months ago
Milo Ivir 4b2a487c37 Translated using Weblate (Croatian)
Currently translated at 99.8% (895 of 896 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
6 months ago
MuhamadSyabitHidayattulloh 726ac21974 Translated using Weblate (Indonesian)
Currently translated at 99.8% (895 of 896 strings)

Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
6 months ago
Robert Broketa 6b35216949 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (896 of 896 strings)

Co-authored-by: Robert Broketa <robert@broketa.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
6 months ago
João Augusto Casagrande 22cae62f17 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (895 of 896 strings)

Co-authored-by: João Augusto Casagrande <joao.augusto1809@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
6 months ago
Oğuz Ersen 4733caf2e6 Translated using Weblate (Turkish)
Currently translated at 100.0% (896 of 896 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
6 months ago
Максим Горпиніч d49103de1f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (896 of 896 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (894 of 894 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
6 months ago
Koitharu 414bab7ce3
Update readme 6 months ago
Koitharu 64c1873eb5
Merge branch 'master' into devel 6 months ago
Koitharu 06a0b5829b
Fix crashes
(cherry picked from commit 1d32f53bdd)
6 months ago
Koitharu 0ce2870c8b
Fix chapters list not accessible
(cherry picked from commit 5701862661)
6 months ago
Koitharu f59027666b
Fix loading empty manga
(cherry picked from commit 5590ab7c8a)
6 months ago
Nathan Bapin 8513bc6daf
Fix forget page when the screen is rotated (#1674)
(cherry picked from commit e2fcfcc7a8)
6 months ago
Koitharu cceaefc896
Avoid memory leak in ExceptionResolver
(cherry picked from commit 7a3b2a9bb4)
6 months ago
Koitharu 1d32f53bdd
Fix crashes 6 months ago
Koitharu 0e98dd8695
Refactor SearchMenuProvider 6 months ago
MuhamadSyabitHidayattulloh 119b7c2ac7 Add filtering options for pinned sources and empty results in search menu 6 months ago
Koitharu 5701862661
Fix chapters list not accessible 6 months ago
Koitharu 5590ab7c8a
Fix loading empty manga 6 months ago
Koitharu 9fde0106be
Fix code formatting 6 months ago
skepsun e73f077dc5 remove unnecessary summary 6 months ago
skepsun c37458d43a Add foldable device support (auto two-page) 6 months ago
Nathan Bapin e2fcfcc7a8
Fix forget page when the screen is rotated (#1674) 6 months ago
Koitharu 7a3b2a9bb4
Avoid memory leak in ExceptionResolver 6 months ago
Koitharu 881f154b5e
Update parsers 6 months ago
Koitharu 34be5d16f2
Merge pull request #1701 from weblate/weblate-kotatsu-strings 6 months ago
Milo Ivir e7e554648d
Translated using Weblate (Croatian)
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
6 months ago
Draken 89a4180b46
Translated using Weblate (Vietnamese)
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
6 months ago
MuhamadSyabitHidayattulloh 4e2e190547
Translated using Weblate (Indonesian)
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
6 months ago
João Augusto Casagrande 3c557aae6c
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: João Augusto Casagrande <joao.augusto1809@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
6 months ago
Nicola Bortoletto 0b00a3675d
Translated using Weblate (Italian)
Currently translated at 100.0% (893 of 893 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
6 months ago
Alvoracz 8f20be6953
Translated using Weblate (Czech)
Currently translated at 97.8% (874 of 893 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
6 months ago
Kanta Sekiguchi 26875c01c6
Translated using Weblate (Japanese)
Currently translated at 90.8% (811 of 893 strings)

Co-authored-by: Kanta Sekiguchi <kanta.sekiguchi360@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
6 months ago
Koitharu 4beb34c1a5 Translated using Weblate (Russian)
Currently translated at 99.7% (891 of 893 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
6 months ago
Conrado 1d50ab00c4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (889 of 889 strings)

Co-authored-by: Conrado <deadlocked53.89@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
6 months ago
Ruffghanor 299cd229ec Translated using Weblate (Portuguese)
Currently translated at 100.0% (889 of 889 strings)

Co-authored-by: Ruffghanor <ruffghanor20@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
6 months ago
Nahid hasan Limon b02f394cd4 Translated using Weblate (Bengali)
Currently translated at 22.9% (204 of 889 strings)

Co-authored-by: Nahid hasan Limon <nahidhasanlimon401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/
Translation: Kotatsu/Strings
6 months ago
Oğuz Ersen 7352f06564 Translated using Weblate (Turkish)
Currently translated at 100.0% (893 of 893 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (882 of 882 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
6 months ago
Milo Ivir 1e4861367e Translated using Weblate (Croatian)
Currently translated at 100.0% (882 of 882 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
6 months ago
Draken bc3208946b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (880 of 880 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
6 months ago
MuhamadSyabitHidayattulloh d5fbb00676 Translated using Weblate (Indonesian)
Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (880 of 880 strings)

Co-authored-by: MuhamadSyabitHidayattulloh <tebepc@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
6 months ago
Hecker_01 7514362ca4 Translated using Weblate (Dutch)
Currently translated at 4.0% (36 of 880 strings)

Translated using Weblate (Dutch)

Currently translated at 88.8% (8 of 9 strings)

Added translation using Weblate (Dutch)

Added translation using Weblate (Dutch)

Co-authored-by: Hecker_01 <jesseflantua@icloud.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/nl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nl/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
6 months ago
Infy's Tagalog Translations e76a04bea0 Translated using Weblate (Filipino)
Currently translated at 98.9% (871 of 880 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
6 months ago
Максим Горпиніч 732a6e7c26 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (882 of 882 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (880 of 880 strings)

Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
6 months ago
Макар Разин f3111dc636 Translated using Weblate (Russian)
Currently translated at 100.0% (876 of 876 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
6 months ago
Nicola Bortoletto e0e0cf4ecd Translated using Weblate (Italian)
Currently translated at 100.0% (889 of 889 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (882 of 882 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (876 of 876 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
6 months ago
Nataniel Dika Kurniawan 50f302a7f8 Translated using Weblate (Indonesian)
Currently translated at 100.0% (876 of 876 strings)

Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
6 months ago
google-labs-jules[bot] 500995a9d8 feat(settings): Add "Every 6 hours" option for periodic backups
Adds a new "Every 6 hours" frequency option to the periodic backup settings.

To maintain consistency with the existing preference values, which are stored in days, this new option is represented internally as a fractional value of `0.25` days.

The implementation includes:
- Adding the new string resource and updating the preference arrays.
- Changing the preference type in `AppSettings.kt` from `Long` to `Float` to accommodate the fractional value.
- Updating the millisecond conversion logic to correctly calculate the interval from a float value in days.

This approach avoids a complex data migration and is simpler and safer than changing the base unit for all values from days to hours.
6 months ago
Koitharu beaf5cc0d5
Remove SavedFilterBackup class 6 months ago
google-labs-jules[bot] 6377de470d feat: Add saved filters to backup and restore
This commit adds support for backing up and restoring saved filters.

- Added a new `SAVED_FILTERS` section to the backup process.
- Implemented the logic to read filters from SharedPreferences during backup and write them back during restore.
- Fixed compilation errors in `AppBackupAgent` and `BackupSectionModel`.
6 months ago
google-labs-jules[bot] dec45f7851 feat: Add saved filters to backup and restore
This commit adds support for backing up and restoring saved filters.

- Added a new `SAVED_FILTERS` section to the backup process.
- Implemented the logic to read filters from SharedPreferences during backup and write them back during restore.
6 months ago
Koitharu dbada34a43
Move pull gesture option to reader settings 6 months ago
Koitharu b62467964e
Fix filters on tablet 6 months ago
Koitharu 3249e10931
Exclude broken sources from catalog 6 months ago
Koitharu 0d5229b112
Improve local manga directories config screen 6 months ago

@ -1,24 +1,16 @@
<div align="center"> > [!IMPORTANT]
> In light of recent challenges — including threating actions from Kakao Entertainment Corp and upcoming Googles
<a href="https://kotatsu.app"> > [new sideloading policy](https://f-droid.org/ru/2025/10/28/sideloading.html) — weve made the difficult decision to shut down Kotatsu and end its support. Were deeply grateful
<img src="./.github/assets/vtuber.png" alt="Kotatsu Logo" title="Kotatsu" width="600"/> > to everyone who contributed and to the amazing community that grew around this project.
</a>
# [Kotatsu](https://kotatsu.app)
**[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is a free and open-source manga reader for Android with built-in online content sources.**
![Downloads count](https://img.shields.io/github/downloads/KotatsuApp/Kotatsu/total?color=1976d2) ![Latest Stable version](https://img.shields.io/github/v/release/KotatsuApp/Kotatsu?color=2596be&label=latest) ![Android 6.0](https://img.shields.io/badge/android-6.0+-brightgreen) [![Sources count](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FKotatsuApp%2Fkotatsu-parsers%2Frefs%2Fheads%2Fmaster%2F.github%2Fsummary.yaml&query=total&label=manga%20sources&color=%23E9321C)](https://github.com/KotatsuApp/kotatsu-parsers) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF?)](https://t.me/kotatsuapp) [![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu)](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE) ---
### Download <div align="center">
<div align="left">
* **Recommended:** Download and install APK from [GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest). Application has a built-in self-updating feature. **[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is a free and open-source manga reader for Android with built-in
* Get it on [F-Droid](https://f-droid.org/packages/org.koitharu.kotatsu). The F-Droid build may be a bit outdated and some fixes might be missing. online content sources.**
* Also [nightly builds](https://github.com/KotatsuApp/Kotatsu-nightly/releases) are available (Unstable, use at your own risk). Application has a built-in self-updating feature.
</div> ![Android 6.0](https://img.shields.io/badge/android-6.0+-brightgreen) [![Sources count](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FKotatsuApp%2Fkotatsu-parsers%2Frefs%2Fheads%2Fmaster%2F.github%2Fsummary.yaml&query=total&label=manga%20sources&color=%23E9321C)](https://github.com/KotatsuApp/kotatsu-parsers) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF?)](https://t.me/kotatsuapp) [![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu)](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
### Main Features ### Main Features
@ -86,7 +78,8 @@ please head over to the [Weblate project page](https://hosted.weblate.org/engage
</br> </br>
**📌 Pull requests are welcome, if you want: See [CONTRIBUTING.md](https://github.com/KotatsuApp/Kotatsu/blob/devel/CONTRIBUTING.md) for the guidelines** **📌 Pull requests are welcome, if you want:
See [CONTRIBUTING.md](https://github.com/KotatsuApp/Kotatsu/blob/devel/CONTRIBUTING.md) for the guidelines**
### Certificate fingerprints ### Certificate fingerprints
@ -104,7 +97,9 @@ please head over to the [Weblate project page](https://hosted.weblate.org/engage
<div align="left"> <div align="left">
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications
to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build &
install instructions.
</div> </div>
@ -112,6 +107,9 @@ You may copy, distribute and modify the software as long as you track changes/da
<div align="left"> <div align="left">
The developers of this application do not have any affiliation with the content available in the app and does not store or distribute any content. This application should be considered a web browser, all content that can be found using this application is freely available on the Internet. All DMCA takedown requests should be sent to the owners of the website where the content is hosted. The developers of this application do not have any affiliation with the content available in the app and does not store
or distribute any content. This application should be considered a web browser, all content that can be found using this
application is freely available on the Internet. All DMCA takedown requests should be sent to the owners of the website
where the content is hosted.
</div> </div>

@ -21,8 +21,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 23 minSdk = 23
targetSdk = 36 targetSdk = 36
versionCode = 1031 versionCode = 1033
versionName = '9.3' versionName = '9.4.1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {
@ -155,6 +155,9 @@ dependencies {
implementation libs.androidx.work.runtime implementation libs.androidx.work.runtime
implementation libs.guava implementation libs.guava
// Foldable/Window layout
implementation libs.androidx.window
implementation libs.androidx.room.runtime implementation libs.androidx.room.runtime
implementation libs.androidx.room.ktx implementation libs.androidx.room.ktx
ksp libs.androidx.room.compiler ksp libs.androidx.room.compiler

@ -34,6 +34,9 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.CompositeResult import org.koitharu.kotatsu.core.util.CompositeResult
import org.koitharu.kotatsu.core.util.progress.Progress import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.data.PersistableFilter
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.data.TapGridSettings import org.koitharu.kotatsu.reader.data.TapGridSettings
import java.io.InputStream import java.io.InputStream
@ -48,6 +51,8 @@ class BackupRepository @Inject constructor(
private val database: MangaDatabase, private val database: MangaDatabase,
private val settings: AppSettings, private val settings: AppSettings,
private val tapGridSettings: TapGridSettings, private val tapGridSettings: TapGridSettings,
private val mangaSourcesRepository: MangaSourcesRepository,
private val savedFiltersRepository: SavedFiltersRepository,
) { ) {
private val json = Json { private val json = Json {
@ -123,6 +128,18 @@ class BackupRepository @Inject constructor(
data = database.getStatsDao().dumpEnabled().map { StatisticBackup(it) }, data = database.getStatsDao().dumpEnabled().map { StatisticBackup(it) },
serializer = serializer(), serializer = serializer(),
) )
BackupSection.SAVED_FILTERS -> {
val sources = mangaSourcesRepository.getEnabledSources()
val filters = sources.flatMap { source ->
savedFiltersRepository.getAll(source)
}
output.writeJsonArray(
section = BackupSection.SAVED_FILTERS,
data = filters.asFlow(),
serializer = serializer(),
)
}
} }
progress?.emit(commonProgress) progress?.emit(commonProgress)
commonProgress++ commonProgress++
@ -142,7 +159,7 @@ class BackupRepository @Inject constructor(
while (entry != null) { while (entry != null) {
val section = BackupSection.of(entry) val section = BackupSection.of(entry)
if (section in sections) { if (section in sections) {
result = result + when (section) { result += when (section) {
BackupSection.INDEX -> CompositeResult.EMPTY // useless in our case BackupSection.INDEX -> CompositeResult.EMPTY // useless in our case
BackupSection.HISTORY -> input.readJsonArray<HistoryBackup>(serializer()).restoreToDb { BackupSection.HISTORY -> input.readJsonArray<HistoryBackup>(serializer()).restoreToDb {
upsertManga(it.manga) upsertManga(it.manga)
@ -185,6 +202,11 @@ class BackupRepository @Inject constructor(
getStatsDao().upsert(it.toEntity()) getStatsDao().upsert(it.toEntity())
} }
BackupSection.SAVED_FILTERS -> input.readJsonArray<PersistableFilter>(serializer())
.restoreWithoutTransaction {
savedFiltersRepository.save(it)
}
null -> CompositeResult.EMPTY // skip unknown entries null -> CompositeResult.EMPTY // skip unknown entries
} }
progress?.emit(commonProgress) progress?.emit(commonProgress)
@ -281,4 +303,12 @@ class BackupRepository @Inject constructor(
} }
} }
} }
private suspend inline fun <T> Sequence<T>.restoreWithoutTransaction(crossinline block: suspend (T) -> Unit): CompositeResult {
return fold(CompositeResult.EMPTY) { result, item ->
result + runCatchingCancellable {
block(item)
}
}
}
} }

@ -12,6 +12,8 @@ import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.backups.data.BackupRepository import org.koitharu.kotatsu.backups.data.BackupRepository
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
import org.koitharu.kotatsu.reader.data.TapGridSettings import org.koitharu.kotatsu.reader.data.TapGridSettings
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
@ -39,9 +41,17 @@ class AppBackupAgent : BackupAgent() {
val file = createBackupFile( val file = createBackupFile(
this, this,
BackupRepository( BackupRepository(
MangaDatabase(context = applicationContext), database = MangaDatabase(context = applicationContext),
AppSettings(applicationContext), settings = AppSettings(applicationContext),
TapGridSettings(applicationContext), tapGridSettings = TapGridSettings(applicationContext),
mangaSourcesRepository = MangaSourcesRepository(
context = applicationContext,
db = MangaDatabase(context = applicationContext),
settings = AppSettings(applicationContext),
),
savedFiltersRepository = SavedFiltersRepository(
context = applicationContext,
),
), ),
) )
try { try {
@ -67,6 +77,14 @@ class AppBackupAgent : BackupAgent() {
database = MangaDatabase(applicationContext), database = MangaDatabase(applicationContext),
settings = AppSettings(applicationContext), settings = AppSettings(applicationContext),
tapGridSettings = TapGridSettings(applicationContext), tapGridSettings = TapGridSettings(applicationContext),
mangaSourcesRepository = MangaSourcesRepository(
context = applicationContext,
db = MangaDatabase(context = applicationContext),
settings = AppSettings(applicationContext),
),
savedFiltersRepository = SavedFiltersRepository(
context = applicationContext,
),
), ),
) )
destination.delete() destination.delete()

@ -17,6 +17,7 @@ enum class BackupSection(
SOURCES("sources"), SOURCES("sources"),
SCROBBLING("scrobbling"), SCROBBLING("scrobbling"),
STATS("statistics"), STATS("statistics"),
SAVED_FILTERS("saved_filters"),
; ;
companion object { companion object {

@ -25,6 +25,7 @@ data class BackupSectionModel(
BackupSection.SOURCES -> R.string.remote_sources BackupSection.SOURCES -> R.string.remote_sources
BackupSection.SCROBBLING -> R.string.tracking BackupSection.SCROBBLING -> R.string.tracking
BackupSection.STATS -> R.string.statistics BackupSection.STATS -> R.string.statistics
BackupSection.SAVED_FILTERS -> R.string.saved_filters
} }
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.core.exceptions
import org.koitharu.kotatsu.details.ui.pager.EmptyMangaReason
import org.koitharu.kotatsu.parsers.model.Manga
class EmptyMangaException(
val reason: EmptyMangaReason?,
val manga: Manga,
cause: Throwable?
) : IllegalStateException(cause)

@ -8,13 +8,15 @@ import androidx.collection.MutableScatterMap
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import dagger.assisted.Assisted import androidx.lifecycle.Lifecycle
import dagger.assisted.AssistedFactory import androidx.lifecycle.LifecycleOwner
import dagger.assisted.AssistedInject import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.async
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyMangaException
import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
@ -24,6 +26,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.util.ext.isHttpUrl import org.koitharu.kotatsu.core.util.ext.isHttpUrl
import org.koitharu.kotatsu.core.util.ext.restartApplication import org.koitharu.kotatsu.core.util.ext.restartApplication
import org.koitharu.kotatsu.details.ui.pager.EmptyMangaReason
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -32,14 +35,15 @@ import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredExcept
import org.koitharu.kotatsu.scrobbling.common.ui.ScrobblerAuthHelper import org.koitharu.kotatsu.scrobbling.common.ui.ScrobblerAuthHelper
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import java.security.cert.CertPathValidatorException import java.security.cert.CertPathValidatorException
import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
import javax.net.ssl.SSLException import javax.net.ssl.SSLException
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class ExceptionResolver @AssistedInject constructor( class ExceptionResolver private constructor(
@Assisted private val host: Host, private val host: Host,
private val settings: AppSettings, private val settings: AppSettings,
private val scrobblerAuthHelperProvider: Provider<ScrobblerAuthHelper>, private val scrobblerAuthHelperProvider: Provider<ScrobblerAuthHelper>,
) { ) {
@ -56,10 +60,11 @@ class ExceptionResolver @AssistedInject constructor(
} }
fun showErrorDetails(e: Throwable, url: String? = null) { fun showErrorDetails(e: Throwable, url: String? = null) {
host.router()?.showErrorDialog(e, url) host.router.showErrorDialog(e, url)
} }
suspend fun resolve(e: Throwable): Boolean = when (e) { suspend fun resolve(e: Throwable): Boolean = host.lifecycleScope.async {
when (e) {
is CloudFlareProtectedException -> resolveCF(e) is CloudFlareProtectedException -> resolveCF(e)
is AuthRequiredException -> resolveAuthException(e.source) is AuthRequiredException -> resolveAuthException(e.source)
is SSLException, is SSLException,
@ -71,7 +76,7 @@ class ExceptionResolver @AssistedInject constructor(
is InteractiveActionRequiredException -> resolveBrowserAction(e) is InteractiveActionRequiredException -> resolveBrowserAction(e)
is ProxyConfigException -> { is ProxyConfigException -> {
host.router()?.openProxySettings() host.router.openProxySettings()
false false
} }
@ -80,6 +85,16 @@ class ExceptionResolver @AssistedInject constructor(
false false
} }
is EmptyMangaException -> {
when (e.reason) {
EmptyMangaReason.NO_CHAPTERS -> openAlternatives(e.manga)
EmptyMangaReason.LOADING_ERROR -> Unit
EmptyMangaReason.RESTRICTED -> host.router.openBrowser(e.manga)
else -> Unit
}
false
}
is UnsupportedSourceException -> { is UnsupportedSourceException -> {
e.manga?.let { openAlternatives(it) } e.manga?.let { openAlternatives(it) }
false false
@ -99,6 +114,7 @@ class ExceptionResolver @AssistedInject constructor(
else -> false else -> false
} }
}.await()
private suspend fun resolveBrowserAction( private suspend fun resolveBrowserAction(
e: InteractiveActionRequiredException e: InteractiveActionRequiredException
@ -118,11 +134,11 @@ class ExceptionResolver @AssistedInject constructor(
} }
private fun openInBrowser(url: String) { private fun openInBrowser(url: String) {
host.router()?.openBrowser(url, null, null) host.router.openBrowser(url, null, null)
} }
private fun openAlternatives(manga: Manga) { private fun openAlternatives(manga: Manga) {
host.router()?.openAlternatives(manga) host.router.openAlternatives(manga)
} }
private fun handleActivityResult(tag: String, result: Boolean) { private fun handleActivityResult(tag: String, result: Boolean) {
@ -130,7 +146,7 @@ class ExceptionResolver @AssistedInject constructor(
} }
private fun showSslErrorDialog() { private fun showSslErrorDialog() {
val ctx = host.getContext() ?: return val ctx = host.context ?: return
if (settings.isSSLBypassEnabled) { if (settings.isSSLBypassEnabled) {
Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show() Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
return return
@ -147,27 +163,65 @@ class ExceptionResolver @AssistedInject constructor(
}.show() }.show()
} }
private inline fun Host.withContext(block: Context.() -> Unit) { class Factory @Inject constructor(
getContext()?.apply(block) private val settings: AppSettings,
private val scrobblerAuthHelperProvider: Provider<ScrobblerAuthHelper>,
) {
fun create(fragment: Fragment) = ExceptionResolver(
host = Host.FragmentHost(fragment),
settings = settings,
scrobblerAuthHelperProvider = scrobblerAuthHelperProvider,
)
fun create(activity: FragmentActivity) = ExceptionResolver(
host = Host.ActivityHost(activity),
settings = settings,
scrobblerAuthHelperProvider = scrobblerAuthHelperProvider,
)
} }
private fun Host.router(): AppRouter? = when (this) { private sealed interface Host : ActivityResultCaller, LifecycleOwner {
is FragmentActivity -> router
is Fragment -> router val context: Context?
else -> null
val router: AppRouter
val fragmentManager: FragmentManager
inline fun withContext(block: Context.() -> Unit) {
context?.apply(block)
} }
interface Host : ActivityResultCaller { class ActivityHost(val activity: FragmentActivity) : Host,
ActivityResultCaller by activity,
LifecycleOwner by activity {
override val context: Context
get() = activity
fun getChildFragmentManager(): FragmentManager override val router: AppRouter
get() = activity.router
fun getContext(): Context? override val fragmentManager: FragmentManager
get() = activity.supportFragmentManager
} }
@AssistedFactory class FragmentHost(val fragment: Fragment) : Host,
interface Factory { ActivityResultCaller by fragment {
override val context: Context?
get() = fragment.context
override val router: AppRouter
get() = fragment.router
fun create(host: Host): ExceptionResolver override val fragmentManager: FragmentManager
get() = fragment.childFragmentManager
override val lifecycle: Lifecycle
get() = fragment.viewLifecycleOwner.lifecycle
}
} }
companion object { companion object {
@ -187,6 +241,12 @@ class ExceptionResolver @AssistedInject constructor(
is InteractiveActionRequiredException -> R.string._continue is InteractiveActionRequiredException -> R.string._continue
is EmptyMangaException -> when (e.reason) {
EmptyMangaReason.RESTRICTED -> if (e.manga.publicUrl.isHttpUrl()) R.string.open_in_browser else 0
EmptyMangaReason.NO_CHAPTERS -> R.string.alternatives
else -> 0
}
else -> 0 else -> 0
} }

@ -215,6 +215,12 @@ class AppRouter private constructor(
startActivity(browserIntent(contextOrNull() ?: return, url, source, title)) startActivity(browserIntent(contextOrNull() ?: return, url, source, title))
} }
fun openBrowser(manga: Manga) = openBrowser(
url = manga.publicUrl,
source = manga.source,
title = manga.title,
)
fun openColorFilterConfig(manga: Manga, page: MangaPage) { fun openColorFilterConfig(manga: Manga, page: MangaPage) {
startActivity( startActivity(
Intent(contextOrNull(), ColorFilterConfigActivity::class.java) Intent(contextOrNull(), ColorFilterConfigActivity::class.java)

@ -138,6 +138,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false) get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false)
set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_PAGES, value) } set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_PAGES, value) }
var isReaderDoubleOnFoldable: Boolean
get() = prefs.getBoolean(KEY_READER_DOUBLE_FOLDABLE, false)
set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_FOLDABLE, value) }
@get:FloatRange(0.0, 1.0) @get:FloatRange(0.0, 1.0)
var readerDoublePagesSensitivity: Float var readerDoublePagesSensitivity: Float
get() = prefs.getFloat(KEY_READER_DOUBLE_PAGES_SENSITIVITY, 0.5f) get() = prefs.getFloat(KEY_READER_DOUBLE_PAGES_SENSITIVITY, 0.5f)
@ -546,11 +550,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isPeriodicalBackupEnabled: Boolean val isPeriodicalBackupEnabled: Boolean
get() = prefs.getBoolean(KEY_BACKUP_PERIODICAL_ENABLED, false) get() = prefs.getBoolean(KEY_BACKUP_PERIODICAL_ENABLED, false)
val periodicalBackupFrequency: Long val periodicalBackupFrequency: Float
get() = prefs.getString(KEY_BACKUP_PERIODICAL_FREQUENCY, null)?.toLongOrNull() ?: 7L get() = prefs.getString(KEY_BACKUP_PERIODICAL_FREQUENCY, null)?.toFloatOrNull() ?: 7f
val periodicalBackupFrequencyMillis: Long val periodicalBackupFrequencyMillis: Long
get() = TimeUnit.DAYS.toMillis(periodicalBackupFrequency) get() = (TimeUnit.DAYS.toMillis(1) * periodicalBackupFrequency).toLong()
val periodicalBackupMaxCount: Int val periodicalBackupMaxCount: Int
get() = if (prefs.getBoolean(KEY_BACKUP_PERIODICAL_TRIM, true)) { get() = if (prefs.getBoolean(KEY_BACKUP_PERIODICAL_TRIM, true)) {
@ -681,7 +685,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_REMOTE_SOURCES = "remote_sources" const val KEY_REMOTE_SOURCES = "remote_sources"
const val KEY_LOCAL_STORAGE = "local_storage" const val KEY_LOCAL_STORAGE = "local_storage"
const val KEY_READER_DOUBLE_PAGES = "reader_double_pages" const val KEY_READER_DOUBLE_PAGES = "reader_double_pages"
const val KEY_READER_DOUBLE_PAGES_SENSITIVITY = "reader_double_pages_sensitivity" const val KEY_READER_DOUBLE_PAGES_SENSITIVITY = "reader_double_pages_sensitivity_2"
const val KEY_READER_DOUBLE_FOLDABLE = "reader_double_foldable"
const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons" const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons"
const val KEY_READER_CONTROL_LTR = "reader_taps_ltr" const val KEY_READER_CONTROL_LTR = "reader_taps_ltr"
const val KEY_READER_NAVIGATION_INVERTED = "reader_navigation_inverted" const val KEY_READER_NAVIGATION_INVERTED = "reader_navigation_inverted"

@ -33,7 +33,6 @@ import androidx.appcompat.R as appcompatR
abstract class BaseActivity<B : ViewBinding> : abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(), AppCompatActivity(),
ExceptionResolver.Host,
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener,
ScreenshotPolicyHelper.ContentContainer { ScreenshotPolicyHelper.ContentContainer {
@ -87,10 +86,6 @@ abstract class BaseActivity<B : ViewBinding> :
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR) @Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
override fun setContentView(view: View?) = throw UnsupportedOperationException() override fun setContentView(view: View?) = throw UnsupportedOperationException()
override fun getContext() = this
override fun getChildFragmentManager(): FragmentManager = supportFragmentManager
protected fun setContentView(binding: B) { protected fun setContentView(binding: B) {
this.viewBinding = binding this.viewBinding = binding
super.setContentView(binding.root) super.setContentView(binding.root)

@ -15,8 +15,7 @@ import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
abstract class BaseFragment<B : ViewBinding> : abstract class BaseFragment<B : ViewBinding> :
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener,
Fragment(), Fragment() {
ExceptionResolver.Host {
var viewBinding: B? = null var viewBinding: B? = null
private set private set

@ -36,8 +36,7 @@ import com.google.android.material.R as materialR
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener,
RecyclerViewOwner, RecyclerViewOwner {
ExceptionResolver.Host {
protected lateinit var exceptionResolver: ExceptionResolver protected lateinit var exceptionResolver: ExceptionResolver
private set private set

@ -5,7 +5,10 @@ import android.view.View
import androidx.annotation.Px import androidx.annotation.Px
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDecoration() { class SpacingItemDecoration(
@Px private val spacing: Int,
private val withBottomPadding: Boolean,
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets( override fun getItemOffsets(
outRect: Rect, outRect: Rect,
@ -13,6 +16,6 @@ class SpacingItemDecoration(@Px private val spacing: Int) : RecyclerView.ItemDec
parent: RecyclerView, parent: RecyclerView,
state: RecyclerView.State, state: RecyclerView.State,
) { ) {
outRect.set(spacing, spacing, spacing, spacing) outRect.set(spacing, spacing, spacing, if (withBottomPadding) spacing else 0)
} }
} }

@ -32,8 +32,7 @@ import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment(), abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment(),
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener {
ExceptionResolver.Host {
private var waitingForDismissAllowingStateLoss = false private var waitingForDismissAllowingStateLoss = false
private var isFitToContentsDisabled = false private var isFitToContentsDisabled = false

@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.exceptions.CaughtException
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.EmptyMangaException
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
@ -103,6 +104,7 @@ private fun Throwable.getDisplayMessageOrNull(resources: Resources): String? = w
is AccessDeniedException -> resources.getString(R.string.no_access_to_file) is AccessDeniedException -> resources.getString(R.string.no_access_to_file)
is NonFileUriException -> resources.getString(R.string.error_non_file_uri) is NonFileUriException -> resources.getString(R.string.error_non_file_uri)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty) is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is EmptyMangaException -> reason?.let { resources.getString(it.msgResId) } ?: cause?.getDisplayMessage(resources)
is ProxyConfigException -> resources.getString(R.string.invalid_proxy_configuration) is ProxyConfigException -> resources.getString(R.string.invalid_proxy_configuration)
is SyncApiException, is SyncApiException,
is ContentUnavailableException -> message is ContentUnavailableException -> message
@ -167,6 +169,8 @@ fun Throwable.getCauseUrl(): String? = when (this) {
is CloudFlareProtectedException -> url is CloudFlareProtectedException -> url
is InteractiveActionRequiredException -> url is InteractiveActionRequiredException -> url
is HttpStatusException -> url is HttpStatusException -> url
is UnsupportedSourceException -> manga?.publicUrl?.takeIf { it.isHttpUrl() }
is EmptyMangaException -> manga.publicUrl.takeIf { it.isHttpUrl() }
is HttpException -> (response.delegate as? Response)?.request?.url?.toString() is HttpException -> (response.delegate as? Response)?.request?.url?.toString()
else -> null else -> null
} }

@ -7,6 +7,7 @@ import org.koitharu.kotatsu.core.ui.model.MangaOverride
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.nullIfEmpty import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.data.filterChapters import org.koitharu.kotatsu.reader.data.filterChapters
@ -50,6 +51,9 @@ data class MangaDetails(
.ifNullOrEmpty { localManga?.manga?.coverUrl } .ifNullOrEmpty { localManga?.manga?.coverUrl }
?.nullIfEmpty() ?.nullIfEmpty()
val isRestricted: Boolean
get() = manga.state == MangaState.RESTRICTED
private val mergedManga by lazy { private val mergedManga by lazy {
if (localManga == null) { if (localManga == null) {
// fast path // fast path

@ -100,7 +100,7 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(),
override fun onStateChanged(sheet: View, newState: Int) { override fun onStateChanged(sheet: View, newState: Int) {
val binding = viewBinding ?: return val binding = viewBinding ?: return
binding.layoutTouchBlock.isTouchEventsAllowed = newState != STATE_COLLAPSED binding.layoutTouchBlock.isTouchEventsAllowed = dialog != null || newState != STATE_COLLAPSED
if (newState == STATE_DRAGGING || newState == STATE_SETTLING) { if (newState == STATE_DRAGGING || newState == STATE_SETTLING) {
return return
} }

@ -53,7 +53,9 @@ class MangaSourcesRepository @Inject constructor(
get() = db.getSourcesDao() get() = db.getSourcesDao()
val allMangaSources: Set<MangaParserSource> = Collections.unmodifiableSet( val allMangaSources: Set<MangaParserSource> = Collections.unmodifiableSet(
EnumSet.allOf(MangaParserSource::class.java) EnumSet.noneOf<MangaParserSource>(MangaParserSource::class.java).also {
MangaParserSource.entries.filterNotTo(it, MangaParserSource::isBroken)
}
) )
suspend fun getEnabledSources(): List<MangaSource> { suspend fun getEnabledSources(): List<MangaSource> {

@ -57,10 +57,16 @@ class SavedFiltersRepository @Inject constructor(
source = source, source = source,
filter = filter, filter = filter,
) )
persist(source, persistableFilter) persist(persistableFilter)
persistableFilter persistableFilter
} }
suspend fun save(
filter: PersistableFilter,
) = withContext(Dispatchers.Default) {
persist(filter)
}
suspend fun rename(source: MangaSource, id: Int, newName: String) = withContext(Dispatchers.Default) { suspend fun rename(source: MangaSource, id: Int, newName: String) = withContext(Dispatchers.Default) {
val filter = load(source, id) ?: return@withContext val filter = load(source, id) ?: return@withContext
val newFilter = filter.copy(name = newName) val newFilter = filter.copy(name = newName)
@ -79,8 +85,8 @@ class SavedFiltersRepository @Inject constructor(
} }
} }
private fun persist(source: MangaSource, persistableFilter: PersistableFilter) { private fun persist(persistableFilter: PersistableFilter) {
val prefs = getPrefs(source) val prefs = getPrefs(persistableFilter.source)
val json = Json.encodeToString(persistableFilter) val json = Json.encodeToString(persistableFilter)
prefs.edit(commit = true) { prefs.edit(commit = true) {
putString(key(persistableFilter.id), json) putString(key(persistableFilter.id), json)

@ -2,16 +2,19 @@ package org.koitharu.kotatsu.filter.ui.sheet
import android.os.Bundle import android.os.Bundle
import android.text.InputFilter import android.text.InputFilter
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
@ -69,8 +72,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
if (dialog == null) { if (dialog == null) {
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom) binding.adjustForEmbeddedLayout()
binding.scrollView.scrollIndicators = 0
} }
val filter = FilterCoordinator.require(this) val filter = FilterCoordinator.require(this)
filter.sortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged) filter.sortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
@ -127,6 +129,18 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
binding.buttonDone.setOnClickListener(this) binding.buttonDone.setOnClickListener(this)
} }
private fun SheetFilterBinding.adjustForEmbeddedLayout() {
layoutBody.updatePadding(top = layoutBody.paddingBottom)
scrollView.scrollIndicators = 0
buttonDone.isVisible = false
this.root.layoutParams?.height = ViewGroup.LayoutParams.MATCH_PARENT
buttonSave.updateLayoutParams<LinearLayout.LayoutParams> {
weight = 0f
width = LinearLayout.LayoutParams.WRAP_CONTENT
gravity = Gravity.END or Gravity.CENTER_VERTICAL
}
}
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val typeMask = WindowInsetsCompat.Type.systemBars() val typeMask = WindowInsetsCompat.Type.systemBars()
viewBinding?.layoutBottom?.updateLayoutParams<ViewGroup.MarginLayoutParams> { viewBinding?.layoutBottom?.updateLayoutParams<ViewGroup.MarginLayoutParams> {

@ -13,11 +13,11 @@ import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toFile import androidx.core.net.toFile
import dagger.Reusable import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Cache import okhttp3.Cache
import org.koitharu.kotatsu.core.LocalizedAppContext
import org.koitharu.kotatsu.core.exceptions.NonFileUriException import org.koitharu.kotatsu.core.exceptions.NonFileUriException
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.computeSize
@ -39,7 +39,7 @@ private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB
@Reusable @Reusable
class LocalStorageManager @Inject constructor( class LocalStorageManager @Inject constructor(
@ApplicationContext private val context: Context, @LocalizedAppContext private val context: Context,
private val settings: AppSettings, private val settings: AppSettings,
) { ) {

@ -51,6 +51,7 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
binding.chipsType.onChipClickListener = this binding.chipsType.onChipClickListener = this
binding.chipBackup.setOnClickListener(this) binding.chipBackup.setOnClickListener(this)
binding.chipSync.setOnClickListener(this) binding.chipSync.setOnClickListener(this)
binding.chipDirectories.setOnClickListener(this)
viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged) viewModel.locales.observe(viewLifecycleOwner, ::onLocalesChanged)
viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged) viewModel.types.observe(viewLifecycleOwner, ::onTypesChanged)
@ -86,6 +87,10 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
val accountType = getString(R.string.account_type_sync) val accountType = getString(R.string.account_type_sync)
am.addAccount(accountType, accountType, null, null, requireActivity(), null, null) am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
} }
R.id.chip_directories -> {
router.openDirectoriesSettings()
}
} }
} }

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui
import android.app.assist.AssistContent import android.app.assist.AssistContent
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
@ -24,6 +25,8 @@ import androidx.transition.Fade
import androidx.transition.Slide import androidx.transition.Slide
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import androidx.transition.TransitionSet import androidx.transition.TransitionSet
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -31,7 +34,9 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@ -110,6 +115,9 @@ class ReaderActivity :
private lateinit var readerManager: ReaderManager private lateinit var readerManager: ReaderManager
private val hideUiRunnable = Runnable { setUiIsVisible(false) } private val hideUiRunnable = Runnable { setUiIsVisible(false) }
// Tracks whether the foldable device is in an unfolded state (half-opened or flat)
private var isFoldUnfolded: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityReaderBinding.inflate(layoutInflater)) setContentView(ActivityReaderBinding.inflate(layoutInflater))
@ -187,6 +195,11 @@ class ReaderActivity :
viewBinding.zoomControl.isVisible = it viewBinding.zoomControl.isVisible = it
} }
addMenuProvider(ReaderMenuProvider(viewModel)) addMenuProvider(ReaderMenuProvider(viewModel))
observeWindowLayout()
// Apply initial double-mode considering foldable setting
applyDoubleModeAuto()
} }
override fun getParentActivityIntent(): Intent? { override fun getParentActivityIntent(): Intent? {
@ -341,7 +354,17 @@ class ReaderActivity :
} }
override fun onDoubleModeChanged(isEnabled: Boolean) { override fun onDoubleModeChanged(isEnabled: Boolean) {
readerManager.setDoubleReaderMode(isEnabled) // Combine manual toggle with foldable auto setting
applyDoubleModeAuto(isEnabled)
}
private fun applyDoubleModeAuto(manualEnabled: Boolean? = null) {
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
// Auto double-page on foldable when device is unfolded (half-opened or flat)
val autoFoldable = settings.isReaderDoubleOnFoldable && isFoldUnfolded
val manualLandscape = (manualEnabled ?: settings.isReaderDoubleOnLandscape) && isLandscape
val autoEnabled = autoFoldable || manualLandscape
readerManager.setDoubleReaderMode(autoEnabled)
} }
private fun setKeepScreenOn(isKeep: Boolean) { private fun setKeepScreenOn(isKeep: Boolean) {
@ -521,6 +544,24 @@ class ReaderActivity :
} }
} }
// Observe foldable window layout to auto-enable double-page if configured
private fun observeWindowLayout() {
WindowInfoTracker.getOrCreate(this)
.windowLayoutInfo(this)
.onEach { info ->
val fold = info.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
val unfolded = when (fold?.state) {
FoldingFeature.State.HALF_OPENED, FoldingFeature.State.FLAT -> true
else -> false
}
if (unfolded != isFoldUnfolded) {
isFoldUnfolded = unfolded
applyDoubleModeAuto()
}
}
.launchIn(lifecycleScope)
}
private fun askForIncognitoMode() { private fun askForIncognitoMode() {
buildAlertDialog(this, isCentered = true) { buildAlertDialog(this, isCentered = true) {
var dontAskAgain = false var dontAskAgain = false

@ -49,7 +49,7 @@ class ReaderManager(
fun setDoubleReaderMode(isEnabled: Boolean) { fun setDoubleReaderMode(isEnabled: Boolean) {
val mode = currentMode val mode = currentMode
val prevReader = currentReader?.javaClass val prevReader = currentReader?.javaClass
invalidateTypesMap(isEnabled && isLandscape()) invalidateTypesMap(isEnabled)
val newReader = modeMap[mode] val newReader = modeMap[mode]
if (mode != null && newReader != prevReader) { if (mode != null && newReader != prevReader) {
replace(mode) replace(mode)

@ -29,6 +29,7 @@ import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.exceptions.EmptyMangaException
import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.nav.MangaIntent import org.koitharu.kotatsu.core.nav.MangaIntent
import org.koitharu.kotatsu.core.nav.ReaderIntent import org.koitharu.kotatsu.core.nav.ReaderIntent
@ -47,6 +48,7 @@ import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsInteractor import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.details.ui.pager.EmptyMangaReason
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase import org.koitharu.kotatsu.history.domain.HistoryUpdateUseCase
@ -405,9 +407,11 @@ class ReaderViewModel @Inject constructor(
private fun loadImpl() { private fun loadImpl() {
loadingJob = launchLoadingJob(Dispatchers.Default + EventExceptionHandler(onLoadingError)) { loadingJob = launchLoadingJob(Dispatchers.Default + EventExceptionHandler(onLoadingError)) {
var exception: Exception? = null var exception: Exception? = null
var loadedDetails: MangaDetails? = null
try { try {
detailsLoadUseCase(intent, force = false) detailsLoadUseCase(intent, force = false)
.collect { details -> .collect { details ->
loadedDetails = details
if (mangaDetails.value == null) { if (mangaDetails.value == null) {
mangaDetails.value = details mangaDetails.value = details
} }
@ -452,9 +456,28 @@ class ReaderViewModel @Inject constructor(
exception = e.mergeWith(exception) exception = e.mergeWith(exception)
} }
if (readingState.value == null) { if (readingState.value == null) {
onLoadingError.call( val loadedManga = loadedDetails // for smart cast
exception ?: IllegalStateException("Unable to load manga. This should never happen. Please report"), if (loadedManga != null) {
mangaDetails.value = loadedManga.filterChapters(selectedBranch.value)
}
val loadingError = when {
exception != null -> exception
loadedManga == null || !loadedManga.isLoaded -> null
loadedManga.isRestricted -> EmptyMangaException(
EmptyMangaReason.RESTRICTED,
loadedManga.toManga(),
null,
) )
loadedManga.allChapters.isEmpty() -> EmptyMangaException(
EmptyMangaReason.NO_CHAPTERS,
loadedManga.toManga(),
null,
)
else -> null
} ?: IllegalStateException("Unable to load manga. This should never happen. Please report")
onLoadingError.call(loadingError)
} else exception?.let { e -> } else exception?.let { e ->
// manga has been loaded but error occurred // manga has been loaded but error occurred
errorEvent.call(e) errorEvent.call(e)

@ -10,6 +10,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.transition.TransitionManager
import com.google.android.material.button.MaterialButtonToggleGroup import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -90,13 +91,11 @@ class ReaderConfigSheet :
binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL binding.buttonVertical.isChecked = mode == ReaderMode.VERTICAL
binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape binding.switchDoubleReader.isChecked = settings.isReaderDoubleOnLandscape
binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED binding.switchDoubleReader.isEnabled = mode == ReaderMode.STANDARD || mode == ReaderMode.REVERSED
binding.switchPullGesture.isChecked = settings.isWebtoonPullGestureEnabled binding.switchDoubleFoldable.isChecked = settings.isReaderDoubleOnFoldable
binding.switchPullGesture.isEnabled = mode == ReaderMode.WEBTOON binding.switchDoubleFoldable.isEnabled = binding.switchDoubleReader.isEnabled
binding.sliderDoubleSensitivity.setValueRounded(settings.readerDoublePagesSensitivity * 100f)
binding.textSensitivity.isVisible = settings.isReaderDoubleOnLandscape binding.sliderDoubleSensitivity.setLabelFormatter(IntPercentLabelFormatter(binding.root.context))
binding.seekbarSensitivity.isVisible = settings.isReaderDoubleOnLandscape binding.adjustSensitivitySlider(withAnimation = false)
binding.seekbarSensitivity.setValueRounded(settings.readerDoublePagesSensitivity * 100f)
binding.seekbarSensitivity.setLabelFormatter(IntPercentLabelFormatter(binding.root.context))
binding.checkableGroup.addOnButtonCheckedListener(this) binding.checkableGroup.addOnButtonCheckedListener(this)
binding.buttonSavePage.setOnClickListener(this) binding.buttonSavePage.setOnClickListener(this)
@ -107,8 +106,8 @@ class ReaderConfigSheet :
binding.buttonScrollTimer.setOnClickListener(this) binding.buttonScrollTimer.setOnClickListener(this)
binding.buttonBookmark.setOnClickListener(this) binding.buttonBookmark.setOnClickListener(this)
binding.switchDoubleReader.setOnCheckedChangeListener(this) binding.switchDoubleReader.setOnCheckedChangeListener(this)
binding.switchPullGesture.setOnCheckedChangeListener(this) binding.switchDoubleFoldable.setOnCheckedChangeListener(this)
binding.seekbarSensitivity.addOnChangeListener(this) binding.sliderDoubleSensitivity.addOnChangeListener(this)
viewModel.isBookmarkAdded.observe(viewLifecycleOwner) { viewModel.isBookmarkAdded.observe(viewLifecycleOwner) {
binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add) binding.buttonBookmark.setText(if (it) R.string.bookmark_remove else R.string.bookmark_add)
@ -183,13 +182,14 @@ class ReaderConfigSheet :
R.id.switch_double_reader -> { R.id.switch_double_reader -> {
settings.isReaderDoubleOnLandscape = isChecked settings.isReaderDoubleOnLandscape = isChecked
viewBinding?.textSensitivity?.isVisible = isChecked viewBinding?.adjustSensitivitySlider(withAnimation = true)
viewBinding?.seekbarSensitivity?.isVisible = isChecked
findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked) findParentCallback(Callback::class.java)?.onDoubleModeChanged(isChecked)
} }
R.id.switch_pull_gesture -> { R.id.switch_double_foldable -> {
settings.isWebtoonPullGestureEnabled = isChecked settings.isReaderDoubleOnFoldable = isChecked
// Re-evaluate double-page considering foldable state and current manual toggle
findParentCallback(Callback::class.java)?.onDoubleModeChanged(settings.isReaderDoubleOnLandscape)
} }
} }
} }
@ -213,8 +213,11 @@ class ReaderConfigSheet :
R.id.button_vertical -> ReaderMode.VERTICAL R.id.button_vertical -> ReaderMode.VERTICAL
else -> return else -> return
} }
viewBinding?.switchDoubleReader?.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED viewBinding?.run {
viewBinding?.switchPullGesture?.isEnabled = newMode == ReaderMode.WEBTOON switchDoubleReader.isEnabled = newMode == ReaderMode.STANDARD || newMode == ReaderMode.REVERSED
switchDoubleFoldable.isEnabled = switchDoubleReader.isEnabled
adjustSensitivitySlider(withAnimation = true)
}
if (newMode == mode) { if (newMode == mode) {
return return
} }
@ -248,6 +251,21 @@ class ReaderConfigSheet :
) )
} }
private fun SheetReaderConfigBinding.adjustSensitivitySlider(withAnimation: Boolean) {
val isSubOptionsVisible = switchDoubleReader.isEnabled && switchDoubleReader.isChecked
val needTransition = withAnimation && (
(isSubOptionsVisible != sliderDoubleSensitivity.isVisible) ||
(isSubOptionsVisible != textDoubleSensitivity.isVisible) ||
(isSubOptionsVisible != switchDoubleFoldable.isVisible)
)
if (needTransition) {
TransitionManager.beginDelayedTransition(layoutMain)
}
sliderDoubleSensitivity.isVisible = isSubOptionsVisible
textDoubleSensitivity.isVisible = isSubOptionsVisible
switchDoubleFoldable.isVisible = isSubOptionsVisible
}
interface Callback { interface Callback {
fun onReaderModeChanged(mode: ReaderMode) fun onReaderModeChanged(mode: ReaderMode)

@ -25,11 +25,26 @@ abstract class BaseReaderFragment<B : ViewBinding> : BaseFragment<B>(), ZoomCont
readerAdapter = onCreateAdapter() readerAdapter = onCreateAdapter()
viewModel.content.observe(viewLifecycleOwner) { viewModel.content.observe(viewLifecycleOwner) {
if (it.state == null && it.pages.isNotEmpty() && readerAdapter?.hasItems != true) { // Determine which state to use for restoring position:
onPagesChanged(it.pages, viewModel.getCurrentState()) // - content.state: explicitly set state (e.g., after mode switch or chapter change)
} else { // - getCurrentState(): current reading position saved in SavedStateHandle
onPagesChanged(it.pages, it.state) val currentState = viewModel.getCurrentState()
val pendingState = when {
// If content.state is null and we have pages, use getCurrentState
it.state == null
&& it.pages.isNotEmpty()
&& readerAdapter?.hasItems != true -> currentState
// use currentState only if it matches the current pages (to avoid the error message)
readerAdapter?.hasItems != true
&& it.state != currentState
&& currentState != null
&& it.pages.any { page -> page.chapterId == currentState.chapterId } -> currentState
// Otherwise, use content.state (normal flow, mode switch, chapter change)
else -> it.state
} }
onPagesChanged(it.pages, pendingState)
} }
} }

@ -94,7 +94,7 @@ class SearchActivity :
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false) setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
supportActionBar?.setSubtitle(R.string.search_results) supportActionBar?.setSubtitle(R.string.search_results)
addMenuProvider(SearchKindMenuProvider(this, viewModel.query, viewModel.kind)) addMenuProvider(SearchMenuProvider(this, viewModel))
viewModel.list.observe(this, adapter) viewModel.list.observe(this, adapter)
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))

@ -9,10 +9,9 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.router import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.search.domain.SearchKind import org.koitharu.kotatsu.search.domain.SearchKind
class SearchKindMenuProvider( class SearchMenuProvider(
private val activity: SearchActivity, private val activity: SearchActivity,
private val query: String, private val viewModel: SearchViewModel,
private val kind: SearchKind
) : MenuProvider { ) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
@ -22,7 +21,7 @@ class SearchKindMenuProvider(
override fun onPrepareMenu(menu: Menu) { override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu) super.onPrepareMenu(menu)
menu.findItem( menu.findItem(
when (kind) { when (viewModel.kind) {
SearchKind.SIMPLE -> R.id.action_kind_simple SearchKind.SIMPLE -> R.id.action_kind_simple
SearchKind.TITLE -> R.id.action_kind_title SearchKind.TITLE -> R.id.action_kind_title
SearchKind.AUTHOR -> R.id.action_kind_author SearchKind.AUTHOR -> R.id.action_kind_author
@ -32,6 +31,20 @@ class SearchKindMenuProvider(
} }
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.action_filter_pinned_only -> {
menuItem.isChecked = !menuItem.isChecked
viewModel.setPinnedOnly(menuItem.isChecked)
return true
}
R.id.action_filter_hide_empty -> {
menuItem.isChecked = !menuItem.isChecked
viewModel.setHideEmpty(menuItem.isChecked)
return true
}
}
val newKind = when (menuItem.itemId) { val newKind = when (menuItem.itemId) {
R.id.action_kind_simple -> SearchKind.SIMPLE R.id.action_kind_simple -> SearchKind.SIMPLE
R.id.action_kind_title -> SearchKind.TITLE R.id.action_kind_title -> SearchKind.TITLE
@ -39,9 +52,9 @@ class SearchKindMenuProvider(
R.id.action_kind_tag -> SearchKind.TAG R.id.action_kind_tag -> SearchKind.TAG
else -> return false else -> return false
} }
if (newKind != kind) { if (newKind != viewModel.kind) {
activity.router.openSearch( activity.router.openSearch(
query = query, query = viewModel.query,
kind = newKind, kind = newKind,
) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

@ -62,6 +62,8 @@ class SearchViewModel @Inject constructor(
val kind = savedStateHandle.get<SearchKind>(AppRouter.KEY_KIND) ?: SearchKind.SIMPLE val kind = savedStateHandle.get<SearchKind>(AppRouter.KEY_KIND) ?: SearchKind.SIMPLE
private var includeDisabledSources = MutableStateFlow(false) private var includeDisabledSources = MutableStateFlow(false)
private var pinnedOnly = MutableStateFlow(false)
private var hideEmpty = MutableStateFlow(false)
private val results = MutableStateFlow<List<SearchResultsListModel>>(emptyList()) private val results = MutableStateFlow<List<SearchResultsListModel>>(emptyList())
private var searchJob: Job? = null private var searchJob: Job? = null
@ -70,9 +72,15 @@ class SearchViewModel @Inject constructor(
results, results,
isLoading.dropWhile { !it }, isLoading.dropWhile { !it },
includeDisabledSources, includeDisabledSources,
) { list, loading, includeDisabled -> hideEmpty,
) { list, loading, includeDisabled, hideEmptyVal ->
val filteredList = if (hideEmptyVal) {
list.filter { it.list.isNotEmpty() }
} else {
list
}
when { when {
list.isEmpty() -> listOf( filteredList.isEmpty() -> listOf(
when { when {
loading -> LoadingState loading -> LoadingState
else -> EmptyState( else -> EmptyState(
@ -84,9 +92,9 @@ class SearchViewModel @Inject constructor(
}, },
) )
loading -> list + LoadingFooter() loading -> filteredList + LoadingFooter()
includeDisabled -> list includeDisabled -> filteredList
else -> list + ButtonFooter(R.string.search_disabled_sources) else -> filteredList + ButtonFooter(R.string.search_disabled_sources)
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
@ -114,6 +122,17 @@ class SearchViewModel @Inject constructor(
doSearch() doSearch()
} }
fun setPinnedOnly(value: Boolean) {
if (pinnedOnly.value != value) {
pinnedOnly.value = value
retry()
}
}
fun setHideEmpty(value: Boolean) {
hideEmpty.value = value
}
fun continueSearch() { fun continueSearch() {
if (includeDisabledSources.value) { if (includeDisabledSources.value) {
return return
@ -122,8 +141,12 @@ class SearchViewModel @Inject constructor(
searchJob = launchLoadingJob(Dispatchers.Default) { searchJob = launchLoadingJob(Dispatchers.Default) {
includeDisabledSources.value = true includeDisabledSources.value = true
prevJob?.join() prevJob?.join()
val sources = sourcesRepository.getDisabledSources() val sources = if (pinnedOnly.value) {
emptyList()
} else {
sourcesRepository.getDisabledSources()
.sortedByDescending { it.priority() } .sortedByDescending { it.priority() }
}
val semaphore = Semaphore(MAX_PARALLELISM) val semaphore = Semaphore(MAX_PARALLELISM)
sources.map { source -> sources.map { source ->
launch { launch {
@ -142,7 +165,11 @@ class SearchViewModel @Inject constructor(
appendResult(searchHistory()) appendResult(searchHistory())
appendResult(searchFavorites()) appendResult(searchFavorites())
appendResult(searchLocal()) appendResult(searchLocal())
val sources = sourcesRepository.getEnabledSources() val sources = if (pinnedOnly.value) {
sourcesRepository.getPinnedSources().toList()
} else {
sourcesRepository.getEnabledSources()
}
val semaphore = Semaphore(MAX_PARALLELISM) val semaphore = Semaphore(MAX_PARALLELISM)
sources.map { source -> sources.map { source ->
launch { launch {

@ -37,7 +37,7 @@ fun searchResultsAD(
binding.recyclerView.addItemDecoration(selectionDecoration) binding.recyclerView.addItemDecoration(selectionDecoration)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer) val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing)) binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing, withBottomPadding = true))
val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener) val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener)
binding.buttonMore.setOnClickListener(eventListener) binding.buttonMore.setOnClickListener(eventListener)

@ -29,7 +29,7 @@ fun searchSuggestionMangaListAD(
left = recyclerView.paddingLeft - spacing, left = recyclerView.paddingLeft - spacing,
right = recyclerView.paddingRight - spacing, right = recyclerView.paddingRight - spacing,
) )
recyclerView.addItemDecoration(SpacingItemDecoration(spacing)) recyclerView.addItemDecoration(SpacingItemDecoration(spacing, withBottomPadding = true))
val scrollResetCallback = RecyclerViewScrollCallback(recyclerView, 0, 0) val scrollResetCallback = RecyclerViewScrollCallback(recyclerView, 0, 0)
bind { bind {

@ -1,38 +1,66 @@
package org.koitharu.kotatsu.settings.storage.directories package org.koitharu.kotatsu.settings.storage.directories
import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.isGone
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.setTooltipCompat import org.koitharu.kotatsu.core.util.ext.setTooltipCompat
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemStorageConfigBinding import org.koitharu.kotatsu.databinding.ItemStorageConfig2Binding
import org.koitharu.kotatsu.settings.storage.DirectoryModel
fun directoryConfigAD( fun directoryConfigAD(
clickListener: OnListItemClickListener<DirectoryModel>, clickListener: OnListItemClickListener<DirectoryConfigModel>,
) = adapterDelegateViewBinding<DirectoryModel, DirectoryModel, ItemStorageConfigBinding>( ) = adapterDelegateViewBinding<DirectoryConfigModel, DirectoryConfigModel, ItemStorageConfig2Binding>(
{ layoutInflater, parent -> ItemStorageConfigBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent -> ItemStorageConfig2Binding.inflate(layoutInflater, parent, false) },
) { ) {
binding.buttonRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) } binding.buttonRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) }
binding.buttonRemove.setTooltipCompat(binding.buttonRemove.contentDescription) binding.buttonRemove.setTooltipCompat(binding.buttonRemove.contentDescription)
bind { bind {
binding.textViewTitle.text = item.title ?: getString(item.titleRes) binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.file?.absolutePath binding.textViewSubtitle.text = item.path.absolutePath
binding.buttonRemove.isVisible = item.isRemovable binding.buttonRemove.isGone = item.isAppPrivate
binding.buttonRemove.isEnabled = !item.isChecked binding.buttonRemove.isEnabled = !item.isDefault
binding.textViewTitle.drawableStart = if (!item.isAvailable) { binding.spacer.visibility = if (item.isAppPrivate) {
ContextCompat.getDrawable(context, R.drawable.ic_alert_outline)?.apply { View.INVISIBLE
setTint(ContextCompat.getColor(context, R.color.warning))
}
} else if (item.isChecked) {
ContextCompat.getDrawable(context, R.drawable.ic_download)
} else { } else {
null View.GONE
} }
binding.textViewInfo.textAndVisible = buildSpannedString {
if (item.isDefault) {
bold {
append(getString(R.string.download_default_directory))
}
}
if (!item.isAccessible) {
if (isNotEmpty()) appendLine()
color(
context.getThemeColor(
androidx.appcompat.R.attr.colorError,
ContextCompat.getColor(context, R.color.common_red),
),
) {
append(getString(R.string.no_write_permission_to_file))
}
}
if (item.isAppPrivate) {
if (isNotEmpty()) appendLine()
append(getString(R.string.private_app_directory_warning))
}
}
binding.indicatorSize.max = FileSize.BYTES.convert(item.available, FileSize.KILOBYTES).toInt()
binding.indicatorSize.progress = FileSize.BYTES.convert(item.size, FileSize.KILOBYTES).toInt()
binding.textViewSize.text = context.getString(
R.string.available_pattern,
FileSize.BYTES.format(context, item.available),
)
} }
} }

@ -0,0 +1,22 @@
package org.koitharu.kotatsu.settings.storage.directories
import androidx.recyclerview.widget.DiffUtil.ItemCallback
class DirectoryConfigDiffCallback : ItemCallback<DirectoryConfigModel>() {
override fun areItemsTheSame(oldItem: DirectoryConfigModel, newItem: DirectoryConfigModel): Boolean {
return oldItem.path == newItem.path
}
override fun areContentsTheSame(oldItem: DirectoryConfigModel, newItem: DirectoryConfigModel): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: DirectoryConfigModel, newItem: DirectoryConfigModel): Any? {
return if (oldItem.isDefault != newItem.isDefault) {
Unit
} else {
super.getChangePayload(oldItem, newItem)
}
}
}

@ -0,0 +1,19 @@
package org.koitharu.kotatsu.settings.storage.directories
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.io.File
data class DirectoryConfigModel(
val title: String,
val path: File,
val isDefault: Boolean,
val isAppPrivate: Boolean,
val isAccessible: Boolean,
val size: Long,
val available: Long,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is DirectoryConfigModel && path == other.path
}
}

@ -20,18 +20,17 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper import org.koitharu.kotatsu.core.os.OpenDocumentTreeHelper
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.ActivityMangaDirectoriesBinding import org.koitharu.kotatsu.databinding.ActivityMangaDirectoriesBinding
import org.koitharu.kotatsu.settings.storage.DirectoryDiffCallback
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract import org.koitharu.kotatsu.settings.storage.RequestStorageManagerPermissionContract
@AndroidEntryPoint @AndroidEntryPoint
class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>(), class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>(),
OnListItemClickListener<DirectoryModel>, View.OnClickListener { OnListItemClickListener<DirectoryConfigModel>, View.OnClickListener {
private val viewModel: MangaDirectoriesViewModel by viewModels() private val viewModel: MangaDirectoriesViewModel by viewModels()
private val pickFileTreeLauncher = OpenDocumentTreeHelper( private val pickFileTreeLauncher = OpenDocumentTreeHelper(
@ -63,8 +62,10 @@ class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityMangaDirectoriesBinding.inflate(layoutInflater)) setContentView(ActivityMangaDirectoriesBinding.inflate(layoutInflater))
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false) setDisplayHomeAsUp(isEnabled = true, showUpAsClose = false)
val adapter = AsyncListDifferDelegationAdapter(DirectoryDiffCallback(), directoryConfigAD(this)) val adapter = AsyncListDifferDelegationAdapter(DirectoryConfigDiffCallback(), directoryConfigAD(this))
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing_large)
viewBinding.recyclerView.adapter = adapter viewBinding.recyclerView.adapter = adapter
viewBinding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing, withBottomPadding = false))
viewBinding.fabAdd.setOnClickListener(this) viewBinding.fabAdd.setOnClickListener(this)
viewModel.items.observe(this) { adapter.items = it } viewModel.items.observe(this) { adapter.items = it }
viewModel.isLoading.observe(this) { viewBinding.progressBar.isVisible = it } viewModel.isLoading.observe(this) { viewBinding.progressBar.isVisible = it }
@ -76,8 +77,8 @@ class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>()
) )
} }
override fun onItemClick(item: DirectoryModel, view: View) { override fun onItemClick(item: DirectoryConfigModel, view: View) {
viewModel.onRemoveClick(item.file ?: return) viewModel.onRemoveClick(item.path)
} }
override fun onClick(v: View?) { override fun onClick(v: View?) {

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.settings.storage.directories package org.koitharu.kotatsu.settings.storage.directories
import android.net.Uri import android.net.Uri
import android.os.StatFs
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -8,10 +9,10 @@ import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.isReadable import org.koitharu.kotatsu.core.util.ext.isReadable
import org.koitharu.kotatsu.core.util.ext.isWriteable import org.koitharu.kotatsu.core.util.ext.isWriteable
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -21,7 +22,7 @@ class MangaDirectoriesViewModel @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel() { ) : BaseViewModel() {
val items = MutableStateFlow(emptyList<DirectoryModel>()) val items = MutableStateFlow(emptyList<DirectoryConfigModel>())
private var loadingJob: Job? = null private var loadingJob: Job? = null
init { init {
@ -64,26 +65,31 @@ class MangaDirectoriesViewModel @Inject constructor(
val customDirs = settings.userSpecifiedMangaDirectories - applicationDirs val customDirs = settings.userSpecifiedMangaDirectories - applicationDirs
items.value = buildList(applicationDirs.size + customDirs.size) { items.value = buildList(applicationDirs.size + customDirs.size) {
applicationDirs.mapTo(this) { dir -> applicationDirs.mapTo(this) { dir ->
DirectoryModel( dir.toDirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false), isDefault = dir == downloadDir,
titleRes = 0, isAppPrivate = true,
file = dir,
isChecked = dir == downloadDir,
isAvailable = dir.isReadable() && dir.isWriteable(),
isRemovable = false,
) )
} }
customDirs.mapTo(this) { dir -> customDirs.mapTo(this) { dir ->
DirectoryModel( dir.toDirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false), isDefault = dir == downloadDir,
titleRes = 0, isAppPrivate = false,
file = dir,
isChecked = dir == downloadDir,
isAvailable = dir.isReadable() && dir.isWriteable(),
isRemovable = true,
) )
} }
} }
} }
} }
private suspend fun File.toDirectoryModel(
isDefault: Boolean,
isAppPrivate: Boolean,
) = DirectoryConfigModel(
title = storageManager.getDirectoryDisplayName(this, isFullPath = false),
path = this,
isDefault = isDefault,
isAccessible = isReadable() && isWriteable(),
isAppPrivate = isAppPrivate,
size = computeSize(),
available = StatFs(this.absolutePath).availableBytes,
)
} }

@ -2,6 +2,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -27,9 +28,11 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="@dimen/list_spacing_large"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_storage_config2" />
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar" android:id="@+id/progressBar"

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
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"
tools:layout_margin="@dimen/screen_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/margin_small">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/screen_padding"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleMedium"
tools:text="@tools:sample/lorem[3]" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[5]" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/screen_padding"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/textView_size"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/margin_small"
android:layout_weight="1"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:textColorSecondary"
tools:text="250MB / 10GB" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/indicator_size"
android:layout_width="160dp"
android:layout_height="wrap_content"
app:trackCornerRadius="5dp"
app:trackThickness="10dp"
tools:progress="40" />
</LinearLayout>
<TextView
android:id="@+id/textView_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/screen_padding"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?android:textColorSecondary"
tools:text="Content will be removed within application" />
<Button
android:id="@+id/button_remove"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="@dimen/margin_small"
android:text="@string/remove" />
<View
android:id="@+id/spacer"
android:layout_width="match_parent"
android:layout_height="@dimen/margin_small"
tools:visibility="gone" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

@ -19,6 +19,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:clipToPadding="false" android:clipToPadding="false"
android:fillViewport="true"
android:scrollIndicators="top|bottom" android:scrollIndicators="top|bottom"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">

@ -21,6 +21,7 @@
android:scrollIndicators="top"> android:scrollIndicators="top">
<LinearLayout <LinearLayout
android:id="@+id/layout_main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
@ -129,8 +130,23 @@
android:textColor="?colorOnSurfaceVariant" android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_split_horizontal" /> app:drawableStartCompat="@drawable/ic_split_horizontal" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_double_foldable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:minHeight="?android:listPreferredItemHeightSmall"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/auto_double_foldable"
android:textAppearance="@style/TextAppearance.Kotatsu.GridTitle"
android:textColor="?colorOnSurfaceVariant"
android:visibility="gone"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/text_sensitivity" android:id="@+id/text_double_sensitivity"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal" android:layout_marginHorizontal="@dimen/margin_normal"
@ -139,7 +155,7 @@
android:textAppearance="@style/TextAppearance.Kotatsu.GridTitle" /> android:textAppearance="@style/TextAppearance.Kotatsu.GridTitle" />
<com.google.android.material.slider.Slider <com.google.android.material.slider.Slider
android:id="@+id/seekbar_sensitivity" android:id="@+id/slider_double_sensitivity"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_small" android:layout_marginHorizontal="@dimen/margin_small"
@ -149,20 +165,6 @@
app:labelBehavior="floating" app:labelBehavior="floating"
tools:value="50" /> tools:value="50" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_pull_gesture"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:minHeight="?android:listPreferredItemHeightSmall"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:text="@string/enable_pull_gesture_title"
android:textAppearance="?textAppearanceListItem"
android:textColor="?colorOnSurfaceVariant"
app:drawableStartCompat="@drawable/ic_gesture_vertical" />
<org.koitharu.kotatsu.core.ui.widgets.ListItemTextView <org.koitharu.kotatsu.core.ui.widgets.ListItemTextView
android:id="@+id/button_screen_rotate" android:id="@+id/button_screen_rotate"
android:layout_width="match_parent" android:layout_width="match_parent"

@ -64,6 +64,14 @@
android:text="@string/sync_auth" android:text="@string/sync_auth"
app:chipIcon="@drawable/ic_sync" /> app:chipIcon="@drawable/ic_sync" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_directories"
style="@style/Widget.Kotatsu.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/local_manga_directories"
app:chipIcon="@drawable/ic_storage" />
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
</HorizontalScrollView> </HorizontalScrollView>

@ -33,6 +33,20 @@
android:title="@string/genre" /> android:title="@string/genre" />
</group> </group>
<group android:id="@+id/group_search_filters">
<item
android:id="@+id/action_filter_pinned_only"
android:checkable="true"
android:title="@string/pinned_sources_only" />
<item
android:id="@+id/action_filter_hide_empty"
android:checkable="true"
android:title="@string/hide_empty_sources" />
</group>
</menu> </menu>
</item> </item>

@ -179,4 +179,43 @@
<string name="incognito_mode_hint">আপনার পড়ার অগ্রগতি সেভ হবে না</string> <string name="incognito_mode_hint">আপনার পড়ার অগ্রগতি সেভ হবে না</string>
<string name="volume_">আওয়াজ%d</string> <string name="volume_">আওয়াজ%d</string>
<string name="volume_unknown">অজানা ভলিউম</string> <string name="volume_unknown">অজানা ভলিউম</string>
<string name="suggested_queries">সাম্প্রতিক প্রশ্ন</string>
<string name="authors">লেখক</string>
<string name="blocked_by_server_message">আপনি সার্ভার দ্বারা অবরুদ্ধ করা হয়. একটি ভিন্ন নেটওয়ার্ক সংযোগ ব্যবহার করার চেষ্টা করুন (ভিপিএন, প্রক্সি, ইত্যাদি)</string>
<string name="disable">নিষ্ক্রিয় করুন</string>
<string name="sources_disabled">উৎস নিষ্ক্রিয়</string>
<string name="disable_connectivity_check">সংযোগ পরীক্ষা করতে অক্ষম করুন</string>
<string name="ignore_ssl_errors_summary">নেটওয়ার্ক সংস্থানগুলি অ্যাক্সেস করার সময় আপনি যদি কোনও SSL- সম্পর্কিত সমস্যার মুখোমুখি হন তবে আপনি SSL শংসাপত্র যাচাইকরণ অক্ষম করতে পারেন৷ এটি আপনার নিরাপত্তা প্রভাবিত করতে পারে। এই সেটিং পরিবর্তন করার পরে অ্যাপ্লিকেশন পুনরায় চালু করা প্রয়োজন।</string>
<string name="disable_connectivity_check_summary">আপনার সমস্যা থাকলে কানেক্টিভিটি চেক এড়িয়ে যান (যেমন, নেটওয়ার্ক সংযুক্ত থাকা সত্ত্বেও অফলাইন মোডে যাওয়া)</string>
<string name="disable_nsfw_notifications">NSFW বিজ্ঞপ্তি অক্ষম করুন</string>
<string name="disable_nsfw_notifications_summary">NSFW মাঙ্গা আপডেট সম্পর্কে বিজ্ঞপ্তি দেখাবেন না</string>
<string name="tracker_debug_info">নতুন অধ্যায় লগ জন্য পরীক্ষা করা হচ্ছে</string>
<string name="tracker_debug_info_summary">নতুন অধ্যায়গুলির জন্য পটভূমি পরীক্ষা সম্পর্কে তথ্য ডিবাগ করুন</string>
<string name="_new">নতুন</string>
<string name="all_languages">সব ভাষা</string>
<string name="screenshots_block_incognito">ছদ্মবেশী মোড হলে ব্লক করুন</string>
<string name="image_server">পছন্দের ইমেজ সার্ভার</string>
<string name="crop_pages">পৃষ্ঠাগুলি কাটা</string>
<string name="pin">পিন</string>
<string name="unpin">আনপিন করুন</string>
<string name="source_pinned">উৎস পিন করা হয়েছে</string>
<string name="source_unpinned">উৎস আনপিন করা হয়েছে</string>
<string name="sources_unpinned">উৎস আনপিন করা হয়েছে</string>
<string name="sources_pinned">উৎস পিন করা হয়েছে</string>
<string name="recent_sources">সাম্প্রতিক সূত্র</string>
<string name="percent_read">শতকরা কতটুকু করেছেন</string>
<string name="percent_left">শতকরা কতটুকু পড়তে বাকি আছে</string>
<string name="chapters_read">অধ্যায় পড়া</string>
<string name="chapters_left">অধ্যায় বাকি আছে</string>
<string name="external_source">বাহ্যিক/প্লাগইন</string>
<string name="plugin_incompatible">বেমানান প্লাগইন বা অভ্যন্তরীণ ত্রুটি. আপনি প্লাগইন এবং Kotatsu এর সর্বশেষ সংস্করণ ব্যবহার করছেন তা নিশ্চিত করুন৷</string>
<string name="plugin_incompatible_with_cause">প্লাগইন ত্রুটি: %s \n· নিশ্চিত করুন যে আপনি প্লাগইন এবং Kotatsu এর সর্বশেষ সংস্করণ ব্যবহার করছেন</string>
<string name="connection_ok">সংযোগ ঠিক আছে</string>
<string name="invalid_proxy_configuration">অবৈধ প্রক্সি কনফিগারেশন</string>
<string name="show_quick_filters">দ্রুত ফিল্টার দেখান</string>
<string name="show_quick_filters_summary">নির্দিষ্ট পরামিতি দ্বারা মাঙ্গা তালিকা ফিল্টার করার ক্ষমতা প্রদান করে</string>
<string name="sfw">SFW</string>
<string name="skip_all">সব এড়িয়ে যান</string>
<string name="stuck">আটকে গেছে</string>
<string name="source_broken_warning">এই মঙ্গা সূত্রটি\nভাঙ্গা হিসাবে চিহ্নিত। কিছু বৈশিষ্ট্য \nকাজ নাও হতে পারে</string>
</resources> </resources>

@ -740,7 +740,7 @@
<string name="all_sources_enabled">Všechny zdroje jsou povoleny</string> <string name="all_sources_enabled">Všechny zdroje jsou povoleny</string>
<string name="rating">Hodnocení</string> <string name="rating">Hodnocení</string>
<string name="source">Zdroj</string> <string name="source">Zdroj</string>
<string name="incognito">Anonymní</string> <string name="incognito">Inkognito</string>
<string name="reader_info_bar_transparent">Průhledný informační proužek pro čtenáře</string> <string name="reader_info_bar_transparent">Průhledný informační proužek pro čtenáře</string>
<string name="handle_links">Spravovat odkazy</string> <string name="handle_links">Spravovat odkazy</string>
<string name="handle_links_summary">Zpracování odkazů na manga z externích aplikací (např. webového prohlížeče). Může být také nutné povolit ji ručně v systémových nastaveních aplikace</string> <string name="handle_links_summary">Zpracování odkazů na manga z externích aplikací (např. webového prohlížeče). Může být také nutné povolit ji ručně v systémových nastaveních aplikace</string>
@ -812,4 +812,75 @@
<string name="creating_backup">Vytváření kopie</string> <string name="creating_backup">Vytváření kopie</string>
<string name="collapse_long_description">Sbalit dlouhý popis</string> <string name="collapse_long_description">Sbalit dlouhý popis</string>
<string name="changelog">Seznam změn</string> <string name="changelog">Seznam změn</string>
<string name="saved_filters">Uložené filtry</string>
<string name="enter_name">Vložte jméno</string>
<string name="theme_name_expressive">Výrazný (Test)</string>
<string name="pull_to_prev_chapter">Uvolněte pro otevření předešlé kapitoly</string>
<string name="pull_to_next_chapter">Uvolněte pro otevření další kapitoly</string>
<string name="pull_top_no_prev">Žádná předešlá kapitola</string>
<string name="pull_bottom_no_next">Žádná další kapitola</string>
<string name="frequency_every_6_hours">Každých 6 hodin</string>
<string name="reader_navigation_inverted">Převrátit navigační prvky</string>
<string name="reader_navigation_inverted_summary">Vyměňit směr tlačítka hlasitosti a šipek na klávesnici (vlevo/nahoru/dolů/vpravo)</string>
<string name="two_page_scroll_sensitivity">Dvoustranná citlivost posouvání</string>
<string name="enable_pull_gesture_title">Povolit tahová gesta</string>
<string name="enable_pull_gesture_summary">Použijte tahová gesta pro přepínání kapitol v režimu webtoon</string>
<string name="reader_chapter_toast">Zobrazit změnu kapitoly pomocí pop-upu</string>
<string name="reader_chapter_toast_summary">Zobrazit pop-up s názvem kapitoly, když se změní</string>
<string name="badges_in_lists">Odznaky v seznamech</string>
<string name="link_to_manga_on_s">Odkaz na mangu na %s</string>
<string name="no_write_permission_to_file">Nemá oprávnění k ukládání souboru</string>
<string name="error_non_file_uri">Zvolená cesta nemůže být použita, protože neoznačuje soubor nebo adresář</string>
<string name="use_default_cover">Použít výchozí přebal</string>
<string name="pick_manga_page">Vyberte stranu mangy</string>
<string name="pick_custom_file">Vybrat vlastní přebal</string>
<string name="change_cover">Změnit přebal</string>
<string name="page_switch_timer">Strana se změní kažých ~%d sekund</string>
<string name="incognito_mode_hint_nsfw">Tato manga může obsahovat dospělá témata. Chcete použít režim inkognito?</string>
<string name="incognito_for_nsfw">Režim inkognito pro NSFW mangu</string>
<string name="additional_action_required">Dodatečná akce je nutná</string>
<string name="hide_from_main_screen">Skrýt z hlavní obrazovky</string>
<string name="changelog_summary">Historie změn pro nedávno vydané verze</string>
<string name="reader_multitask">Otevřít čtečku ve vlastním okně</string>
<string name="reader_multitask_summary">Umožňuje zároveň otevřít několik různých mang ve vlastních oknech</string>
<string name="theme_name_itsuka">Itsuka</string>
<string name="theme_name_totoro">Totoro</string>
<string name="book_effect">Nažloutlé pozadí (filtr modrého světla)</string>
<string name="local_storage_cleanup">Čištění místního úložišťe</string>
<string name="packup_creation_failed">Nepodařilo se vytvořit zálohu</string>
<string name="main_screen">Hlavní obrazovka</string>
<string name="main_screen_fab">Zobrazit plovoucí tlačítko Pokračovat</string>
<string name="main_screen_fab_summary">Umožňuje pokračovat ve čtení jedním kliknutím. Toto tlačítko se nezobrazí v režimu inkognito nebo když je historie prázdná</string>
<string name="error_corrupted_zip">Poškozený ZIP archiv (%s)</string>
<string name="discord_rpc">Discord Rich Presence</string>
<string name="discord_token">Discord Token</string>
<string name="discord_token_summary">Zadejte svůj Discord Token, abyste zapnuli Rich Presence</string>
<string name="discord_token_description">Zadejte svůj Discord Token nebo klikněte na %s, abyste ho získali pomocí prohlížeče</string>
<string name="discord_token_hint">Vložte svůj Discord Token zde</string>
<string name="discord_rpc_summary">Zobrazit stav čtení na Discordu</string>
<string name="obtain">Získat</string>
<string name="discord_rpc_description">Čte mangu na Kotatsu - aplikace pro čtení mangy</string>
<string name="reading_s">Čtení %s</string>
<string name="read_on_s">Číst na %s</string>
<string name="rpc_skip_nsfw_summary">Nepoužívat RPC pro mangu pro dospělé</string>
<string name="invalid_token">Neplatný token: %s</string>
<string name="show_floating_control_button">Zobrazit plovoucí ovládací tlačítko</string>
<string name="unavailable">Nedostupné</string>
<string name="manga_restricted_description">Tato manga není k dispozici od tohoto zdroje. Zkuste ji vyhledat v jiných zdrojích nebo otevřít v prohlížeči pro více informací</string>
<string name="no_chapters_in_manga">Tato manga neobsahuje žádné kapitoly</string>
<string name="chapters_load_failed">Načítání kapitol selhalo</string>
<string name="telegram_integration">Integrace Telegramu</string>
<string name="test_parser">Vyzkoušet zdroje mang</string>
<string name="rename">Přejmenovat</string>
<string name="save_filter">Uložit filtr</string>
<string name="overwrite">Nahradit</string>
<string name="filter_overwrite_confirm">Filtr pojmenován\"%s\" již existuje. Chcete ho nahradit?</string>
<string name="storage_and_network">Uložiště a síť</string>
<string name="create_or_restore_backup">Vytvořit nebo obnovit zálohu</string>
<string name="data_removal">Odstranění dat</string>
<string name="privacy">Soukromí</string>
<string name="source_broken_warning">Tento zdroj byl označen jako rozbitý. Některé funkce nemusí fungovat</string>
<string name="download_default_directory">Výchozí adresář pro stahování mangy</string>
<string name="private_app_directory_warning">Tento adresář a všechna data v něm budou smazána, pokud odinstalujete aplikaci</string>
<string name="available_pattern">%1$s dostupný</string>
</resources> </resources>

@ -859,4 +859,8 @@
<string name="pull_bottom_no_next">Walang susunod na kabanata</string> <string name="pull_bottom_no_next">Walang susunod na kabanata</string>
<string name="enable_pull_gesture_title">Paganahin ang paghila na gesture</string> <string name="enable_pull_gesture_title">Paganahin ang paghila na gesture</string>
<string name="enable_pull_gesture_summary">Gamitin ang paghila na gesture para makapagpalit ng kabanata sa webtoon</string> <string name="enable_pull_gesture_summary">Gamitin ang paghila na gesture para makapagpalit ng kabanata sa webtoon</string>
<string name="saved_filters">Naka-save na mga filter</string>
<string name="enter_name">Ipasok ang pangalan</string>
<string name="reader_chapter_toast">Ipakita ang popup sa pagpalit ng kabanata</string>
<string name="reader_chapter_toast_summary">Magpakita ng pop-up na mensahe na may pamagat ng kabanata kapag binago ito</string>
</resources> </resources>

@ -579,8 +579,8 @@
<string name="reader_actions_summary">Konfigurirajte radnje za dodirna područja zaslona</string> <string name="reader_actions_summary">Konfigurirajte radnje za dodirna područja zaslona</string>
<string name="switch_pages_volume_buttons_summary">Koristite tipke za glasnoću za prebacivanje stranica</string> <string name="switch_pages_volume_buttons_summary">Koristite tipke za glasnoću za prebacivanje stranica</string>
<string name="tap_action">Radnja pri dodiru</string> <string name="tap_action">Radnja pri dodiru</string>
<string name="long_tap_action">Radnja dugog dodira</string> <string name="long_tap_action">Radnja pri dugom dodiru</string>
<string name="use_two_pages_landscape">Koristi izgled dvije stranice u pejzažnoj orijentaciji (beta)</string> <string name="use_two_pages_landscape">Koristi raspored dviju stranice u poleženom položaju (beta)</string>
<string name="download_option_next_unread_n_chapters">Sljedeći nepročitani %s</string> <string name="download_option_next_unread_n_chapters">Sljedeći nepročitani %s</string>
<string name="download_option_all_unread">Sva nepročitana poglavlja</string> <string name="download_option_all_unread">Sva nepročitana poglavlja</string>
<string name="download_option_all_unread_b">Sva nepročitana poglavlja (%s)</string> <string name="download_option_all_unread_b">Sva nepročitana poglavlja (%s)</string>
@ -861,4 +861,24 @@
<string name="test_parser">Testiraj izvor mange</string> <string name="test_parser">Testiraj izvor mange</string>
<string name="discord_rpc">Discord Rich Presence</string> <string name="discord_rpc">Discord Rich Presence</string>
<string name="discord_token_summary">Unesi svoj Discord token za uključivanje Rich Presence podataka</string> <string name="discord_token_summary">Unesi svoj Discord token za uključivanje Rich Presence podataka</string>
<string name="saved_filters">Spremljeni filtri</string>
<string name="enter_name">Upiši ime</string>
<string name="reader_chapter_toast_summary">Prikaži skočnu poruku s naslovom poglavlja kada je promijenjen</string>
<string name="rename">Preimenuj</string>
<string name="save_filter">Spremi filtar</string>
<string name="reader_chapter_toast">Prikaži skočnu poruku za promijenjeno poglavlje</string>
<string name="two_page_scroll_sensitivity">Osjetljivost pomicanja dviju stranica</string>
<string name="frequency_every_6_hours">Svakih 6 sati</string>
<string name="overwrite">Prepiši</string>
<string name="filter_overwrite_confirm">Filtar s imenom „%s” već postoji. Želiš li ga prepisati?</string>
<string name="storage_and_network">Spremište i mreža</string>
<string name="create_or_restore_backup">Stvori ili obnovi sigurnosnu kopiju</string>
<string name="data_removal">Uklanjanje podataka</string>
<string name="privacy">Privatnost</string>
<string name="source_broken_warning">Ovaj izvor mange označen je kao pokvaren. Neke funkcije možda neće raditi</string>
<string name="download_default_directory">Standardni direktorij za preuzimanje manga</string>
<string name="private_app_directory_warning">Ako deinstaliraš aplikaciju, ovaj će se direktorij izbrisati sa svim podacima</string>
<string name="available_pattern">%1$s dostupno</string>
<string name="pinned_sources_only">Samo prikvačeni izvori</string>
<string name="hide_empty_sources">Sakrij prazne izvore</string>
</resources> </resources>

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<plurals name="items"> <plurals name="items">
<item quantity="other">”%1$d butir</item> <item quantity="other">%1$d item</item>
</plurals> </plurals>
<plurals name="new_chapters"> <plurals name="new_chapters">
<item quantity="other">%1$d Sub-bab Baru</item> <item quantity="other">%1$d Sub-bab Baru</item>

@ -865,4 +865,24 @@
<string name="pull_bottom_no_next">Tidak ada bab berikutnya</string> <string name="pull_bottom_no_next">Tidak ada bab berikutnya</string>
<string name="enable_pull_gesture_title">Aktifkan gerakan tarik</string> <string name="enable_pull_gesture_title">Aktifkan gerakan tarik</string>
<string name="enable_pull_gesture_summary">Gunakan gerakan tarik untuk pindah bab di webtoon</string> <string name="enable_pull_gesture_summary">Gunakan gerakan tarik untuk pindah bab di webtoon</string>
<string name="two_page_scroll_sensitivity">Sensitivitas Gulir Dua Halaman</string>
<string name="saved_filters">Filter tersimpan</string>
<string name="enter_name">Masukkan nama</string>
<string name="reader_chapter_toast">Tampilkan popup perubahan bab</string>
<string name="reader_chapter_toast_summary">Tampilkan pesan pop-up dengan judul bab saat diubah</string>
<string name="rename">Ganti nama</string>
<string name="save_filter">Simpan filter</string>
<string name="overwrite">Timpa</string>
<string name="filter_overwrite_confirm">Filter bernama \"%s\" sudah ada. Ingin menimpanya?</string>
<string name="storage_and_network">Penyimpanan dan jaringan</string>
<string name="create_or_restore_backup">Buat atau pulihkan cadangan</string>
<string name="data_removal">Penghapusan data</string>
<string name="source_broken_warning">Sumber manga ini telah ditandai rusak. Beberapa fitur mungkin tidak berfungsi</string>
<string name="privacy">Pribadi</string>
<string name="frequency_every_6_hours">Setiap 6 jam</string>
<string name="download_default_directory">Direktori default untuk mengunduh manga</string>
<string name="private_app_directory_warning">Direktori ini beserta semua datanya akan dihapus jika Anda menghapus aplikasi</string>
<string name="available_pattern">%1$s tersedia</string>
<string name="pinned_sources_only">Hanya sumber yang disematkan</string>
<string name="hide_empty_sources">Sembunyikan sumber kosong</string>
</resources> </resources>

@ -866,4 +866,22 @@
<string name="pull_bottom_no_next">Nessun capitolo successivo</string> <string name="pull_bottom_no_next">Nessun capitolo successivo</string>
<string name="enable_pull_gesture_title">Abilita gesto di scorrimento</string> <string name="enable_pull_gesture_title">Abilita gesto di scorrimento</string>
<string name="enable_pull_gesture_summary">Abilita gesto di scorrimento per cambiare capitolo in webtoon</string> <string name="enable_pull_gesture_summary">Abilita gesto di scorrimento per cambiare capitolo in webtoon</string>
<string name="two_page_scroll_sensitivity">Sensibilità di scorrimento a due pagine</string>
<string name="saved_filters">Filtri salvati</string>
<string name="enter_name">Inserisci nome</string>
<string name="reader_chapter_toast">Mostra popup di cambio capitolo</string>
<string name="reader_chapter_toast_summary">Mostra un messaggio pop-up con un titolo del capitolo quando è cambiato</string>
<string name="rename">Rinomina</string>
<string name="save_filter">Salva filtro</string>
<string name="overwrite">Sovrascrivi</string>
<string name="filter_overwrite_confirm">Un filtro di nome \"%s\" esiste già. Vuoi sovrascriverlo?</string>
<string name="storage_and_network">Memoria e rete</string>
<string name="create_or_restore_backup">Crea o ripristina un backup</string>
<string name="data_removal">Rimozione dei dati</string>
<string name="privacy">Privacy</string>
<string name="source_broken_warning">Questa fonte manga è stata contrassegnata come non funzionante. Alcune funzioni potrebbero non essere disponibili</string>
<string name="frequency_every_6_hours">Ogni 6 ore</string>
<string name="download_default_directory">Cartella predefinita per scaricare manga</string>
<string name="private_app_directory_warning">Questa cartella e tutti i suoi dati verranno eliminati se disinstalli l\'applicazione</string>
<string name="available_pattern">%1$s disponibile</string>
</resources> </resources>

@ -93,7 +93,7 @@
<string name="manga_shelf">本棚</string> <string name="manga_shelf">本棚</string>
<string name="recent_manga">最近</string> <string name="recent_manga">最近</string>
<string name="pages_animation">ページアニメーション</string> <string name="pages_animation">ページアニメーション</string>
<string name="manga_save_location">ダウンロード用のフォルダ</string> <string name="manga_save_location">ダウンロードフォルダ</string>
<string name="not_available">利用不可</string> <string name="not_available">利用不可</string>
<string name="cannot_find_available_storage">使用可能なストレージがありません</string> <string name="cannot_find_available_storage">使用可能なストレージがありません</string>
<string name="other_storage">その他のストレージ</string> <string name="other_storage">その他のストレージ</string>
@ -101,7 +101,7 @@
<string name="all_favourites">全てのお気に入り</string> <string name="all_favourites">全てのお気に入り</string>
<string name="read_later">後で読む</string> <string name="read_later">後で読む</string>
<string name="updates">更新</string> <string name="updates">更新</string>
<string name="text_feed_holder">あなたが読んでいるものの新しいチャプターがここに示されています</string> <string name="text_feed_holder">あなたが読んでいるものの新しいがここに示されています</string>
<string name="search_results">の検索結果</string> <string name="search_results">の検索結果</string>
<string name="size_s">サイズ:%s</string> <string name="size_s">サイズ:%s</string>
<string name="clear_updates_feed">更新フィードをクリア</string> <string name="clear_updates_feed">更新フィードをクリア</string>
@ -249,7 +249,7 @@
<string name="send">送信</string> <string name="send">送信</string>
<string name="disable_all">すべて無効にする</string> <string name="disable_all">すべて無効にする</string>
<string name="appwidget_recent_description">最近読んだ漫画</string> <string name="appwidget_recent_description">最近読んだ漫画</string>
<string name="use_fingerprint">指紋がある場合は、指紋を使用する</string> <string name="use_fingerprint">利用可能な場合、指紋認証を使用する</string>
<string name="appwidget_shelf_description">お気に入りの漫画</string> <string name="appwidget_shelf_description">お気に入りの漫画</string>
<string name="report">報告</string> <string name="report">報告</string>
<string name="status_reading">読書</string> <string name="status_reading">読書</string>
@ -316,7 +316,7 @@
<string name="reader_control_ltr">人間工学に基づいたリーダーコントロール</string> <string name="reader_control_ltr">人間工学に基づいたリーダーコントロール</string>
<string name="history_shortcuts">最近のマンガのショートカットを表示</string> <string name="history_shortcuts">最近のマンガのショートカットを表示</string>
<string name="history_shortcuts_summary">アプリケーションアイコンを長押しして最近のマンガを利用できるようにする</string> <string name="history_shortcuts_summary">アプリケーションアイコンを長押しして最近のマンガを利用できるようにする</string>
<string name="reader_control_ltr_summary">右端をタップするか、右キーを押すと、常に次のページに切り替わります</string> <string name="reader_control_ltr_summary">ページ切り替え方向をリーダーモードに調整しないでください。例えば、右キーを押すと常に次のページに切り替わります。このオプションはハードウェア入力デバイスにのみ影響します。</string>
<string name="color_correction">色補正</string> <string name="color_correction">色補正</string>
<string name="brightness">輝度</string> <string name="brightness">輝度</string>
<string name="contrast">コントラスト</string> <string name="contrast">コントラスト</string>
@ -565,7 +565,7 @@
<string name="show_labels_in_navbar">ナビゲーションバーにラベルを表示する</string> <string name="show_labels_in_navbar">ナビゲーションバーにラベルを表示する</string>
<string name="pages_saving">ページを保存</string> <string name="pages_saving">ページを保存</string>
<string name="remove_from_history">履歴から削除</string> <string name="remove_from_history">履歴から削除</string>
<string name="preferred_download_format">希望するダウンロード形式</string> <string name="preferred_download_format">推奨ダウンロード形式</string>
<string name="single_cbz_file">単一のCBZファイル</string> <string name="single_cbz_file">単一のCBZファイル</string>
<string name="multiple_cbz_files">複数のCBZファイル</string> <string name="multiple_cbz_files">複数のCBZファイル</string>
<string name="other_manga">他のマンガ</string> <string name="other_manga">他のマンガ</string>
@ -671,4 +671,148 @@
<string name="genre">ジャンル</string> <string name="genre">ジャンル</string>
<string name="download_added">ダウンロードに追加</string> <string name="download_added">ダウンロードに追加</string>
<string name="chapter_selection_hint">チャプターリストの項目を長押しすると、ダウンロードするチャプターを選択できます。</string> <string name="chapter_selection_hint">チャプターリストの項目を長押しすると、ダウンロードするチャプターを選択できます。</string>
<string name="frequency_every_6_hours">6時間毎に</string>
<string name="available_pattern">%1$sが有効</string>
<string name="no_chapters_in_manga">この漫画には利用可能なチャプターが含まれていません</string>
<string name="chapters_load_failed">チャプターリストの読み込みに失敗</string>
<string name="telegram_integration">Telegramで連携</string>
<string name="test_parser">漫画のソースをテストする</string>
<string name="rename">リネーム</string>
<string name="save_filter">フィルターを保存</string>
<string name="overwrite">上書き</string>
<string name="filter_overwrite_confirm">「%s」という名前のフィルターが既に存在します。上書きしますか</string>
<string name="storage_and_network">ストレージとネットワーク</string>
<string name="create_or_restore_backup">バックアップを作成または復元する</string>
<string name="data_removal">データ削除</string>
<string name="privacy">プライバシー</string>
<string name="source_broken_warning">この漫画ソースは破損しているとマークされています。一部の機能が動作しない可能性があります</string>
<string name="download_default_directory">漫画ダウンロードのデフォルトディレクトリ</string>
<string name="private_app_directory_warning">アプリケーションをアンインストールすると、このディレクトリとすべてのデータが削除されます</string>
<string name="author">著者</string>
<string name="rating">評価</string>
<string name="source">ソース</string>
<string name="translation">翻訳</string>
<string name="captcha_required_message">このソースを続行するにはキャプチャを解決する必要があります</string>
<string name="ask_every_time">いつでも聞いて</string>
<string name="screen_orientation">画面の向き</string>
<string name="portrait">肖像画</string>
<string name="landscape">風景</string>
<string name="by_name_reverse">名前を反転</string>
<string name="content_rating">コンテンツレーティング</string>
<string name="global_search">グローバルサーチ</string>
<string name="disable_captcha_notifications">Captcha通知を無効にする</string>
<string name="disable_captcha_notifications_summary">このソースのCaptchaに関する通知は届きませんが、これによりバックグラウンド操作新章の確認、おすすめ情報の取得などが中断される可能性があります</string>
<string name="chapter_volume_number">巻 %1$s 章 %2$s</string>
<string name="chapter_number">章%s</string>
<string name="unnamed_chapter">名前のない章</string>
<string name="search_disabled_sources">無効化されたソースを検索する</string>
<string name="error_details">エラーの詳細</string>
<string name="error_disclaimer_manga">ウェブブラウザで漫画を開いて、そのソースで利用可能かどうかを確認してください。</string>
<string name="error_disclaimer_app_outdated">お使いのKotatsuのバージョンが古くなっているようです。最新のバージョンをインストールして、利用可能なすべての修正プログラムを入手してください。</string>
<string name="error_disclaimer_report">開発者へバグ報告を提出できます。これにより問題の調査と修正に役立ちます。</string>
<string name="link_to_manga_on_s">%sの漫画へのリンク</string>
<string name="link_to_manga_in_app">Kotatsuの中の漫画へのリンク</string>
<string name="clear_browser_data">ブラウザのデータを消去する</string>
<string name="clear_browser_data_summary">キャッシュやクッキーなどのブラウザデータを消去してください。警告:マンガソースでの認証が無効になる可能性があります</string>
<string name="no_write_permission_to_file">ファイルへの書き込み権限がありません</string>
<string name="exclude_nsfw_from_suggestions_summary">成人向け漫画はおすすめに表示されません。このオプションは一部のソースでは正確に機能しない場合があります</string>
<string name="include_disabled_sources">無効化されたソースを含める</string>
<string name="suggestions_disabled_sources_summary">すべてのマンガソースからの提案を表示する(無効化されているものも含む)</string>
<string name="tags_warnings">危険なジャンルを強調する</string>
<string name="tags_warnings_summary">ほとんどのユーザーにとって不適切と思われるジャンルを強調表示する</string>
<string name="error_non_file_uri">選択されたパスはファイルまたはディレクトリを指していないため使用できません</string>
<string name="manga_override_hint">これらの変更は、アプリ内でのマンガの表示方法に影響を与えます</string>
<string name="use_default_cover">デフォルトの表紙を使用する</string>
<string name="pick_manga_page">漫画のページを選ぶ</string>
<string name="pick_custom_file">カスタムファイルを選択</string>
<string name="change_cover">カバーを変える</string>
<string name="page_switch_timer">ページは約%d秒ごとに切り替わります</string>
<string name="dont_ask_again">二度と聞くな</string>
<string name="incognito_mode_hint_nsfw">この漫画には成人向けの内容が含まれている可能性があります。シークレットモードを使用しますか?</string>
<string name="incognito_for_nsfw">成人向け漫画のシークレットモード</string>
<string name="additional_action_required">追加の操作が必要です</string>
<string name="hide_from_main_screen">メイン画面から非表示にする</string>
<string name="changelog">変更履歴</string>
<string name="changelog_summary">最近リリースされたバージョンの変更履歴</string>
<string name="collapse">折りたたむ</string>
<string name="expand">拡張する</string>
<string name="adblock">ブラウザで広告をブロックする</string>
<string name="adblock_summary">組み込みブラウザでの広告ブロック(ベータ版)</string>
<string name="collapse_long_description">長い説明を折りたたむ</string>
<string name="creating_backup">バックアップを作成する</string>
<string name="share_backup">バックアップを共有する</string>
<string name="reader_multitask">別のタスクでリーダーを開く</string>
<string name="reader_multitask_summary">複数の異なる漫画を開いたまま、複数のリーダーを同時に保持できます</string>
<string name="theme_name_itsuka">トピックス</string>
<string name="theme_name_totoro">トトロ</string>
<string name="book_effect">黄色がかった背景(青いフィルター)</string>
<string name="local_storage_cleanup">ローカルストレージのクリーンアップ</string>
<string name="packup_creation_failed">バックアップの作成に失敗しました</string>
<string name="main_screen">メイン画面</string>
<string name="main_screen_fab">フローティングの「続行」ボタンを表示</string>
<string name="main_screen_fab_summary">ワンクリックで読み続けられます。このボタンはシークレットモード時や履歴が空の時には表示されません</string>
<string name="error_corrupted_zip">破損したZIPアーカイブ(%s)</string>
<string name="discord_rpc">Discord リッチプレゼンス</string>
<string name="discord_token">Discordトークン</string>
<string name="discord_token_summary">リッチプレゼンスを有効にするには、Discordトークンを入力してください</string>
<string name="discord_token_description">Discord トークンを入力するか、%s をクリックしてブラウザーで取得します</string>
<string name="discord_token_hint">ここにDiscordトークンを貼り付けてください</string>
<string name="discord_rpc_summary">Discordで読み取りステータスを表示</string>
<string name="obtain">アクセス</string>
<string name="discord_rpc_description">Kotatsuで漫画を読む - 漫画リーダーアプリ</string>
<string name="reading_s">読書 %s</string>
<string name="read_on_s">%s で読む</string>
<string name="rpc_skip_nsfw_summary">成人向けコンテンツに RPC を使用しないでください</string>
<string name="invalid_token">無効なトークン: %s</string>
<string name="show_floating_control_button">「続行」ボタンをフローティングで表示</string>
<string name="unavailable">利用案内</string>
<string name="manga_restricted_description">このマンガは、このソースで読み込むことはできません。 他のソースで検索するか、ブラウザで開くと、より多くの情報が得られます</string>
<string name="saved_filters">保存フィルター</string>
<string name="chapters_grid_view">グリッドビュー</string>
<string name="enter_name">名前を入力してください</string>
<string name="nsfw_16">16歳以上</string>
<string name="theme_name_expressive">エクスプレス(テスト)</string>
<string name="pull_to_prev_chapter">前の章を開く</string>
<string name="pull_to_next_chapter">次の章を開く</string>
<string name="state_abandoned">ドロップ</string>
<string name="speed_value">x%.1f</string>
<string name="lock_screen_rotation">ロック スクリーンの回転</string>
<string name="manage_sources">ソースの管理</string>
<string name="manual">マニュアル</string>
<string name="available_d">利用可能: %1$d</string>
<string name="reader_optimize_summary">画面外ページの品質を下げ、メモリ使用量を削減する</string>
<string name="state">ステータス</string>
<string name="error_filter_states_genre_not_supported">両方のジャンルや状態のフィルタリングは、このソースではサポートされていません</string>
<string name="volume_unknown">未知のボリューム</string>
<string name="vertical">プロフィール</string>
<string name="last_read">最近の投稿</string>
<string name="reader_actions">リーダーの動作</string>
<string name="none">なし</string>
<string name="two_page_scroll_sensitivity">2ページスクロール感度</string>
<string name="ask_for_dest_dir_every_time">毎回、目的のディレクトリを指定してください</string>
<string name="default_page_save_dir">デフォルトページ保存ディレクトリ</string>
<string name="location">アクセス</string>
<string name="reading_stats">統計を読む</string>
<string name="all_time">すべての時間</string>
<string name="pages_read_s">ページの読み込み: %s</string>
<string name="migrate_confirmation">「%2$s」から「%3$s」を「%4$s」に置き換えられます</string>
<string name="chapters_deleted_pattern">%1$s を削除し、%2$s をクリア</string>
<string name="split_by_translations">翻訳によるスプリット</string>
<string name="order_oldest">最新記事</string>
<string name="long_ago_read">昔読んだ</string>
<string name="enable_source">ソースを有効にする</string>
<string name="last_used">最後の使用</string>
<string name="show_updated">更新情報</string>
<string name="more_frequently">より頻繁に</string>
<string name="recent_queries">最近の質問</string>
<string name="suggested_queries">提案されたクエリ</string>
<string name="sources_disabled">無効なソース</string>
<string name="disable_connectivity_check">接続チェックを無効にする</string>
<string name="tracker_debug_info">新しいチャプターログの確認</string>
<string name="tracker_debug_info_summary">新規チャプター向けバックグラウンドチェックに関するデバッグ情報</string>
<string name="_new">新しい</string>
<string name="image_server">優先画像サーバ</string>
<string name="pin">ピン</string>
<string name="unpin">ピンを外す</string>
<string name="source_pinned">ソース ピン留め</string>
</resources> </resources>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="new_chapters">
<item quantity="one">%1$d nieuwe hoofdstuk</item>
<item quantity="other">%1$d nieuwe hoofdstukken</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d hoofdstuk</item>
<item quantity="other">%1$d hoofdstukken</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d minuut geleden</item>
<item quantity="other">%1$d minuten geleden</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d uur geleden</item>
<item quantity="other">%1$d uren geleden</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d dag geleden</item>
<item quantity="other">%1$d dagen geleden</item>
</plurals>
<plurals name="months_ago">
<item quantity="one">%1$d maand geleden</item>
<item quantity="other">%1$d maanden geleden</item>
</plurals>
<plurals name="hours">
<item quantity="one">%1$d uur</item>
<item quantity="other">%1$d uren</item>
</plurals>
<plurals name="minutes">
<item quantity="one">%1$d minuut</item>
<item quantity="other">%1$d minuten</item>
</plurals>
</resources>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="chapters">Hoofdstukken</string>
<string name="new_chapters">Nieuwe hoofdstukken</string>
<string name="favourites">Favorieten</string>
<string name="history">Geschiedenis</string>
<string name="error_occurred">Er is een fout opgetreden</string>
<string name="network_error">Netwerk fout</string>
<string name="details">Details</string>
<string name="detailed_list">Gedetailleerde lijst</string>
<string name="grid">Rooster</string>
<string name="list_mode">Lijstmodus</string>
<string name="settings">Instellingen</string>
<string name="remote_sources">Mangabronnen</string>
<string name="loading_">Bezig met laden…</string>
<string name="computing_">Computeren…</string>
<string name="chapter_d_of_d">Hoofdstuk %1$d van %2$d</string>
<string name="close">Sluiten</string>
<string name="try_again">Probeer het opnieuw</string>
<string name="retry">Opnieuw proberen</string>
<string name="clear_history">Geschiedenis wissen</string>
</resources>

@ -756,7 +756,7 @@
<string name="telegram_chat_id">ID do bate-papo do telegram</string> <string name="telegram_chat_id">ID do bate-papo do telegram</string>
<string name="open_telegram_bot">Abra o bot do Telegram</string> <string name="open_telegram_bot">Abra o bot do Telegram</string>
<string name="clear_database">Limpar banco de dados</string> <string name="clear_database">Limpar banco de dados</string>
<string name="clear_database_summary">Excluir informações sobre mangás que não estão sendo utilizadas</string> <string name="clear_database_summary">Excluir informações sobre mangás que não estão sendo utilizados</string>
<string name="test_connection">Teste de conexão</string> <string name="test_connection">Teste de conexão</string>
<string name="backup_tg_echo">Mensagem de teste</string> <string name="backup_tg_echo">Mensagem de teste</string>
<string name="translation">Tradução</string> <string name="translation">Tradução</string>
@ -800,7 +800,7 @@
<string name="tags_warnings">Destacar gêneros perigosos</string> <string name="tags_warnings">Destacar gêneros perigosos</string>
<string name="tags_warnings_summary">Destacar gêneros que podem ser inapropriados para a maioria dos usuários</string> <string name="tags_warnings_summary">Destacar gêneros que podem ser inapropriados para a maioria dos usuários</string>
<string name="nsfw_16">+16</string> <string name="nsfw_16">+16</string>
<string name="exclude_nsfw_from_suggestions_summary">Mangás adultos não serão exibidas nas sugestões. Esta opção pode funcionar de forma imprecisa com algumas fontes</string> <string name="exclude_nsfw_from_suggestions_summary">Mangás adultos não serão exibidos nas sugestões. Esta opção pode funcionar de forma imprecisa com algumas fontes</string>
<string name="include_disabled_sources">Incluir fontes desabilitadas</string> <string name="include_disabled_sources">Incluir fontes desabilitadas</string>
<string name="suggestions_disabled_sources_summary">Mostrar sugestões de todas as fontes, incluindo as desabilitadas</string> <string name="suggestions_disabled_sources_summary">Mostrar sugestões de todas as fontes, incluindo as desabilitadas</string>
<string name="manga_override_hint">Essas mudanças afetarão como a obra é exibida no aplicativo</string> <string name="manga_override_hint">Essas mudanças afetarão como a obra é exibida no aplicativo</string>
@ -847,7 +847,7 @@
<string name="invalid_token">Token inválido: %s</string> <string name="invalid_token">Token inválido: %s</string>
<string name="show_floating_control_button">Mostrar botão de controle flutuante</string> <string name="show_floating_control_button">Mostrar botão de controle flutuante</string>
<string name="unavailable">Indisponível</string> <string name="unavailable">Indisponível</string>
<string name="manga_restricted_description">Este mangá não está disponível para leitura nessa fonte. Tente pesquisar por ele em outras fontes ou abrir em um navegador para mais informação</string> <string name="manga_restricted_description">Este mangá não está disponível para leitura nessa fonte. Tente pesquisar por ele em outras fontes ou abrir em um navegador para mais informações</string>
<string name="no_chapters_in_manga">Este mangá não contém nenhum capítulo</string> <string name="no_chapters_in_manga">Este mangá não contém nenhum capítulo</string>
<string name="chapters_load_failed">Falha ao carregar lista de capítulos</string> <string name="chapters_load_failed">Falha ao carregar lista de capítulos</string>
<string name="telegram_integration">Integração com Telegram</string> <string name="telegram_integration">Integração com Telegram</string>
@ -856,4 +856,32 @@
<string name="discord_rpc">Discord Rich Presence</string> <string name="discord_rpc">Discord Rich Presence</string>
<string name="discord_token_description">Insira seu Token do Discord ou clique em %s para obtê-lo usando o navegador</string> <string name="discord_token_description">Insira seu Token do Discord ou clique em %s para obtê-lo usando o navegador</string>
<string name="read_on_s">Leia em %s</string> <string name="read_on_s">Leia em %s</string>
<string name="saved_filters">Filtros salvos</string>
<string name="pull_to_prev_chapter">Soltar para abrir o capítulo anterior</string>
<string name="pull_to_next_chapter">Soltar para abrir o próximo capítulo</string>
<string name="pull_top_no_prev">Capítulo anterior indisponível</string>
<string name="pull_bottom_no_next">Próximo capítulo indisponível</string>
<string name="two_page_scroll_sensitivity">Sensibilidade de Rolagem Dupla</string>
<string name="enable_pull_gesture_title">Ativar gesto de puxar</string>
<string name="enable_pull_gesture_summary">Puxe para trocar de capítulo em webtoons</string>
<string name="reader_chapter_toast">Exibir mensagem ao mudar de capítulo</string>
<string name="reader_chapter_toast_summary">Exibir título do capítulo ao mudar de capítulo</string>
<string name="rename">Renomear</string>
<string name="save_filter">Salvar filtro</string>
<string name="overwrite">Sobrescrever</string>
<string name="filter_overwrite_confirm">O filtro \"%s\" já existe. Deseja substituí-lo?</string>
<string name="storage_and_network">Armazenamento e rede</string>
<string name="create_or_restore_backup">Criar ou restaurar um backup</string>
<string name="privacy">Privacidade</string>
<string name="enter_name">Insira um nome</string>
<string name="test_parser">Testar fonte de mangás</string>
<string name="data_removal">Remoção de dados</string>
<string name="source_broken_warning">Esta fonte de mangá está quebrada. Algumas funções podem não funcionar</string>
<string name="frequency_every_6_hours">A cada 6 horas</string>
<string name="download_default_directory">Diretório padrão onde baixar os mangás</string>
<string name="private_app_directory_warning">Esse diretório e todo seu conteúdo será deletado se desinstalar a aplicação</string>
<string name="available_pattern">%1$s disponível</string>
<string name="hide_empty_sources">Esconder fontes vazias</string>
<string name="pinned_sources_only">Apenas fontes fixadas</string>
<string name="auto_double_foldable">Duas Páginas Automático em Tela Dobrável</string>
</resources> </resources>

@ -858,4 +858,25 @@
<string name="no_chapters_in_manga">Esse manga não contém capítulos</string> <string name="no_chapters_in_manga">Esse manga não contém capítulos</string>
<string name="chapters_load_failed">Falha ao carregar lista de capítulos</string> <string name="chapters_load_failed">Falha ao carregar lista de capítulos</string>
<string name="telegram_integration">Integração Telegram</string> <string name="telegram_integration">Integração Telegram</string>
<string name="saved_filters">Filtros salvos</string>
<string name="enter_name">Digite o nome</string>
<string name="pull_to_prev_chapter">Solte para abrir o capítulo anterior</string>
<string name="pull_to_next_chapter">Solte para abrir o próximo capítulo</string>
<string name="pull_top_no_prev">Nenhum capítulo anterior</string>
<string name="pull_bottom_no_next">Sem mais capítulos</string>
<string name="two_page_scroll_sensitivity">Sensibilidade de rolagem de página dupla</string>
<string name="enable_pull_gesture_title">Ativar gesto de puxar</string>
<string name="enable_pull_gesture_summary">Use o gesto de puxar para mudar de capítulo na webtoon</string>
<string name="reader_chapter_toast">Mostrar pop-up de mudança de capítulo</string>
<string name="reader_chapter_toast_summary">Mostrar uma mensagem pop-up com o título do capítulo quando ele for alterado</string>
<string name="test_parser">Teste fonte do mangá</string>
<string name="rename">Renomear</string>
<string name="save_filter">Salvar filtro</string>
<string name="overwrite">Sobrescrever</string>
<string name="filter_overwrite_confirm">Já existe um filtro chamado \"%s\". Você quer sobrescrevê-lo?</string>
<string name="storage_and_network">Armazenamento e internet</string>
<string name="create_or_restore_backup">Criar e restaurar backup</string>
<string name="data_removal">Remover dados</string>
<string name="privacy">Privacidade</string>
<string name="source_broken_warning">Esta fonte de mangá foi marcada como quebrada. Alguns recursos podem não funcionar</string>
</resources> </resources>

@ -859,4 +859,27 @@
<string name="no_chapters_in_manga">Эта манга не содержит глав</string> <string name="no_chapters_in_manga">Эта манга не содержит глав</string>
<string name="chapters_load_failed">Не удалось загрузить список глав</string> <string name="chapters_load_failed">Не удалось загрузить список глав</string>
<string name="telegram_integration">Интеграция с Telegram</string> <string name="telegram_integration">Интеграция с Telegram</string>
<string name="pull_to_prev_chapter">Отпустите, чтобы открыть предыдущую главу</string>
<string name="pull_top_no_prev">Нет предыдущей главы</string>
<string name="pull_bottom_no_next">Нет следующей главы</string>
<string name="two_page_scroll_sensitivity">Чувствительность прокрутки двух страниц</string>
<string name="enable_pull_gesture_title">Включить жест перетягивания</string>
<string name="enable_pull_gesture_summary">Используйте жест перетаскивания для переключения глав в вебтуне</string>
<string name="test_parser">Проверка источников манги</string>
<string name="pull_to_next_chapter">Отпустите, чтобы открыть следующую главу</string>
<string name="reader_chapter_toast">Отображать сообщение о смене главы</string>
<string name="reader_chapter_toast_summary">Показывать всплывающее сообщение с названием главы при переходе между главами</string>
<string name="rename">Переименовать</string>
<string name="save_filter">Сохранить фильтр</string>
<string name="overwrite">Перезаписать</string>
<string name="filter_overwrite_confirm">Фильтр с именем \"%s\" уже существует. Перезаписать?</string>
<string name="storage_and_network">Хранилище и сеть</string>
<string name="create_or_restore_backup">Создать или восстановить резервную копию</string>
<string name="data_removal">Удаление данных</string>
<string name="privacy">Приватность</string>
<string name="source_broken_warning">Данный источник манги помечен как сломанный. Некоторые функции могут не работать</string>
<string name="download_default_directory">Каталог по умолчанию для загрузки манги</string>
<string name="private_app_directory_warning">Данный каталог вместе со всеми данными будет удалён при удалении приложения</string>
<string name="available_pattern">%1$s доступно</string>
<string name="enter_name">Введите имя</string>
</resources> </resources>

@ -30,7 +30,7 @@
<string name="download_complete">İndirildi</string> <string name="download_complete">İndirildi</string>
<string name="downloads">İndirmeler</string> <string name="downloads">İndirmeler</string>
<string name="by_name">Ad</string> <string name="by_name">Ad</string>
<string name="updated">Güncellendi</string> <string name="updated">Güncellenenler</string>
<string name="newest">En yeniler</string> <string name="newest">En yeniler</string>
<string name="by_rating">Puanlama</string> <string name="by_rating">Puanlama</string>
<string name="filter">Filtre</string> <string name="filter">Filtre</string>
@ -207,7 +207,7 @@
<string name="only_using_wifi">Yalnızca Wi-Fi\'de</string> <string name="only_using_wifi">Yalnızca Wi-Fi\'de</string>
<string name="preload_pages">Sayfaları önceden yükle</string> <string name="preload_pages">Sayfaları önceden yükle</string>
<string name="logged_in_as">%s olarak oturum açıldı</string> <string name="logged_in_as">%s olarak oturum açıldı</string>
<string name="nsfw">18+</string> <string name="nsfw">+18</string>
<string name="various_languages">Çeşitli diller</string> <string name="various_languages">Çeşitli diller</string>
<string name="search_chapters">Bölüm bul</string> <string name="search_chapters">Bölüm bul</string>
<string name="chapters_empty">Bu mangada bölüm yok</string> <string name="chapters_empty">Bu mangada bölüm yok</string>
@ -503,7 +503,7 @@
<string name="lock_screen_rotation">Ekran döndürmeyi kilitle</string> <string name="lock_screen_rotation">Ekran döndürmeyi kilitle</string>
<string name="error_search_not_supported">Arama bu manga kaynağı tarafından desteklenmemektedir</string> <string name="error_search_not_supported">Arama bu manga kaynağı tarafından desteklenmemektedir</string>
<string name="manual">Manuel</string> <string name="manual">Manuel</string>
<string name="disable_nsfw_summary">Eğer mümkünse yetişkin içerik ve NSFW kaynaklarını arama listesinden kaldır</string> <string name="disable_nsfw_summary">NSFW kaynakları devre dışı bırak ve yetişkinlere yönelik mangaları listeden gizle</string>
<string name="available_d">Mevcut:%1$d</string> <string name="available_d">Mevcut:%1$d</string>
<string name="state">Durum</string> <string name="state">Durum</string>
<string name="error_filter_states_genre_not_supported">Hem türlere hem de duruma göre filtreleme bu kaynak tarafından desteklenmiyor</string> <string name="error_filter_states_genre_not_supported">Hem türlere hem de duruma göre filtreleme bu kaynak tarafından desteklenmiyor</string>
@ -867,4 +867,24 @@
<string name="enable_pull_gesture_title">Çekme hareketini etkinleştir</string> <string name="enable_pull_gesture_title">Çekme hareketini etkinleştir</string>
<string name="enable_pull_gesture_summary">Webtoon modunda bölüm değiştirmek için çekme hareketi kullan</string> <string name="enable_pull_gesture_summary">Webtoon modunda bölüm değiştirmek için çekme hareketi kullan</string>
<string name="two_page_scroll_sensitivity">İki Sayfa Kaydırma Hassaslığı</string> <string name="two_page_scroll_sensitivity">İki Sayfa Kaydırma Hassaslığı</string>
<string name="saved_filters">Kaydedilen filtreler</string>
<string name="enter_name">Ad girin</string>
<string name="reader_chapter_toast">Bölüm değişikliği açılır penceresini göster</string>
<string name="reader_chapter_toast_summary">Değiştirildiğinde bölüm başlığını içeren bir açılır pencere mesajı göster</string>
<string name="rename">Yeniden adlandır</string>
<string name="save_filter">Filtreyi kaydet</string>
<string name="overwrite">Üzerine yaz</string>
<string name="filter_overwrite_confirm">\"%s\" adında bir filtre zaten var. Üzerine yazmak istiyor musunuz?</string>
<string name="storage_and_network">Depolama ve ağ</string>
<string name="create_or_restore_backup">Yedekleme oluştur veya geri yükle</string>
<string name="data_removal">Verilerin silinmesi</string>
<string name="privacy">Gizlilik</string>
<string name="source_broken_warning">Bu manga kaynağı bozuk olarak işaretlendi. Bazı özellikler çalışmayabilir</string>
<string name="download_default_directory">Mangaların indirileceği varsayılan dizin</string>
<string name="private_app_directory_warning">Uygulamayı kaldırırsanız, bu dizin ve içindeki tüm veriler silinecektir</string>
<string name="available_pattern">%1$s var</string>
<string name="frequency_every_6_hours">6 saatte bir</string>
<string name="pinned_sources_only">Yalnızca sabitlenen kaynaklar</string>
<string name="hide_empty_sources">Boş kaynakları gizle</string>
<string name="auto_double_foldable">Otomatik İki Sayfa Katlanabilir</string>
</resources> </resources>

@ -832,9 +832,9 @@
<string name="error_disclaimer_report">Ви можете надіслати звіт про помилку розробникам. Це допоможе нам виправити проблему.</string> <string name="error_disclaimer_report">Ви можете надіслати звіт про помилку розробникам. Це допоможе нам виправити проблему.</string>
<string name="error_disclaimer_app_outdated">Схоже, що ваша версія Kotatsu застаріла. Будь ласка, установіть останню версію, щоб отримати всі доступні виправлення.</string> <string name="error_disclaimer_app_outdated">Схоже, що ваша версія Kotatsu застаріла. Будь ласка, установіть останню версію, щоб отримати всі доступні виправлення.</string>
<string name="error_disclaimer_manga">Спробуйте відкрити манґу в браузері, щоб переконатися, що вона доступна джерелом.</string> <string name="error_disclaimer_manga">Спробуйте відкрити манґу в браузері, щоб переконатися, що вона доступна джерелом.</string>
<string name="handle_links_summary">Обробка посилань на мангу з зовнішніх програм (наприклад, веб-браузера). Можливо, вам також доведеться ввімкнути цю функцію вручну в системних налаштуваннях програми.</string> <string name="handle_links_summary">Обробка посилань на мангу з зовнішніх програм (наприклад, веб-браузера). Можливо, вам також доведеться ввімкнути цю функцію вручну в системних налаштуваннях програми</string>
<string name="disable_captcha_notifications_summary">Ви не будете отримувати повідомлення про вирішення CAPTCHA для цього джерела, але це може призвести до порушення фонових операцій (перевірка нових розділів, отримання рекомендацій тощо).</string> <string name="disable_captcha_notifications_summary">Ви не будете отримувати повідомлення про вирішення CAPTCHA для цього джерела, але це може призвести до порушення фонових операцій (перевірка нових розділів, отримання рекомендацій тощо)</string>
<string name="clear_browser_data_summary">Очистити дані браузера, такі як кеш і файли cookie. Попередження: авторизація в джерелах манги може стати недійсною.</string> <string name="clear_browser_data_summary">Очистити дані браузера, такі як кеш і файли cookie. Попередження: авторизація в джерелах манги може стати недійсною</string>
<string name="local_storage_cleanup">Очищення локального сховища</string> <string name="local_storage_cleanup">Очищення локального сховища</string>
<string name="packup_creation_failed">Не вдалося створити резервну копію</string> <string name="packup_creation_failed">Не вдалося створити резервну копію</string>
<string name="main_screen">Головний екран</string> <string name="main_screen">Головний екран</string>
@ -867,4 +867,24 @@
<string name="enable_pull_gesture_title">Увімкнути жест потягування</string> <string name="enable_pull_gesture_title">Увімкнути жест потягування</string>
<string name="enable_pull_gesture_summary">Використовуйте жест потягування для перемикання розділів у вебтуні</string> <string name="enable_pull_gesture_summary">Використовуйте жест потягування для перемикання розділів у вебтуні</string>
<string name="two_page_scroll_sensitivity">Чутливість прокручування на дві сторінки</string> <string name="two_page_scroll_sensitivity">Чутливість прокручування на дві сторінки</string>
<string name="saved_filters">Збережені фільтри</string>
<string name="enter_name">Введіть ім\'я</string>
<string name="reader_chapter_toast">Показати спливаюче вікно зміни розділу</string>
<string name="reader_chapter_toast_summary">Показувати спливаюче повідомлення з назвою розділу, коли її змінено</string>
<string name="rename">Перейменувати</string>
<string name="save_filter">Зберегти фільтр</string>
<string name="overwrite">Перезаписати</string>
<string name="filter_overwrite_confirm">Фільтр з назвою \"%s\" вже існує. Перезаписати його?</string>
<string name="storage_and_network">Зберігання та мережа</string>
<string name="create_or_restore_backup">Створення або відновлення резервної копії</string>
<string name="data_removal">Вилучення даних</string>
<string name="privacy">Конфіденційність</string>
<string name="source_broken_warning">Це джерело манги позначено як непрацююче. Деякі функції можуть не працювати</string>
<string name="frequency_every_6_hours">Кожні 6 годин</string>
<string name="download_default_directory">Каталог за замовчуванням для завантаження манги</string>
<string name="private_app_directory_warning">Цей каталог з усіма даними буде видалено, якщо ви видалите програму</string>
<string name="available_pattern">Доступно %1$s</string>
<string name="auto_double_foldable">Автоматичне двосторінкове розміщення на складному</string>
<string name="pinned_sources_only">Тільки закріплені джерела</string>
<string name="hide_empty_sources">Приховати порожні джерела</string>
</resources> </resources>

@ -864,4 +864,22 @@
<string name="pull_bottom_no_next">Không còn chương nào để xem tiếp</string> <string name="pull_bottom_no_next">Không còn chương nào để xem tiếp</string>
<string name="enable_pull_gesture_title">Bật cử chỉ kéo - thả</string> <string name="enable_pull_gesture_title">Bật cử chỉ kéo - thả</string>
<string name="enable_pull_gesture_summary">Sử dụng cử chỉ tay kéo - thả để chuyển chương trong chế độ đọc Webtoon</string> <string name="enable_pull_gesture_summary">Sử dụng cử chỉ tay kéo - thả để chuyển chương trong chế độ đọc Webtoon</string>
<string name="saved_filters">Lưu bộ lọc tìm kiếm</string>
<string name="enter_name">Hãy điền tên</string>
<string name="two_page_scroll_sensitivity">Độ nhạy khi cuộn hai trang</string>
<string name="reader_chapter_toast">Hiện thông báo chuyển chương</string>
<string name="reader_chapter_toast_summary">Hiển thị một bong bóng nổi nho nhỏ ở phía dưới màn hình cùng với tên của chương truyện khi bạn đọc sang một chương truyện mới</string>
<string name="rename">Đặt tên</string>
<string name="save_filter">Lưu bộ lọc</string>
<string name="overwrite">Đè lên</string>
<string name="filter_overwrite_confirm">Một bộ lộc có tên là \"%s\" đã tồn tại trước đó. Bạn có muốn ghi đè nó lên không?</string>
<string name="storage_and_network">Lưu trữ và mạng</string>
<string name="create_or_restore_backup">Tạo hoặc khôi phục từ một bản sao lưu</string>
<string name="data_removal">Xoá dữ liệu</string>
<string name="privacy">Quyền riêng tư</string>
<string name="source_broken_warning">Nguồn đọc này đã được gắn là \"Không còn sử dụng được\". Một vài tính năng có thể sẽ không hoạt động</string>
<string name="frequency_every_6_hours">6 tiếng một lần</string>
<string name="download_default_directory">Đường dẫn mặc định để tải truyện về thiết bị</string>
<string name="private_app_directory_warning">Đường dẫn này với tất cả dữ liệu của ứng dụng sẽ bị xóa nếu bạn gỡ ứng dụng Kotatsu</string>
<string name="available_pattern">Hiện có sẵn %1$s</string>
</resources> </resources>

@ -866,4 +866,5 @@
<string name="pull_bottom_no_next">到底了</string> <string name="pull_bottom_no_next">到底了</string>
<string name="enable_pull_gesture_title">启用推拉手势</string> <string name="enable_pull_gesture_title">启用推拉手势</string>
<string name="enable_pull_gesture_summary">条漫模式下使用推拉手势切换章节</string> <string name="enable_pull_gesture_summary">条漫模式下使用推拉手势切换章节</string>
<string name="auto_double_foldable">折叠设备自动双页</string>
</resources> </resources>

@ -661,4 +661,5 @@
<string name="download_new_chapters">下載新的漫畫章節</string> <string name="download_new_chapters">下載新的漫畫章節</string>
<string name="enable_all_sources">啟用所有漫畫來源</string> <string name="enable_all_sources">啟用所有漫畫來源</string>
<string name="all_sources_enabled">所有漫畫來源已啟用</string> <string name="all_sources_enabled">所有漫畫來源已啟用</string>
<string name="auto_double_foldable">摺疊設備自動雙頁</string>
</resources> </resources>

@ -75,6 +75,7 @@
<item>@string/advanced</item> <item>@string/advanced</item>
</string-array> </string-array>
<string-array name="backup_frequency" translatable="false"> <string-array name="backup_frequency" translatable="false">
<item>@string/frequency_every_6_hours</item>
<item>@string/frequency_every_day</item> <item>@string/frequency_every_day</item>
<item>@string/frequency_every_2_days</item> <item>@string/frequency_every_2_days</item>
<item>@string/frequency_once_per_week</item> <item>@string/frequency_once_per_week</item>

@ -56,6 +56,7 @@
<item>SOCKS</item> <item>SOCKS</item>
</string-array> </string-array>
<string-array name="values_backup_frequency" translatable="false"> <string-array name="values_backup_frequency" translatable="false">
<item>0.25</item>
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>
<item>7</item> <item>7</item>

@ -210,6 +210,8 @@
<string name="disabled">Disabled</string> <string name="disabled">Disabled</string>
<string name="reset_filter">Reset filter</string> <string name="reset_filter">Reset filter</string>
<string name="enter_name">Enter name</string> <string name="enter_name">Enter name</string>
<string name="pinned_sources_only">Pinned sources only</string>
<string name="hide_empty_sources">Hide empty sources</string>
<string name="onboard_text">Select languages which you want to read manga. You can change it later in settings.</string> <string name="onboard_text">Select languages which you want to read manga. You can change it later in settings.</string>
<string name="never">Never</string> <string name="never">Never</string>
<string name="only_using_wifi">Only on Wi-Fi</string> <string name="only_using_wifi">Only on Wi-Fi</string>
@ -498,6 +500,7 @@
<string name="online_variant">Online variant</string> <string name="online_variant">Online variant</string>
<string name="periodic_backups">Periodic backups</string> <string name="periodic_backups">Periodic backups</string>
<string name="backup_frequency">Backup creation frequency</string> <string name="backup_frequency">Backup creation frequency</string>
<string name="frequency_every_6_hours">Every 6 hours</string>
<string name="frequency_every_day">Every day</string> <string name="frequency_every_day">Every day</string>
<string name="frequency_every_2_days">Every 2 days</string> <string name="frequency_every_2_days">Every 2 days</string>
<string name="frequency_once_per_week">Once per week</string> <string name="frequency_once_per_week">Once per week</string>
@ -578,6 +581,7 @@
<string name="none">None</string> <string name="none">None</string>
<string name="config_reset_confirm">Reset settings to default values? This action cannot be undone.</string> <string name="config_reset_confirm">Reset settings to default values? This action cannot be undone.</string>
<string name="use_two_pages_landscape">Use two pages layout on landscape orientation (beta)</string> <string name="use_two_pages_landscape">Use two pages layout on landscape orientation (beta)</string>
<string name="auto_double_foldable">Auto Two-Page On Foldable</string>
<string name="two_page_scroll_sensitivity">Two-Page Scroll Sensitivity</string> <string name="two_page_scroll_sensitivity">Two-Page Scroll Sensitivity</string>
<string name="default_webtoon_zoom_out">Default webtoon zoom out</string> <string name="default_webtoon_zoom_out">Default webtoon zoom out</string>
<string name="fullscreen_mode">Fullscreen mode</string> <string name="fullscreen_mode">Fullscreen mode</string>
@ -900,4 +904,7 @@
<string name="data_removal">Data removal</string> <string name="data_removal">Data removal</string>
<string name="privacy">Privacy</string> <string name="privacy">Privacy</string>
<string name="source_broken_warning">This manga source has been marked as broken. Some features may not work</string> <string name="source_broken_warning">This manga source has been marked as broken. Some features may not work</string>
<string name="download_default_directory">Default directory for downloading manga</string>
<string name="private_app_directory_warning">This directory with all data will be deleted if you uninstall the application</string>
<string name="available_pattern">%1$s available</string>
</resources> </resources>

@ -87,6 +87,12 @@
android:title="@string/pages_animation" android:title="@string/pages_animation"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="webtoon_pull_gesture"
android:summary="@string/enable_pull_gesture_summary"
android:title="@string/enable_pull_gesture_title" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="enhanced_colors" android:key="enhanced_colors"

@ -35,7 +35,7 @@ material = "1.14.0-alpha05"
moshi = "1.15.2" moshi = "1.15.2"
okhttp = "5.2.1" okhttp = "5.2.1"
okio = "3.16.1" okio = "3.16.1"
parsers = "df1cab3f9d" parsers = "4d1e521aef"
preference = "1.2.1" preference = "1.2.1"
recyclerview = "1.4.0" recyclerview = "1.4.0"
room = "2.7.2" room = "2.7.2"
@ -49,6 +49,7 @@ viewpager2 = "1.1.0"
webkit = "1.14.0" webkit = "1.14.0"
workRuntime = "2.10.5" workRuntime = "2.10.5"
workinspector = "1.2" workinspector = "1.2"
window = "1.3.0"
[libraries] [libraries]
acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" } acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" }
@ -115,6 +116,7 @@ okhttp-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref = "okhttp
okio = { module = "com.squareup.okio:okio", version.ref = "okio" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
ssiv = { module = "com.github.KotatsuApp:subsampling-scale-image-view", version.ref = "ssiv" } ssiv = { module = "com.github.KotatsuApp:subsampling-scale-image-view", version.ref = "ssiv" }
workinspector = { module = "com.github.Koitharu:WorkInspector", version.ref = "workinspector" } workinspector = { module = "com.github.Koitharu:WorkInspector", version.ref = "workinspector" }
androidx-window = { module = "androidx.window:window", version.ref = "window" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "gradle" } android-application = { id = "com.android.application", version.ref = "gradle" }

Loading…
Cancel
Save