Compare commits

...

55 Commits

Author SHA1 Message Date
Koitharu
439a01c43f Fix bookmark has direct url detection #424 2023-07-18 11:43:31 +03:00
Koitharu
3a9d0def7d Update parsers 2023-07-18 10:13:46 +03:00
Koitharu
e4c80b4443 Remove rubbish file 2023-07-17 14:14:23 +03:00
Koitharu
940d448e00 Fix local manga update on shelf 2023-07-17 14:13:16 +03:00
Koitharu
5ab48a7545 Fix scrobbling rating 2023-07-17 13:30:50 +03:00
Koitharu
cb2bdbdd9a Update parsers 2023-07-17 13:08:39 +03:00
Cookies
8fdaf92cc4 Translated using Weblate (Vietnamese)
Currently translated at 89.2% (399 of 447 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-07-17 12:39:46 +03:00
Shubham Niraula
0416077964 Translated using Weblate (Nepali)
Currently translated at 51.9% (232 of 447 strings)

Co-authored-by: Shubham Niraula <niraulas018@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2023-07-17 12:39:46 +03:00
Koitharu
7b60ed6bad Fix new sources dialog list 2023-07-13 13:12:21 +03:00
Cookies
619be69580 Translated using Weblate (Vietnamese)
Currently translated at 89.2% (399 of 447 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-07-12 11:00:13 +03:00
Shubham Niraula
9f3c3f8985 Translated using Weblate (Nepali)
Currently translated at 51.2% (229 of 447 strings)

Co-authored-by: Shubham Niraula <niraulas018@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2023-07-12 11:00:13 +03:00
Vítor Fernandes Almado
f345977858 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: Vítor Fernandes Almado <vfalmado@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2023-07-12 11:00:13 +03:00
Koitharu
9610caf002 Downloads scheduler fixes 2023-07-12 10:58:02 +03:00
Koitharu
b75220a1b7 Fix cover loading in details 2023-07-11 11:45:57 +03:00
Koitharu
ab2a6f5a17 Fix loading state 2023-07-11 11:34:47 +03:00
Koitharu
2aeefc607b Udpate dependencies 2023-07-11 11:09:45 +03:00
Shubham Niraula
9af769bc69 Translated using Weblate (Nepali)
Currently translated at 50.3% (225 of 447 strings)

Co-authored-by: Shubham Niraula <niraulas018@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Pluto
46b78cfcd7 Translated using Weblate (Czech)
Currently translated at 100.0% (6 of 6 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (447 of 447 strings)

Added translation using Weblate (Czech)

Added translation using Weblate (Czech)

Co-authored-by: Pluto <notemailprotected@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-07-11 09:54:06 +03:00
Nguyễn Mạnh Hùng
c24324de9a Translated using Weblate (Vietnamese)
Currently translated at 81.8% (366 of 447 strings)

Co-authored-by: Nguyễn Mạnh Hùng <hungmn13@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Hosted Weblate
48b9c1236d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
plum7x
c69d293caa Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (447 of 447 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.5% (436 of 447 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (435 of 447 strings)

Co-authored-by: plum7x <plumgift@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Clxff H3r4ld0
0f4cca0e07 Translated using Weblate (Indonesian)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Luiz-bro
d6500b8fec Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.1% (443 of 447 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Detrimental God
86140cab1e Added translation using Weblate (Malayalam)
Co-authored-by: Detrimental God <judeliger1@gmail.com>
2023-07-11 09:54:06 +03:00
Koitharu
90dfc84119 Update dependencies 2023-07-01 12:54:44 +03:00
Koitharu
6a792f8ac3 Use CoroutineStart.ATOMIC in some cases 2023-06-30 14:04:22 +03:00
Koitharu
c81e8749b6 Update parsers and headers processing 2023-06-28 13:27:26 +03:00
ztimms73
5fa260a0c7 Update issue template 2023-06-28 03:14:30 +03:00
Koitharu
e0ba4e2686 Remove unused code 2023-06-27 12:52:41 +03:00
Koitharu
f188d1c0f3 Remove ongoing flag from background work notifications 2023-06-27 12:34:12 +03:00
CakesTwix
6de55afa27 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: CakesTwix <cakestwix1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
J. Lavoie
21dcb5b754 Translated using Weblate (French)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
gallegonovato
9b3ea57db1 Translated using Weblate (Spanish)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
kuragehime
032a8607ba Translated using Weblate (Japanese)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
Макар Разин
f7303c5957 Translated using Weblate (Serbian)
Currently translated at 29.3% (131 of 447 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (445 of 447 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (447 of 447 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
Koitharu
d696606ef9 Misc fixes 2023-06-27 10:28:47 +03:00
Koitharu
0a6e106a1d Filter local manga files 2023-06-24 13:18:09 +03:00
Koitharu
de1a7f0ca8 Fix IndexOutOfBoundsException in RemoteViewsFactory 2023-06-24 09:38:13 +03:00
Koitharu
9d31e76cc7 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Cookies
20910ffb5d Translated using Weblate (Vietnamese)
Currently translated at 81.2% (360 of 443 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Clxff H3r4ld0
7497ee6364 Translated using Weblate (Indonesian)
Currently translated at 100.0% (443 of 443 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Abay Emes
0f2ed50e18 Translated using Weblate (Kazakh)
Currently translated at 48.8% (213 of 436 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
BlackSpectrum
ba066b577b Translated using Weblate (Hindi)
Currently translated at 15.5% (68 of 436 strings)

Co-authored-by: BlackSpectrum <tittan5000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
CakesTwix
4496fe876f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (436 of 436 strings)

Co-authored-by: CakesTwix <cakestwix1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
gallegonovato
a9f5abebf0 Translated using Weblate (Spanish)
Currently translated at 100.0% (443 of 443 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (436 of 436 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
qrynill
bebee2ef27 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 91.7% (399 of 435 strings)

Co-authored-by: qrynill <tryvseu@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Макар Разин
4ec2b0c8fe Translated using Weblate (Vietnamese)
Currently translated at 79.3% (345 of 435 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Korean)

Currently translated at 75.1% (327 of 435 strings)

Translated using Weblate (Greek)

Currently translated at 19.3% (84 of 435 strings)

Translated using Weblate (Serbian)

Currently translated at 28.2% (123 of 435 strings)

Translated using Weblate (Arabic)

Currently translated at 18.1% (79 of 435 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Italian)

Currently translated at 85.2% (371 of 435 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/el/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Koitharu
4a7be70898 Update queries in manifest 2023-06-23 16:14:41 +03:00
Koitharu
2bcba1eb21 Configure manga directories 2023-06-22 13:45:29 +03:00
Koitharu
feca7ba3fc Support for custom directories for manga 2023-06-22 10:11:11 +03:00
Koitharu
745b349e5e Ability to remove item from updates 2023-06-21 15:27:20 +03:00
Koitharu
13946783a5 Fix crashes 2023-06-21 15:06:01 +03:00
Koitharu
84e5400522 Download options dialog 2023-06-21 14:54:11 +03:00
Koitharu
02c9a933d2 Fix offline manga details 2023-06-20 17:06:18 +03:00
Koitharu
92af851d3b Option to clear single source cookies 2023-06-20 13:43:09 +03:00
122 changed files with 5245 additions and 2424 deletions

View File

@@ -61,4 +61,6 @@ body:
label: Acknowledgements
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
required: true
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
required: true

1
.idea/.gitignore generated vendored
View File

@@ -1,3 +1,4 @@
# Default ignored files
/shelf/
/workspace.xml
/migrations.xml

3
.idea/gradle.xml generated
View File

@@ -4,8 +4,6 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
@@ -14,6 +12,7 @@
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View File

@@ -14,9 +14,11 @@ android {
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
//TODO: update as soon as sources becomes available
//noinspection OldTargetApi
targetSdkVersion 33
versionCode 555
versionName '5.2.3'
versionCode 563
versionName '5.3.6'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -79,12 +81,12 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:86a82970fc') {
implementation('com.github.KotatsuApp:kotatsu-parsers:a1598fd712') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'
@@ -107,53 +109,54 @@ dependencies {
// TODO https://issuetracker.google.com/issues/254846063
implementation 'androidx.work:work-runtime-ktx:2.8.1'
//noinspection GradleDependency
implementation('com.google.guava:guava:32.0.0-android') {
implementation('com.google.guava:guava:32.0.1-android') {
exclude group: 'com.google.guava', module: 'failureaccess'
exclude group: 'org.checkerframework', module: 'checker-qual'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
}
implementation 'androidx.room:room-runtime:2.5.1'
implementation 'androidx.room:room-ktx:2.5.1'
kapt 'androidx.room:room-compiler:2.5.1'
implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.room:room-ktx:2.5.2'
//noinspection KaptUsageInsteadOfKsp
kapt 'androidx.room:room-compiler:2.5.2'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
implementation 'com.squareup.okio:okio:3.3.0'
implementation 'com.squareup.okio:okio:3.4.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.46.1'
kapt 'com.google.dagger:hilt-compiler:2.46.1'
implementation 'com.google.dagger:hilt-android:2.47'
kapt 'com.google.dagger:hilt-compiler:2.47'
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.4.0'
implementation 'io.coil-kt:coil-svg:2.4.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:9b1d20be67'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'ch.acra:acra-http:5.9.7'
implementation 'ch.acra:acra-dialog:5.9.7'
implementation 'ch.acra:acra-http:5.10.1'
implementation 'ch.acra:acra-dialog:5.10.1'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20230227'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
testImplementation 'org.json:json:20230618'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2'
androidTestImplementation 'androidx.room:room-testing:2.5.1'
androidTestImplementation 'androidx.room:room-testing:2.5.2'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.47'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.47'
}

View File

@@ -18,6 +18,22 @@
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<intent>
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
</intent>
</queries>
<application
android:name="org.koitharu.kotatsu.KotatsuApp"
@@ -32,6 +48,7 @@
android:largeHeap="true"
android:localeConfig="@xml/locales"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Kotatsu"
@@ -98,6 +115,9 @@
<data android:host="sync-settings" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity"
android:label="@string/local_manga_directories" />
<activity
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.local.data.ImageFileFilter
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import java.util.Date
@@ -26,7 +27,8 @@ class Bookmark(
)
private fun isImageUrlDirect(): Boolean {
return imageUrl.substringAfterLast('.').length in 2..4
val extension = imageUrl.substringAfterLast('.')
return extension.isNotEmpty() && ImageFileFilter().isExtensionValid(extension)
}
override fun equals(other: Any?): Boolean {

View File

@@ -5,15 +5,19 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.drawable.TextDrawable
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeResId
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
import org.koitharu.kotatsu.parsers.util.format
import com.google.android.material.R as materialR
fun bookmarkListAD(
coil: ImageLoader,

View File

@@ -8,6 +8,7 @@ import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.webkit.CookieManager
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
@@ -16,6 +17,7 @@ import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled")
@@ -34,8 +36,9 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
with(viewBinding.webView.settings) {
javaScriptEnabled = true
userAgentString = CommonHeadersInterceptor.userAgentChrome
userAgentString = UserAgents.CHROME_MOBILE
}
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -49,10 +50,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
val url = intent?.dataString.orEmpty()
with(viewBinding.webView.settings) {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback
userAgentString = intent?.getStringExtra(ARG_UA) ?: UserAgents.CHROME_MOBILE
}
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {

View File

@@ -33,11 +33,21 @@ abstract class ErrorObserver(
return resolver != null && ExceptionResolver.canResolve(error)
}
private fun isAlive(): Boolean {
return when {
fragment != null -> fragment.view != null
activity != null -> !activity.isDestroyed
else -> true
}
}
protected fun resolve(error: Throwable) {
lifecycleScope.launch {
val isResolved = resolver?.resolve(error) ?: false
if (isActive) {
onResolved?.accept(isResolved)
if (isAlive()) {
lifecycleScope.launch {
val isResolved = resolver?.resolve(error) ?: false
if (isActive) {
onResolved?.accept(isResolved)
}
}
}
}

View File

@@ -0,0 +1,17 @@
package org.koitharu.kotatsu.core.exceptions.resolve
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
class ToastErrorObserver(
host: View,
fragment: Fragment?,
) : ErrorObserver(host, fragment, null, null) {
override suspend fun emit(value: Throwable) {
val toast = Toast.makeText(host.context, value.getDisplayMessage(host.context.resources), Toast.LENGTH_SHORT)
toast.show()
}
}

View File

@@ -8,10 +8,14 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
@JvmName("mangaIds")
fun Collection<Manga>.ids() = mapToSet { it.id }
fun Collection<Manga>.distinctById() = distinctBy { it.id }
@JvmName("chaptersIds")
fun Collection<MangaChapter>.ids() = mapToSet { it.id }
fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
if (size <= 1) {
return size

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.network
import android.os.Build
import android.util.Log
import dagger.Lazy
import okhttp3.Headers
@@ -10,11 +9,11 @@ import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mergeWith
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.mergeWith
import java.net.IDN
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -39,7 +38,7 @@ class CommonHeadersInterceptor @Inject constructor(
headersBuilder.mergeWith(it, replaceExisting = false)
}
if (headersBuilder[CommonHeaders.USER_AGENT] == null) {
headersBuilder[CommonHeaders.USER_AGENT] = userAgentFallback
headersBuilder[CommonHeaders.USER_AGENT] = UserAgents.CHROME_MOBILE
}
if (headersBuilder[CommonHeaders.REFERER] == null && repository != null) {
val idn = IDN.toASCII(repository.domain)
@@ -62,26 +61,4 @@ class CommonHeadersInterceptor @Inject constructor(
override fun request(): Request = request
}
companion object {
val userAgentFallback
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME,
Build.VERSION.RELEASE,
Build.MODEL,
Build.BRAND,
Build.DEVICE,
Locale.getDefault().language,
)
val userAgentChrome
get() = (
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/100.0.4896.127 Mobile Safari/537.36"
).format(
Build.VERSION.RELEASE,
Build.MODEL,
)
}
}

View File

@@ -4,6 +4,7 @@ import android.webkit.CookieManager
import androidx.annotation.WorkerThread
import okhttp3.Cookie
import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.util.ext.newBuilder
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -30,6 +31,21 @@ class AndroidCookieJar : MutableCookieJar {
}
}
override fun removeCookies(url: HttpUrl) {
val cookies = loadForRequest(url)
if (cookies.isEmpty()) {
return
}
val urlString = url.toString()
for (c in cookies) {
val nc = c.newBuilder()
.expiresAt(System.currentTimeMillis() - 100000)
.build()
cookieManager.setCookie(urlString, nc.toString())
}
check(loadForRequest(url).isEmpty())
}
override suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
cookieManager.removeAllCookies(continuation::resume)
}

View File

@@ -13,5 +13,8 @@ interface MutableCookieJar : CookieJar {
@WorkerThread
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
@WorkerThread
fun removeCookies(url: HttpUrl)
suspend fun clear(): Boolean
}

View File

@@ -21,6 +21,7 @@ class PreferencesCookieJar(
private var isLoaded = false
@WorkerThread
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
loadPersistent()
val expired = HashSet<String>()
@@ -40,6 +41,7 @@ class PreferencesCookieJar(
}
@WorkerThread
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val wrapped = cookies.map { CookieWrapper(it) }
prefs.edit(commit = true) {
@@ -53,6 +55,22 @@ class PreferencesCookieJar(
}
}
@Synchronized
@WorkerThread
override fun removeCookies(url: HttpUrl) {
loadPersistent()
val toRemove = HashSet<String>()
for ((key, cookie) in cache) {
if (cookie.isExpired() || cookie.cookie.matches(url)) {
toRemove += key
}
}
if (toRemove.isNotEmpty()) {
cache.removeAll(toRemove)
removePersistent(toRemove)
}
}
override suspend fun clear(): Boolean {
cache.clear()
withContext(Dispatchers.IO) {

View File

@@ -9,6 +9,6 @@ fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaPa
return if (source == MangaSource.DUMMY) {
DummyParser(loaderContext)
} else {
source.newParser(loaderContext)
loaderContext.newParserInstance(source)
}
}
}

View File

@@ -21,9 +21,11 @@ import org.koitharu.kotatsu.core.util.ext.filterToSet
import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
import java.io.File
@@ -234,14 +236,28 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
}.getOrDefault(ScreenshotsPolicy.ALLOW)
var userSpecifiedMangaDirectories: Set<File>
get() {
val set = prefs.getStringSet(KEY_LOCAL_MANGA_DIRS, emptySet()).orEmpty()
return set.mapNotNullToSet { File(it).takeIfReadable() }
}
set(value) {
val set = value.mapToSet { it.absolutePath }
prefs.edit { putStringSet(KEY_LOCAL_MANGA_DIRS, set) }
}
var mangaStorageDir: File?
get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
File(it)
}?.takeIf { it.exists() }
}?.takeIf { it.exists() && it in userSpecifiedMangaDirectories }
set(value) = prefs.edit {
if (value == null) {
remove(KEY_LOCAL_STORAGE)
} else {
val userDirs = userSpecifiedMangaDirectories
if (value !in userDirs) {
userSpecifiedMangaDirectories = userDirs + value
}
putString(KEY_LOCAL_STORAGE, value.path)
}
}
@@ -461,6 +477,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROXY_LOGIN = "proxy_login"
const val KEY_PROXY_PASSWORD = "proxy_password"
const val KEY_IMAGES_PROXY = "images_proxy"
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
// About
const val KEY_APP_UPDATE = "app_update"

View File

@@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
@@ -103,8 +104,7 @@ abstract class BaseActivity<B : ViewBinding> :
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
// ActivityCompat.recreate(this)
error("Test")
ActivityCompat.recreate(this)
return true
}
return super.onKeyDown(keyCode, event)

View File

@@ -34,7 +34,7 @@ abstract class BaseViewModel : ViewModel() {
val isLoading: StateFlow<Boolean>
get() = loadingCounter.map { it > 0 }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), loadingCounter.value > 0)
protected fun launchJob(
context: CoroutineContext = EmptyCoroutineContext,

View File

@@ -1,101 +0,0 @@
package org.koitharu.kotatsu.core.ui.dialog
import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemStorageBinding
import org.koitharu.kotatsu.local.data.LocalStorageManager
import java.io.File
class StorageSelectDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
fun show() = delegate.show()
class Builder(context: Context, storageManager: LocalStorageManager, listener: OnStorageSelectListener) {
private val adapter = VolumesAdapter(storageManager)
private val delegate = MaterialAlertDialogBuilder(context)
init {
if (adapter.isEmpty) {
delegate.setMessage(R.string.cannot_find_available_storage)
} else {
val defaultValue = runBlocking {
storageManager.getDefaultWriteableDir()
}
adapter.selectedItemPosition = adapter.volumes.indexOfFirst {
it.first.canonicalPath == defaultValue?.canonicalPath
}
delegate.setAdapter(adapter) { d, i ->
listener.onStorageSelected(adapter.getItem(i).first)
d.dismiss()
}
}
}
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder {
delegate.setTitle(title)
return this
}
fun setNegativeButton(@StringRes textId: Int): Builder {
delegate.setNegativeButton(textId, null)
return this
}
fun create() = StorageSelectDialog(delegate.create())
}
private class VolumesAdapter(storageManager: LocalStorageManager) : BaseAdapter() {
var selectedItemPosition: Int = -1
val volumes = getAvailableVolumes(storageManager)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.item_storage, parent, false)
val binding = (view.tag as? ItemStorageBinding) ?: ItemStorageBinding.bind(view).also {
view.tag = it
}
val item = volumes[position]
binding.imageViewIndicator.isChecked = selectedItemPosition == position
binding.textViewTitle.text = item.second
binding.textViewSubtitle.text = item.first.path
return view
}
override fun getItem(position: Int): Pair<File, String> = volumes[position]
override fun getItemId(position: Int) = position.toLong()
override fun getCount() = volumes.size
override fun hasStableIds() = true
private fun getAvailableVolumes(storageManager: LocalStorageManager): List<Pair<File, String>> {
return runBlocking {
storageManager.getWriteableDirs().map {
it to storageManager.getStorageDisplayName(it)
}
}
}
}
fun interface OnStorageSelectListener {
fun onStorageSelected(file: File)
}
}

View File

@@ -0,0 +1,100 @@
package org.koitharu.kotatsu.core.ui.drawable
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import androidx.annotation.ColorInt
import androidx.annotation.Px
import androidx.annotation.StyleRes
import androidx.core.graphics.withTranslation
import com.google.android.material.resources.TextAppearance
import com.google.android.material.resources.TextAppearanceFontCallback
import org.koitharu.kotatsu.core.util.ext.getThemeColor
class TextDrawable(
val text: CharSequence,
) : Drawable() {
private val paint = TextPaint(Paint.ANTI_ALIAS_FLAG)
private var cachedLayout: StaticLayout? = null
@SuppressLint("RestrictedApi")
constructor(context: Context, text: CharSequence, @StyleRes textAppearanceId: Int) : this(text) {
val ta = TextAppearance(context, textAppearanceId)
paint.color = ta.textColor?.defaultColor ?: context.getThemeColor(android.R.attr.textColorPrimary, Color.BLACK)
paint.typeface = ta.fallbackFont
ta.getFontAsync(
context, paint,
object : TextAppearanceFontCallback() {
override fun onFontRetrieved(typeface: Typeface?, fontResolvedSynchronously: Boolean) = Unit
override fun onFontRetrievalFailed(reason: Int) = Unit
},
)
paint.letterSpacing = ta.letterSpacing
}
var alignment = Layout.Alignment.ALIGN_NORMAL
var lineSpacingMultiplier = 1f
@Px
var lineSpacingExtra = 0f
@get:ColorInt
var textColor: Int
get() = paint.color
set(@ColorInt value) {
paint.color = value
}
override fun draw(canvas: Canvas) {
val b = bounds
if (b.isEmpty) {
return
}
canvas.withTranslation(x = b.left.toFloat(), y = b.top.toFloat()) {
obtainLayout().draw(canvas)
}
}
override fun setAlpha(alpha: Int) {
paint.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.setColorFilter(colorFilter)
}
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
private fun obtainLayout(): StaticLayout {
val width = bounds.width()
cachedLayout?.let {
if (it.width == width) {
return it
}
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder.obtain(text, 0, text.length, paint, width)
.setAlignment(alignment)
.setLineSpacing(lineSpacingExtra, lineSpacingMultiplier)
.setIncludePad(true)
.build()
} else {
@Suppress("DEPRECATION")
StaticLayout(text, paint, width, alignment, lineSpacingMultiplier, lineSpacingExtra, true)
}.also { cachedLayout = it }
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.ui.util
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
@@ -13,7 +14,7 @@ fun interface ReversibleHandle {
suspend fun reverse()
}
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default, CoroutineStart.ATOMIC) {
runCatchingCancellable {
withContext(NonCancellable) {
reverse()

View File

@@ -24,6 +24,7 @@ import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ViewTwoLinesItemBinding
@SuppressLint("RestrictedApi")
@@ -35,6 +36,18 @@ class TwoLinesItemView @JvmOverloads constructor(
private val binding = ViewTwoLinesItemBinding.inflate(LayoutInflater.from(context), this)
var title: CharSequence?
get() = binding.title.text
set(value) {
binding.title.text = value
}
var subtitle: CharSequence?
get() = binding.subtitle.textAndVisible
set(value) {
binding.subtitle.textAndVisible = value
}
init {
var textColors: ColorStateList? = null
context.withStyledAttributes(
@@ -76,8 +89,7 @@ class TwoLinesItemView @JvmOverloads constructor(
}
fun setIconResource(@DrawableRes resId: Int) {
val icon = if (resId != 0) ContextCompat.getDrawable(context, resId) else null
binding.icon.setImageDrawable(icon)
binding.icon.setImageResource(resId)
}
private fun createShapeDrawable(ta: TypedArray): InsetDrawable {

View File

@@ -1,6 +0,0 @@
package org.koitharu.kotatsu.core.util
fun interface BufferedObserver<T> {
fun onChanged(t: T, previous: T?)
}

View File

@@ -0,0 +1,90 @@
package org.koitharu.kotatsu.core.util.ext
import android.annotation.TargetApi
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import org.koitharu.kotatsu.parsers.util.removeSuffix
import java.io.File
import java.lang.reflect.Array as ArrayReflect
private const val PRIMARY_VOLUME_NAME = "primary"
fun Uri.resolveFile(context: Context): File? {
val volumeId = getVolumeIdFromTreeUri(this) ?: return null
val volumePath = getVolumePath(volumeId, context)?.removeSuffix(File.separatorChar) ?: return null
val documentPath = getDocumentPathFromTreeUri(this)?.removeSuffix(File.separatorChar) ?: return null
return File(
if (documentPath.isNotEmpty()) {
if (documentPath.startsWith(File.separator)) {
volumePath + documentPath
} else {
volumePath + File.separator + documentPath
}
} else {
volumePath
},
)
}
private fun getVolumePath(volumeId: String, context: Context): String? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getVolumePathForAndroid11AndAbove(volumeId, context)
} else {
getVolumePathBeforeAndroid11(volumeId, context)
}
}
private fun getVolumePathBeforeAndroid11(volumeId: String, context: Context): String? = runCatching {
val mStorageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume")
val getVolumeList = mStorageManager.javaClass.getMethod("getVolumeList")
val getUuid = storageVolumeClazz.getMethod("getUuid")
val getPath = storageVolumeClazz.getMethod("getPath")
val isPrimary = storageVolumeClazz.getMethod("isPrimary")
val result = getVolumeList.invoke(mStorageManager)
val length = ArrayReflect.getLength(checkNotNull(result))
(0 until length).firstNotNullOfOrNull { i ->
val storageVolumeElement = ArrayReflect.get(result, i)
val uuid = getUuid.invoke(storageVolumeElement) as String
val primary = isPrimary.invoke(storageVolumeElement) as Boolean
when {
primary && volumeId == PRIMARY_VOLUME_NAME -> getPath.invoke(storageVolumeElement) as String
uuid == volumeId -> getPath.invoke(storageVolumeElement) as String
else -> null
}
}
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
@TargetApi(Build.VERSION_CODES.R)
private fun getVolumePathForAndroid11AndAbove(volumeId: String, context: Context): String? = runCatching {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
storageManager.storageVolumes.firstNotNullOfOrNull { volume ->
if (volume.isPrimary && volumeId == PRIMARY_VOLUME_NAME) {
volume.directory?.path
} else {
val uuid = volume.uuid
if (uuid != null && uuid == volumeId) volume.directory?.path else null
}
}
}.onFailure {
it.printStackTraceDebug()
}.getOrNull()
private fun getVolumeIdFromTreeUri(treeUri: Uri): String? {
val docId = DocumentsContract.getTreeDocumentId(treeUri)
val split = docId.split(":".toRegex())
return split.firstOrNull()?.takeUnless { it.isEmpty() }
}
private fun getDocumentPathFromTreeUri(treeUri: Uri): String? {
val docId = DocumentsContract.getTreeDocumentId(treeUri)
val split: Array<String?> = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
return if (split.size >= 2 && split[1] != null) split[1] else File.separator
}

View File

@@ -4,6 +4,7 @@ import android.os.SystemClock
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
@@ -62,3 +63,23 @@ fun <T> Flow<T>.zipWithPrevious(): Flow<Pair<T?, T>> = flow {
emit(result)
}
}
@Suppress("UNCHECKED_CAST")
fun <T1, T2, T3, T4, T5, T6, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
transform: suspend (T1, T2, T3, T4, T5, T6) -> R
): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> ->
transform(
args[0] as T1,
args[1] as T2,
args[2] as T3,
args[3] as T4,
args[4] as T5,
args[5] as T6,
)
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.util.ext
import okhttp3.Cookie
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
@@ -38,3 +39,23 @@ fun Response.ensureSuccess() = apply {
throw IllegalStateException(message)
}
}
fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
c.name(name)
c.value(value)
if (persistent) {
c.expiresAt(expiresAt)
}
if (hostOnly) {
c.hostOnlyDomain(domain)
} else {
c.domain(domain)
}
c.path(path)
if (secure) {
c.secure()
}
if (httpOnly) {
c.httpOnly()
}
}

View File

@@ -60,3 +60,10 @@ fun Context.getThemeColorStateList(
) = obtainStyledAttributes(intArrayOf(resId)).use {
it.getColorStateList(0)
}
fun Context.getThemeResId(
@AttrRes resId: Int,
fallback: Int
): Int = obtainStyledAttributes(intArrayOf(resId)).use {
it.getResourceId(0, fallback)
}

View File

@@ -34,6 +34,7 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is FileNotFoundException -> resources.getString(R.string.file_not_found)
is AccessDeniedException -> resources.getString(R.string.no_access_to_file)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is SyncApiException,
is ContentUnavailableException,

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.core.util.ext
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.Toolbar
fun Toolbar.setNavigationIconSafe(@DrawableRes iconRes: Int, retry: Boolean = true) {
try {
setNavigationIcon(iconRes)
} catch (e: IllegalStateException) {
if (retry) {
post { setNavigationIconSafe(iconRes, retry = false) }
}
}
}

View File

@@ -30,6 +30,7 @@ class ChaptersBottomSheetMediator(
}
override fun onActionModeStarted(mode: ActionMode) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
lock()
}

View File

@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
@@ -66,6 +67,9 @@ class ChaptersFragment :
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
binding.textViewHolder.isVisible = it
}
viewModel.onSelectChapter.observeEvent(viewLifecycleOwner) {
selectionController?.onItemLongClick(it)
}
}
override fun onDestroyView() {

View File

@@ -43,6 +43,7 @@ import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.setNavigationIconSafe
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService
@@ -211,7 +212,7 @@ class DetailsActivity :
}
if (isExpanded) {
toolbar.addMenuProvider(chaptersMenuProvider)
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material)
toolbar.setNavigationIconSafe(materialR.drawable.abc_ic_clear_material)
} else {
toolbar.removeMenuProvider(chaptersMenuProvider)
toolbar.navigationIcon = null

View File

@@ -15,6 +15,7 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import coil.ImageLoader
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
@@ -296,7 +297,7 @@ class DetailsFragment :
private fun loadCover(manga: Manga) {
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
val lastResult = CoilUtils.result(requireViewBinding().imageViewCover)
if (lastResult?.request?.data == imageUrl) {
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
return
}
val request = ImageRequest.Builder(context ?: return)

View File

@@ -16,12 +16,11 @@ import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ShareHelper
import org.koitharu.kotatsu.details.ui.model.MangaBranch
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
@@ -30,7 +29,7 @@ class DetailsMenuProvider(
private val viewModel: DetailsViewModel,
private val snackbarHost: View,
private val appShortcutManager: AppShortcutManager,
) : MenuProvider {
) : MenuProvider, OnListItemClickListener<DownloadOption> {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_details, menu)
@@ -44,7 +43,7 @@ class DetailsMenuProvider(
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value == true) R.drawable.ic_heart else R.drawable.ic_heart_outline,
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
)
}
@@ -80,15 +79,7 @@ class DetailsMenuProvider(
}
R.id.action_save -> {
viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0
val branches = viewModel.branches.value.orEmpty()
if (chaptersCount > 5 || branches.size > 1) {
showSaveConfirmation(it, chaptersCount, branches)
} else {
viewModel.download(null)
}
}
DownloadDialogHelper(snackbarHost, viewModel).show(this)
}
R.id.action_browser -> {
@@ -125,35 +116,16 @@ class DetailsMenuProvider(
return true
}
private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List<MangaBranch>) {
val dialogBuilder = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.save_manga)
.setNegativeButton(android.R.string.cancel, null)
if (branches.size > 1) {
val items = Array(branches.size) { i -> branches[i].name.orEmpty() }
val currentBranch = branches.indexOfFirst { it.isSelected }
val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch }
dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked ->
checkedIndices[i] = checked
}.setPositiveButton(R.string.save) { _, _ ->
val selectedBranches = branches.mapIndexedNotNullTo(HashSet()) { i, b ->
if (checkedIndices[i]) b.name else null
}
val chaptersIds = manga.chapters?.mapNotNullToSet { c ->
if (c.branch in selectedBranches) c.id else null
}
viewModel.download(chaptersIds)
}
} else {
dialogBuilder.setMessage(
activity.getString(
R.string.large_manga_save_confirm,
activity.resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount),
),
).setPositiveButton(R.string.save) { _, _ ->
viewModel.download(null)
override fun onItemClick(item: DownloadOption, view: View) {
val chaptersIds: Set<Long>? = when (item) {
is DownloadOption.WholeManga -> null
is DownloadOption.SelectionHint -> {
viewModel.startChaptersSelection()
return
}
else -> item.chaptersIds
}
dialogBuilder.show()
viewModel.download(chaptersIds)
}
}

View File

@@ -31,12 +31,14 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.core.util.ext.sanitize
@@ -72,6 +74,7 @@ class DetailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
private val doubleMangaLoadUseCase: DoubleMangaLoadUseCase,
networkState: NetworkState,
) : BaseViewModel() {
private val intent = MangaIntent(savedStateHandle)
@@ -81,6 +84,7 @@ class DetailsViewModel @Inject constructor(
val onShowToast = MutableEventFlow<Int>()
val onShowTip = MutableEventFlow<Unit>()
val onSelectChapter = MutableEventFlow<Long>()
val onDownloadStarted = MutableEventFlow<Unit>()
val manga = doubleManga.map { it?.any }
@@ -176,8 +180,9 @@ class DetailsViewModel @Inject constructor(
selectedBranch,
newChaptersCount,
bookmarks,
) { manga, history, branch, news, bookmarks ->
mapChapters(manga?.remote, manga?.local, history, news, branch, bookmarks)
networkState,
) { manga, history, branch, news, bookmarks, isOnline ->
mapChapters(manga?.remote?.takeIf { isOnline }, manga?.local, history, news, branch, bookmarks)
},
isChaptersReversed,
chaptersQuery,
@@ -286,6 +291,14 @@ class DetailsViewModel @Inject constructor(
}
}
fun startChaptersSelection() {
val chapters = chapters.value
val chapter = chapters.find {
it.isUnread && !it.isDownloaded
} ?: chapters.firstOrNull() ?: return
onSelectChapter.call(chapter.chapter.id)
}
fun onButtonTipClosed() {
settings.closeTip(DetailsActivity.TIP_BUTTON)
}

View File

@@ -0,0 +1,64 @@
package org.koitharu.kotatsu.details.ui
import android.content.DialogInterface
import android.view.View
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
import org.koitharu.kotatsu.download.ui.dialog.downloadOptionAD
class DownloadDialogHelper(
private val host: View,
private val viewModel: DetailsViewModel,
) {
fun show(callback: OnListItemClickListener<DownloadOption>) {
val branch = viewModel.selectedBranchValue
val allChapters = viewModel.manga.value?.chapters ?: return
val branchChapters = viewModel.manga.value?.getChapters(branch).orEmpty()
val history = viewModel.history.value
val options = buildList {
add(DownloadOption.WholeManga(allChapters.ids()))
if (branch != null && branchChapters.isNotEmpty()) {
add(DownloadOption.AllChapters(branch, branchChapters.ids()))
}
if (history != null) {
val unreadChapters = branchChapters.takeLastWhile { it.id != history.chapterId }
if (unreadChapters.isNotEmpty() && unreadChapters.size < branchChapters.size) {
add(DownloadOption.AllUnreadChapters(unreadChapters.ids(), branch))
if (unreadChapters.size > 5) {
add(DownloadOption.NextUnreadChapters(unreadChapters.take(5).ids()))
if (unreadChapters.size > 10) {
add(DownloadOption.NextUnreadChapters(unreadChapters.take(10).ids()))
}
}
}
} else {
if (branchChapters.size > 5) {
add(DownloadOption.FirstChapters(branchChapters.take(5).ids()))
if (branchChapters.size > 10) {
add(DownloadOption.FirstChapters(branchChapters.take(10).ids()))
}
}
}
add(DownloadOption.SelectionHint())
}
var dialog: DialogInterface? = null
val listener = OnListItemClickListener<DownloadOption> { item, _ ->
callback.onItemClick(item, host)
dialog?.dismiss()
}
dialog = RecyclerViewAlertDialog.Builder<DownloadOption>(host.context)
.addAdapterDelegate(downloadOptionAD(listener))
.setCancelable(true)
.setTitle(R.string.download)
.setNegativeButton(android.R.string.cancel)
.setItems(options)
.create()
.also { it.show() }
}
}

View File

@@ -1,16 +0,0 @@
package org.koitharu.kotatsu.details.ui
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
class MangaDetailsAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount() = 2
override fun createFragment(position: Int): Fragment = when (position) {
0 -> DetailsFragment()
1 -> ChaptersFragment()
else -> throw IndexOutOfBoundsException("No fragment for position $position")
}
}

View File

@@ -0,0 +1,99 @@
package org.koitharu.kotatsu.download.ui.dialog
import android.content.res.Resources
import androidx.annotation.DrawableRes
import org.koitharu.kotatsu.R
import java.util.Locale
import com.google.android.material.R as materialR
sealed interface DownloadOption {
val chaptersIds: Set<Long>
@get:DrawableRes
val iconResId: Int
val chaptersCount: Int
get() = chaptersIds.size
fun getLabel(resources: Resources): CharSequence
class AllChapters(
val branch: String,
override val chaptersIds: Set<Long>,
) : DownloadOption {
override val iconResId = R.drawable.ic_select_group
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(R.string.download_option_all_chapters, branch)
}
}
class WholeManga(
override val chaptersIds: Set<Long>,
) : DownloadOption {
override val iconResId = materialR.drawable.abc_ic_menu_selectall_mtrl_alpha
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(R.string.download_option_whole_manga)
}
}
class FirstChapters(
override val chaptersIds: Set<Long>,
) : DownloadOption {
override val iconResId = R.drawable.ic_list_start
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(
R.string.download_option_first_n_chapters,
resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount)
.lowercase(Locale.getDefault()),
)
}
}
class AllUnreadChapters(
override val chaptersIds: Set<Long>,
val branch: String?,
) : DownloadOption {
override val iconResId = R.drawable.ic_list_end
override fun getLabel(resources: Resources): CharSequence {
return if (branch == null) {
resources.getString(R.string.download_option_all_unread)
} else {
resources.getString(R.string.download_option_all_unread_b, branch)
}
}
}
class NextUnreadChapters(
override val chaptersIds: Set<Long>,
) : DownloadOption {
override val iconResId = R.drawable.ic_list_next
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(
R.string.download_option_next_unread_n_chapters,
resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount)
.lowercase(Locale.getDefault()),
)
}
}
class SelectionHint : DownloadOption {
override val chaptersIds: Set<Long> = emptySet()
override val iconResId = R.drawable.ic_tap
override fun getLabel(resources: Resources): CharSequence {
return resources.getString(R.string.download_option_manual_selection)
}
}
}

View File

@@ -0,0 +1,27 @@
package org.koitharu.kotatsu.download.ui.dialog
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemDownloadOptionBinding
fun downloadOptionAD(
onClickListener: OnListItemClickListener<DownloadOption>,
) = adapterDelegateViewBinding<DownloadOption, DownloadOption, ItemDownloadOptionBinding>(
{ layoutInflater, parent -> ItemDownloadOptionBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v -> onClickListener.onItemClick(item, v) }
bind {
with(binding.root) {
title = item.getLabel(resources)
subtitle = if (item.chaptersCount == 0) null else resources.getQuantityString(
R.plurals.chapters,
item.chaptersCount,
item.chaptersCount,
)
setIconResource(item.iconResId)
}
}
}

View File

@@ -141,11 +141,13 @@ class DownloadsViewModel @Inject constructor(
fun remove(ids: Set<Long>) {
launchJob(Dispatchers.Default) {
val snapshot = works.value ?: return@launchJob
val uuids = HashSet<UUID>(ids.size)
for (work in snapshot) {
if (work.id.mostSignificantBits in ids) {
workScheduler.delete(work.id)
uuids.add(work.id)
}
}
workScheduler.delete(uuids)
onActionDone.call(ReversibleAction(R.string.downloads_removed, null))
}
}

View File

@@ -45,6 +45,7 @@ import org.koitharu.kotatsu.core.util.WorkManagerHelper
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.core.util.progress.TimeLeftEstimator
import org.koitharu.kotatsu.download.domain.DownloadState
@@ -60,7 +61,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File
import java.util.UUID
import java.util.concurrent.TimeUnit
@@ -322,9 +322,9 @@ class DownloadWorker @AssistedInject constructor(
manga: Manga,
includedIds: LongArray?,
): List<MangaChapter> {
val chapters = checkNotNull(manga.chapters?.toMutableList()) {
val chapters = checkNotNull(manga.chapters) {
"Chapters list must not be null"
}
}.toMutableList()
if (includedIds != null) {
val chaptersIdsSet = includedIds.toMutableSet()
chapters.retainAll { x -> chaptersIdsSet.remove(x.id) }
@@ -399,6 +399,13 @@ class DownloadWorker @AssistedInject constructor(
WorkManagerHelper(workManager).deleteWork(id)
}
suspend fun delete(ids: Collection<UUID>) {
val wm = workManager
val helper = WorkManagerHelper(wm)
ids.forEach { id -> wm.cancelWorkById(id).await() }
helper.deleteWorks(ids)
}
suspend fun removeCompleted() {
val helper = WorkManagerHelper(workManager)
val finishedWorks = helper.getFinishedWorkInfosByTag(TAG)
@@ -406,10 +413,7 @@ class DownloadWorker @AssistedInject constructor(
}
suspend fun updateConstraints() {
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED)
.build()
val constraints = createConstraints()
val helper = WorkManagerHelper(workManager)
val works = helper.getWorkInfosByTag(TAG)
for (work in works) {
@@ -418,6 +422,7 @@ class DownloadWorker @AssistedInject constructor(
}
val request = OneTimeWorkRequestBuilder<DownloadWorker>()
.setConstraints(constraints)
.addTag(TAG)
.setId(work.id)
.build()
helper.updateWork(request)
@@ -425,15 +430,15 @@ class DownloadWorker @AssistedInject constructor(
}
private suspend fun scheduleImpl(data: Collection<Data>) {
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
.setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED)
.build()
if (data.isEmpty()) {
return
}
val constraints = createConstraints()
val requests = data.map { inputData ->
OneTimeWorkRequestBuilder<DownloadWorker>()
.setConstraints(constraints)
.addTag(TAG)
.keepResultsForAtLeast(7, TimeUnit.DAYS)
.keepResultsForAtLeast(30, TimeUnit.DAYS)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.setInputData(inputData)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
@@ -441,6 +446,10 @@ class DownloadWorker @AssistedInject constructor(
}
workManager.enqueue(requests).await()
}
private fun createConstraints() = Constraints.Builder()
.setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED)
.build()
}
private companion object {

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.history.domain
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
@@ -30,7 +31,7 @@ class HistoryUpdateUseCase @Inject constructor(
manga: Manga,
readerState: ReaderState,
percent: Float
) = processLifecycleScope.launch(Dispatchers.Default) {
) = processLifecycleScope.launch(Dispatchers.Default, CoroutineStart.ATOMIC) {
runCatchingCancellable {
withContext(NonCancellable) {
invoke(manga, readerState, percent)

View File

@@ -23,7 +23,7 @@ class ImageFileFilter : FilenameFilter, FileFilter {
return isExtensionValid(ext)
}
private fun isExtensionValid(ext: String): Boolean {
fun isExtensionValid(ext: String): Boolean {
return ext == "png" || ext == "jpg" || ext == "jpeg" || ext == "webp"
}
}

View File

@@ -29,6 +29,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File
import java.io.FilenameFilter
import java.util.EnumSet
import javax.inject.Inject
import javax.inject.Singleton
@@ -192,7 +193,7 @@ class LocalMangaRepository @Inject constructor(
val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM)
files.map { file ->
async(dispatcher) {
runCatchingCancellable { LocalMangaInput.of(file).getManga() }.getOrNull()
runCatchingCancellable { LocalMangaInput.ofOrNull(file)?.getManga() }.getOrNull()
}
}.awaitAll()
}.filterNotNullTo(ArrayList(files.size))

View File

@@ -2,8 +2,11 @@ package org.koitharu.kotatsu.local.data
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.StatFs
import androidx.annotation.WorkerThread
import androidx.core.net.toFile
import dagger.Reusable
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -13,11 +16,14 @@ import okhttp3.Cache
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.getStorageName
import org.koitharu.kotatsu.core.util.ext.resolveFile
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.io.File
import javax.inject.Inject
private const val DIR_NAME = "manga"
private const val NOMEDIA = ".nomedia"
private const val CACHE_DISK_PERCENTAGE = 0.02
private const val CACHE_SIZE_MIN: Long = 10 * 1024 * 1024 // 10MB
private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB
@@ -74,14 +80,42 @@ class LocalStorageManager @Inject constructor(
preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() }
}
fun getStorageDisplayName(file: File) = file.getStorageName(context)
suspend fun getApplicationStorageDirs(): Set<File> = runInterruptible(Dispatchers.IO) {
getAvailableStorageDirs()
}
suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
if (uri.scheme == "file") {
uri.toFile()
} else {
uri.resolveFile(context)
}
}
suspend fun setDirIsNoMedia(dir: File) = runInterruptible(Dispatchers.IO) {
File(dir, NOMEDIA).createNewFile()
}
fun takePermissions(uri: Uri) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, flags)
}
suspend fun getDirectoryDisplayName(dir: File, isFullPath: Boolean): String = runInterruptible(Dispatchers.IO) {
val packageName = context.packageName
if (dir.absolutePath.contains(packageName)) {
dir.getStorageName(context)
} else if (isFullPath) {
dir.path
} else {
dir.name
}
}
@WorkerThread
private fun getConfiguredStorageDirs(): MutableSet<File> {
val set = getAvailableStorageDirs()
settings.mangaStorageDir?.let {
set.add(it)
}
set.addAll(settings.userSpecifiedMangaDirectories)
return set
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.local.data.input
import android.net.Uri
import androidx.core.net.toFile
import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -30,6 +31,12 @@ sealed class LocalMangaInput(
else -> LocalMangaZipInput(file)
}
fun ofOrNull(file: File): LocalMangaInput? = when {
file.isDirectory -> LocalMangaDirInput(file)
CbzFilter.isFileSupported(file.name) -> LocalMangaZipInput(file)
else -> null
}
@JvmStatic
protected fun zipUri(file: File, entryName: String): String =
Uri.fromParts("cbz", file.path, entryName).toString()

View File

@@ -35,18 +35,6 @@ class LocalMangaUtil(
}
}
suspend fun writeIndex(index: MangaIndex) {
newOutput().use { output ->
when (output) {
is LocalMangaDirOutput -> {
TODO()
}
is LocalMangaZipOutput -> TODO()
}
}
}
private suspend fun newOutput(): LocalMangaOutput = runInterruptible(Dispatchers.IO) {
val file = manga.url.toUri().toFile()
if (file.isDirectory) {

View File

@@ -85,7 +85,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
.setOngoing(true)
.setOngoing(false)
.build()
startForeground(NOTIFICATION_ID, notification)
}

View File

@@ -33,7 +33,7 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick))
addMenuProvider(LocalListMenuProvider(binding.root.context, this::onEmptyActionClick))
viewModel.onMangaRemoved.observeEvent(viewLifecycleOwner) { onItemRemoved() }
}
@@ -45,7 +45,7 @@ class LocalListFragment : MangaListFragment(), FilterOwner {
FilterSheetFragment.show(childFragmentManager)
}
override fun onScrolledToEnd() = Unit
override fun onScrolledToEnd() = viewModel.loadNextPage()
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_local, menu)

View File

@@ -1,12 +1,15 @@
package org.koitharu.kotatsu.local.ui
import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
class LocalListMenuProvider(
private val context: Context,
private val onImportClick: Function0<Unit>,
) : MenuProvider {
@@ -20,6 +23,12 @@ class LocalListMenuProvider(
onImportClick()
true
}
R.id.action_settings -> {
context.startActivity(MangaDirectoriesActivity.newIntent(context))
true
}
else -> false
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.local.ui
import android.content.SharedPreferences
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -24,7 +25,7 @@ class LocalListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
filter: FilterCoordinator,
settings: AppSettings,
private val settings: AppSettings,
downloadScheduler: DownloadWorker.Scheduler,
listExtraProvider: ListExtraProvider,
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
@@ -36,7 +37,7 @@ class LocalListViewModel @Inject constructor(
settings,
listExtraProvider,
downloadScheduler,
) {
), SharedPreferences.OnSharedPreferenceChangeListener {
val onMangaRemoved = MutableEventFlow<Unit>()
@@ -47,6 +48,18 @@ class LocalListViewModel @Inject constructor(
loadList(filter.snapshot(), append = false).join()
}
}
settings.subscribe(this)
}
override fun onCleared() {
settings.unsubscribe(this)
super.onCleared()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == AppSettings.KEY_LOCAL_MANGA_DIRS) {
onRefresh()
}
}
fun delete(ids: Set<Long>) {

View File

@@ -13,7 +13,7 @@ enum class ScoreFormat {
POINT_5 -> score / 5f
POINT_3 -> score / 3f
}
}.coerceIn(0f, 1f)
companion object {

View File

@@ -173,7 +173,7 @@ class MALRepository @Inject constructor(
status = json.getString("status"),
chapter = json.getInt("num_chapters_read"),
comment = json.getString("comments"),
rating = json.getDouble("score").toFloat() / 10f,
rating = (json.getDouble("score").toFloat() / 10f).coerceIn(0f, 1f),
)
db.scrobblingDao.upsert(entity)
}

View File

@@ -190,7 +190,7 @@ class ShikimoriRepository @Inject constructor(
status = json.getString("status"),
chapter = json.getInt("chapters"),
comment = json.getString("text"),
rating = json.getDouble("score").toFloat() / 10f,
rating = (json.getDouble("score").toFloat() / 10f).coerceIn(0f, 1f),
)
db.scrobblingDao.upsert(entity)
}

View File

@@ -11,20 +11,18 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.dialog.StorageSelectDialog
import org.koitharu.kotatsu.core.util.ext.getStorageName
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File
import org.koitharu.kotatsu.settings.storage.MangaDirectorySelectDialog
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
import javax.inject.Inject
@AndroidEntryPoint
class DownloadsSettingsFragment :
BasePreferenceFragment(R.string.downloads),
SharedPreferences.OnSharedPreferenceChangeListener,
StorageSelectDialog.OnStorageSelectListener {
SharedPreferences.OnSharedPreferenceChangeListener {
@Inject
lateinit var storageManager: LocalStorageManager
@@ -39,6 +37,7 @@ class DownloadsSettingsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
findPreference<Preference>(AppSettings.KEY_LOCAL_MANGA_DIRS)?.bindDirectoriesCount()
settings.subscribe(this)
}
@@ -53,6 +52,10 @@ class DownloadsSettingsFragment :
findPreference<Preference>(key)?.bindStorageName()
}
AppSettings.KEY_LOCAL_MANGA_DIRS -> {
findPreference<Preference>(key)?.bindDirectoriesCount()
}
AppSettings.KEY_DOWNLOADS_WIFI -> {
updateDownloadsConstraints()
}
@@ -62,12 +65,12 @@ class DownloadsSettingsFragment :
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_LOCAL_STORAGE -> {
val ctx = context ?: return false
StorageSelectDialog.Builder(ctx, storageManager, this)
.setTitle(preference.title ?: "")
.setNegativeButton(android.R.string.cancel)
.create()
.show()
MangaDirectorySelectDialog.show(childFragmentManager)
true
}
AppSettings.KEY_LOCAL_MANGA_DIRS -> {
startActivity(MangaDirectoriesActivity.newIntent(preference.context))
true
}
@@ -75,14 +78,21 @@ class DownloadsSettingsFragment :
}
}
override fun onStorageSelected(file: File) {
settings.mangaStorageDir = file
}
private fun Preference.bindStorageName() {
viewLifecycleScope.launch {
val storage = storageManager.getDefaultWriteableDir()
summary = storage?.getStorageName(context) ?: getString(R.string.not_available)
summary = if (storage != null) {
storageManager.getDirectoryDisplayName(storage, isFullPath = true)
} else {
getString(R.string.not_available)
}
}
}
private fun Preference.bindDirectoriesCount() {
viewLifecycleScope.launch {
val dirs = storageManager.getReadableDirs().size
summary = resources.getQuantityString(R.plurals.items, dirs, dirs)
}
}

View File

@@ -4,6 +4,9 @@ import androidx.annotation.WorkerThread
import androidx.core.os.LocaleListCompat
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.model.getLocaleTitle
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -19,18 +22,26 @@ class NewSourcesViewModel @Inject constructor(
private val initialList = settings.newSources
val sources = MutableStateFlow<List<SourceConfigItem>?>(null)
private var listUpdateJob: Job? = null
init {
launchJob(Dispatchers.Default) {
listUpdateJob = launchJob(Dispatchers.Default) {
sources.value = buildList()
}
}
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
if (isEnabled) {
settings.hiddenSources -= item.source.name
} else {
settings.hiddenSources += item.source.name
val prevJob = listUpdateJob
listUpdateJob = launchJob(Dispatchers.Default) {
if (isEnabled) {
settings.hiddenSources -= item.source.name
} else {
settings.hiddenSources += item.source.name
}
prevJob?.cancelAndJoin()
val list = buildList()
ensureActive()
sources.value = list
}
}
@@ -61,3 +72,4 @@ class NewSourcesViewModel @Inject constructor(
}
}
}

View File

@@ -4,16 +4,15 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
@@ -49,10 +48,18 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
getString(R.string.logged_in_as, it)
}
}
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
viewModel.onError.observeEvent(
viewLifecycleOwner,
SnackbarErrorObserver(
listView,
this,
exceptionResolver,
) { viewModel.onResume() },
)
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
findPreference<Preference>(KEY_AUTH)?.isEnabled = !isLoading
}
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView))
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
@@ -61,32 +68,15 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
startActivity(SourceAuthActivity.newIntent(preference.context, viewModel.source))
true
}
AppSettings.KEY_COOKIES_CLEAR -> {
viewModel.clearCookies()
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
private fun onError(error: Throwable) {
val snackbar = Snackbar.make(
listView ?: return,
error.getDisplayMessage(resources),
Snackbar.LENGTH_INDEFINITE,
)
if (ExceptionResolver.canResolve(error)) {
snackbar.setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
}
snackbar.show()
}
private fun resolveError(error: Throwable) {
view ?: return
viewLifecycleScope.launch {
if (exceptionResolver.resolve(error)) {
viewModel.onResume()
}
}
}
companion object {
private const val KEY_AUTH = "auth"

View File

@@ -5,9 +5,15 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import okhttp3.HttpUrl
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -17,11 +23,13 @@ import javax.inject.Inject
class SourceSettingsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
private val cookieJar: MutableCookieJar,
) : BaseViewModel() {
val source = savedStateHandle.require<MangaSource>(SourceSettingsFragment.EXTRA_SOURCE)
val repository = mangaRepositoryFactory.create(source) as RemoteMangaRepository
val onActionDone = MutableEventFlow<ReversibleAction>()
val username = MutableStateFlow<String?>(null)
private var usernameLoadJob: Job? = null
@@ -35,6 +43,18 @@ class SourceSettingsViewModel @Inject constructor(
}
}
fun clearCookies() {
launchLoadingJob(Dispatchers.Default) {
val url = HttpUrl.Builder()
.scheme("https")
.host(repository.domain)
.build()
cookieJar.removeCookies(url)
onActionDone.call(ReversibleAction(R.string.cookies_cleared, null))
loadUsername()
}
}
private fun loadUsername() {
launchLoadingJob(Dispatchers.Default) {
try {

View File

@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.webkit.CookieManager
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets
@@ -27,6 +28,7 @@ import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -66,8 +68,11 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
}
with(viewBinding.webView.settings) {
javaScriptEnabled = true
userAgentString = CommonHeadersInterceptor.userAgentChrome
domStorageEnabled = true
databaseEnabled = true
userAgentString = UserAgents.CHROME_MOBILE
}
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)

View File

@@ -0,0 +1,21 @@
package org.koitharu.kotatsu.settings.storage
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemStorageBinding
fun directoryAD(
clickListener: OnListItemClickListener<DirectoryModel>,
) = adapterDelegateViewBinding<DirectoryModel, DirectoryModel, ItemStorageBinding>(
{ layoutInflater, parent -> ItemStorageBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v -> clickListener.onItemClick(item, v) }
bind {
binding.textViewTitle.text = item.title ?: getString(item.titleRes)
binding.textViewSubtitle.textAndVisible = item.file?.absolutePath
binding.imageViewIndicator.isChecked = item.isChecked
}
}

View File

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

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.settings.storage
import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.io.File
class DirectoryModel(
val title: String?,
@StringRes val titleRes: Int,
val file: File?,
val isChecked: Boolean,
val isAvailable: Boolean,
) : ListModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DirectoryModel
if (title != other.title) return false
if (titleRes != other.titleRes) return false
if (file != other.file) return false
if (isChecked != other.isChecked) return false
return isAvailable == other.isAvailable
}
override fun hashCode(): Int {
var result = title?.hashCode() ?: 0
result = 31 * result + titleRes
result = 31 * result + (file?.hashCode() ?: 0)
result = 31 * result + isChecked.hashCode()
result = 31 * result + isAvailable.hashCode()
return result
}
}

View File

@@ -0,0 +1,81 @@
package org.koitharu.kotatsu.settings.storage
import android.Manifest
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ToastErrorObserver
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.databinding.DialogDirectorySelectBinding
@AndroidEntryPoint
class MangaDirectorySelectDialog : AlertDialogFragment<DialogDirectorySelectBinding>(),
OnListItemClickListener<DirectoryModel> {
private val viewModel: MangaDirectorySelectViewModel by viewModels()
private val pickFileTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
if (it != null) viewModel.onCustomDirectoryPicked(it)
}
private val permissionRequestLauncher = registerForActivityResult(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
RequestStorageManagerPermissionContract()
} else {
ActivityResultContracts.RequestPermission()
},
) {
if (it) {
viewModel.refresh()
pickFileTreeLauncher.launch(null)
}
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): DialogDirectorySelectBinding {
return DialogDirectorySelectBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: DialogDirectorySelectBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = AsyncListDifferDelegationAdapter(DirectoryDiffCallback(), directoryAD(this))
binding.root.adapter = adapter
viewModel.items.observe(viewLifecycleOwner) { adapter.items = it }
viewModel.onDismissDialog.observeEvent(viewLifecycleOwner) { dismiss() }
viewModel.onPickDirectory.observeEvent(viewLifecycleOwner) { pickCustomDirectory() }
viewModel.onError.observeEvent(viewLifecycleOwner, ToastErrorObserver(binding.root, this))
}
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
return super.onBuildDialog(builder)
.setCancelable(true)
.setTitle(R.string.manga_save_location)
.setNegativeButton(android.R.string.cancel, null)
}
override fun onItemClick(item: DirectoryModel, view: View) {
viewModel.onItemClick(item)
}
private fun pickCustomDirectory() {
permissionRequestLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
companion object {
private const val TAG = "MangaDirectorySelectDialog"
fun show(fm: FragmentManager) = MangaDirectorySelectDialog()
.showDistinct(fm, TAG)
}
}

View File

@@ -0,0 +1,80 @@
package org.koitharu.kotatsu.settings.storage
import android.net.Uri
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import okio.FileNotFoundException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.local.data.LocalStorageManager
import javax.inject.Inject
@HiltViewModel
class MangaDirectorySelectViewModel @Inject constructor(
private val storageManager: LocalStorageManager,
private val settings: AppSettings,
) : BaseViewModel() {
val items = MutableStateFlow(emptyList<DirectoryModel>())
val onDismissDialog = MutableEventFlow<Unit>()
val onPickDirectory = MutableEventFlow<Unit>()
init {
refresh()
}
fun onItemClick(item: DirectoryModel) {
if (item.file != null) {
settings.mangaStorageDir = item.file
onDismissDialog.call(Unit)
} else {
onPickDirectory.call(Unit)
}
}
fun onCustomDirectoryPicked(uri: Uri) {
launchJob(Dispatchers.Default) {
storageManager.takePermissions(uri)
val dir = requireNotNull(storageManager.resolveUri(uri)) {
"Cannot resolve file name of \"$uri\""
}
if (!dir.canWrite()) {
throw AccessDeniedException(dir)
}
if (dir !in storageManager.getApplicationStorageDirs()) {
settings.mangaStorageDir = dir
storageManager.setDirIsNoMedia(dir)
}
onDismissDialog.call(Unit)
}
}
fun refresh() {
launchJob(Dispatchers.Default) {
val defaultValue = storageManager.getDefaultWriteableDir()
val available = storageManager.getWriteableDirs()
items.value = buildList(available.size + 1) {
available.mapTo(this) { dir ->
DirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
titleRes = 0,
file = dir,
isChecked = dir == defaultValue,
isAvailable = true,
)
}
this += DirectoryModel(
title = null,
titleRes = R.string.pick_custom_directory,
file = null,
isChecked = false,
isAvailable = true,
)
}
}
}
}

View File

@@ -0,0 +1,34 @@
package org.koitharu.kotatsu.settings.storage
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
@RequiresApi(Build.VERSION_CODES.R)
class RequestStorageManagerPermissionContract : ActivityResultContract<String, Boolean>() {
override fun createIntent(context: Context, input: String): Intent {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.addCategory("android.intent.category.DEFAULT")
intent.data = "package:${context.packageName}".toUri()
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
return Environment.isExternalStorageManager()
}
override fun getSynchronousResult(context: Context, input: String): SynchronousResult<Boolean>? {
return if (Environment.isExternalStorageManager()) {
SynchronousResult(true)
} else {
null
}
}
}

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.settings.storage.directories
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemStorageConfigBinding
import org.koitharu.kotatsu.settings.storage.DirectoryModel
fun directoryConfigAD(
clickListener: OnListItemClickListener<DirectoryModel>,
) = adapterDelegateViewBinding<DirectoryModel, DirectoryModel, ItemStorageConfigBinding>(
{ layoutInflater, parent -> ItemStorageConfigBinding.inflate(layoutInflater, parent, false) },
) {
binding.imageViewRemove.setOnClickListener { v -> clickListener.onItemClick(item, v) }
bind {
binding.textViewTitle.text = item.title ?: getString(item.titleRes)
binding.textViewSubtitle.textAndVisible = item.file?.absolutePath
binding.imageViewRemove.isVisible = item.isChecked
binding.textViewTitle.drawableStart = if (item.isAvailable) {
null
} else {
ContextCompat.getDrawable(context, R.drawable.ic_alert_outline)
}
}
}

View File

@@ -0,0 +1,93 @@
package org.koitharu.kotatsu.settings.storage.directories
import android.Manifest
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
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
@AndroidEntryPoint
class MangaDirectoriesActivity : BaseActivity<ActivityMangaDirectoriesBinding>(),
OnListItemClickListener<DirectoryModel>, View.OnClickListener {
private val viewModel: MangaDirectoriesViewModel by viewModels()
private val pickFileTreeLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) {
if (it != null) viewModel.onCustomDirectoryPicked(it)
}
private val permissionRequestLauncher = registerForActivityResult(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
RequestStorageManagerPermissionContract()
} else {
ActivityResultContracts.RequestPermission()
},
) {
if (it) {
viewModel.updateList()
pickFileTreeLauncher.launch(null)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityMangaDirectoriesBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val adapter = AsyncListDifferDelegationAdapter(DirectoryDiffCallback(), directoryConfigAD(this))
viewBinding.recyclerView.adapter = adapter
viewBinding.fabAdd.setOnClickListener(this)
viewModel.items.observe(this) { adapter.items = it }
viewModel.isLoading.observe(this) { viewBinding.progressBar.isVisible = it }
viewModel.onError.observeEvent(
this,
SnackbarErrorObserver(viewBinding.root, null, exceptionResolver) {
if (it) viewModel.updateList()
},
)
}
override fun onItemClick(item: DirectoryModel, view: View) {
viewModel.onRemoveClick(item.file ?: return)
}
override fun onClick(v: View?) {
permissionRequestLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.fabAdd.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = topMargin + insets.right
leftMargin = topMargin + insets.left
bottomMargin = topMargin + insets.bottom
}
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
)
viewBinding.recyclerView.updatePadding(
bottom = insets.bottom,
)
}
companion object {
fun newIntent(context: Context) = Intent(context, MangaDirectoriesActivity::class.java)
}
}

View File

@@ -0,0 +1,87 @@
package org.koitharu.kotatsu.settings.storage.directories
import android.net.Uri
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import okio.FileNotFoundException
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.settings.storage.DirectoryModel
import java.io.File
import javax.inject.Inject
@HiltViewModel
class MangaDirectoriesViewModel @Inject constructor(
private val storageManager: LocalStorageManager,
private val settings: AppSettings,
) : BaseViewModel() {
val items = MutableStateFlow(emptyList<DirectoryModel>())
private var loadingJob: Job? = null
init {
loadList()
}
fun updateList() {
loadList()
}
fun onCustomDirectoryPicked(uri: Uri) {
launchLoadingJob(Dispatchers.Default) {
loadingJob?.cancelAndJoin()
storageManager.takePermissions(uri)
val dir = requireNotNull(storageManager.resolveUri(uri)) {
"Cannot resolve file name of \"$uri\""
}
if (!dir.canWrite()) {
throw AccessDeniedException(dir)
}
if (dir !in storageManager.getApplicationStorageDirs()) {
settings.userSpecifiedMangaDirectories += dir
loadList()
}
}
}
fun onRemoveClick(directory: File) {
settings.userSpecifiedMangaDirectories -= directory
if (settings.mangaStorageDir == directory) {
settings.mangaStorageDir = null
}
loadList()
}
private fun loadList() {
val prevJob = loadingJob
loadingJob = launchJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
val applicationDirs = storageManager.getApplicationStorageDirs()
val customDirs = settings.userSpecifiedMangaDirectories
items.value = buildList(applicationDirs.size + customDirs.size) {
applicationDirs.mapTo(this) { dir ->
DirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
titleRes = 0,
file = dir,
isChecked = false,
isAvailable = dir.canRead() && dir.canWrite(),
)
}
customDirs.mapTo(this) { dir ->
DirectoryModel(
title = storageManager.getDirectoryDisplayName(dir, isFullPath = false),
titleRes = 0,
file = dir,
isChecked = true,
isAvailable = dir.canRead() && dir.canWrite(),
)
}
}
}
}
}

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -11,6 +12,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.favourites.data.toMangaList
@@ -32,6 +34,7 @@ class ShelfContentObserveUseCase @Inject constructor(
private val trackingRepository: TrackingRepository,
private val suggestionRepository: SuggestionRepository,
private val db: MangaDatabase,
private val settings: AppSettings,
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
) {
@@ -46,7 +49,10 @@ class ShelfContentObserveUseCase @Inject constructor(
}
private fun observeLocalManga(sortOrder: SortOrder, limit: Int): Flow<List<Manga>> {
return localStorageChanges
return combine<LocalManga?, String, Any?>(
localStorageChanges,
settings.observe().filter { it == AppSettings.KEY_LOCAL_MANGA_DIRS }.onStart { emit("") }
) { a, b -> a to b }
.onStart { emit(null) }
.mapLatest {
localMangaRepository.getList(0, null, sortOrder).take(limit)

View File

@@ -109,7 +109,7 @@ class SuggestionsWorker @AssistedInject constructor(
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setDefaults(0)
.setOngoing(true)
.setOngoing(false)
.setSilent(true)
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)

View File

@@ -13,9 +13,9 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE
)
]
onDelete = ForeignKey.CASCADE,
),
],
)
class TrackEntity(
@PrimaryKey(autoGenerate = false)
@@ -27,4 +27,4 @@ class TrackEntity(
@ColumnInfo(name = "last_check") val lastCheck: Long,
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR)
@ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long
)
)

View File

@@ -43,6 +43,9 @@ abstract class TracksDao {
@Query("UPDATE tracks SET chapters_new = 0")
abstract suspend fun clearCounters()
@Query("UPDATE tracks SET chapters_new = 0 WHERE manga_id = :mangaId")
abstract suspend fun clearCounter(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long)

View File

@@ -126,6 +126,18 @@ class TrackingRepository @Inject constructor(
}
}
suspend fun clearUpdates(ids: Collection<Long>) {
when {
ids.isEmpty() -> return
ids.size == 1 -> db.tracksDao.clearCounter(ids.single())
else -> db.withTransaction {
for (id in ids) {
db.tracksDao.clearCounter(id)
}
}
}
}
suspend fun syncWithHistory(manga: Manga, chapterId: Long) {
val chapters = manga.chapters ?: return
val chapterIndex = chapters.indexOfFirst { x -> x.id == chapterId }

View File

@@ -1,7 +1,12 @@
package org.koitharu.kotatsu.tracker.ui.updates
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.view.ActionMode
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.list.ui.MangaListFragment
@AndroidEntryPoint
@@ -12,6 +17,22 @@ class UpdatesFragment : MangaListFragment() {
override fun onScrolledToEnd() = Unit
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_updates, menu)
return super.onCreateActionMode(controller, mode, menu)
}
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_remove -> {
viewModel.remove(controller.snapshot())
true
}
else -> super.onActionItemClicked(controller, mode, item)
}
}
companion object {
fun newInstance() = UpdatesFragment()

View File

@@ -59,4 +59,10 @@ class UpdatesViewModel @Inject constructor(
override fun onRefresh() = Unit
override fun onRetry() = Unit
fun remove(ids: Set<Long>) {
launchJob(Dispatchers.Default) {
repository.clearUpdates(ids)
}
}
}

View File

@@ -221,7 +221,7 @@ class TrackWorker @AssistedInject constructor(
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setDefaults(0)
.setOngoing(true)
.setOngoing(false)
.setSilent(true)
.setProgress(0, 0, true)
.setSmallIcon(android.R.drawable.stat_notify_sync)

View File

@@ -37,7 +37,7 @@ class RecentListFactory(
override fun getLoadingView() = null
override fun getItemId(position: Int) = dataSet[position].id
override fun getItemId(position: Int) = dataSet.getOrNull(position)?.id ?: 0L
override fun onDataSetChanged() {
val data = runBlocking { historyRepository.getList(0, 10) }
@@ -48,7 +48,7 @@ class RecentListFactory(
override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.item_recent)
val item = dataSet[position]
val item = dataSet.getOrNull(position) ?: return views
runCatching {
coil.executeBlocking(
ImageRequest.Builder(context)

View File

@@ -40,7 +40,7 @@ class ShelfListFactory(
override fun getLoadingView() = null
override fun getItemId(position: Int) = dataSet[position].id
override fun getItemId(position: Int) = dataSet.getOrNull(position)?.id ?: 0L
override fun onDataSetChanged() {
val data = runBlocking {
@@ -58,7 +58,7 @@ class ShelfListFactory(
override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.item_shelf)
val item = dataSet[position]
val item = dataSet.getOrNull(position) ?: return views
views.setTextViewText(R.id.textView_title, item.title)
runCatching {
coil.executeBlocking(

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12C7,9.58 8.72,7.56 11,7.1V3H13V7.1C15.28,7.56 17,9.58 17,12M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12C17,14.42 15.28,16.44 13,16.9V21H11V16.9C8.72,16.44 7,14.42 7,12M12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15M13,3V5H11V3H13Z" />
</vector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,7A5,5 0 0,1 17,12C17,14.42 15.28,16.44 13,16.9V21H11V16.9C8.72,16.44 7,14.42 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M5 3A2 2 0 0 0 3 5H5M7 3V5H9V3M11 3V5H13V3M15 3V5H17V3M19 3V5H21A2 2 0 0 0 19 3M3 7V9H5V7M7 7V11H11V7M13 7V11H17V7M19 7V9H21V7M3 11V13H5V11M19 11V13H21V11M7 13V17H11V13M13 13V17H17V13M3 15V17H5V15M19 15V17H21V15M3 19A2 2 0 0 0 5 21V19M7 19V21H9V19M11 19V21H13V19M15 19V21H17V19M19 19V21A2 2 0 0 0 21 19Z" />
</vector>

View File

@@ -58,7 +58,6 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_new_category"
android:src="@drawable/ic_add"
android:text="@string/create_category"
app:fabSize="normal"
app:icon="@drawable/ic_add"

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="noScroll">
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/pick_custom_directory"
android:text="@string/add"
app:fabSize="normal"
app:icon="@drawable/ic_add"
app:layout_anchor="@id/recyclerView"
app:layout_anchorGravity="bottom|end"
app:layout_behavior="org.koitharu.kotatsu.core.ui.util.ShrinkOnScrollBehavior"
app:layout_dodgeInsetEdges="bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -93,8 +93,7 @@
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:trackCornerRadius="@dimen/mtrl_progress_indicator_full_rounded_corner_radius" />
android:indeterminate="true" />
<TextView
android:id="@+id/textView_loading"

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollIndicators="top|bottom"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:ignore="UnusedAttribute" />

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/button_file"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="?android:listPreferredItemPaddingStart"
android:minHeight="?android:listPreferredItemHeightSmall"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
tools:subtitle="@string/chapters"
tools:title="@string/download_option_whole_manga" />

View File

@@ -1,48 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:minHeight="?listPreferredItemHeightLarge"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeight"
android:orientation="horizontal"
android:paddingVertical="12dp"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="16dp"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingBottom="16dp">
android:paddingEnd="?listPreferredItemPaddingEnd">
<org.koitharu.kotatsu.core.ui.widgets.CheckableImageView
android:id="@+id/imageView_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="?android:listChoiceIndicatorSingle"
tools:ignore="TouchTargetSizeCheck" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:layout_toEndOf="@id/imageView_indicator"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[3]" />
android:orientation="vertical">
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/textView_title"
android:layout_alignParentEnd="true"
android:layout_marginStart="?listPreferredItemPaddingStart"
android:layout_marginTop="6dp"
android:layout_toEndOf="@id/imageView_indicator"
android:ellipsize="end"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[20]" />
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="@tools:sample/lorem[3]" />
</RelativeLayout>
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[20]" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?listPreferredItemHeight"
android:orientation="horizontal"
android:paddingVertical="12dp"
android:paddingStart="?listPreferredItemPaddingStart">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="6dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
app:drawableTint="@color/warning"
tools:drawableStart="@drawable/ic_alert_outline"
tools:text="@tools:sample/lorem[3]" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[20]" />
</LinearLayout>
<ImageView
android:id="@+id/imageView_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/remove"
android:padding="?listPreferredItemPaddingEnd"
app:srcCompat="@drawable/ic_delete" />
</LinearLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:icon="?actionModeShareDrawable"
android:title="@string/share"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_remove"
android:icon="@drawable/ic_delete"
android:title="@string/delete"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_save"
android:title="@string/save"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_select_all"
android:icon="?actionModeSelectAllDrawable"
android:title="@android:string/selectAll"
app:showAsAction="ifRoom|withText" />
</menu>

View File

@@ -20,7 +20,7 @@
<item
android:id="@+id/action_save"
android:orderInCategory="40"
android:title="@string/save"
android:title="@string/download"
android:visible="false"
app:showAsAction="never" />

View File

@@ -9,4 +9,10 @@
android:title="@string/_import"
app:showAsAction="never" />
</menu>
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/settings"
app:showAsAction="never" />
</menu>

View File

@@ -1,77 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="detailed_list">تفاصيل القائمة</string>
<string name="error_occurred">حدث خطأ</string>
<string name="details">تفاصيل</string>
<string name="grid">شبكة</string>
<string name="list_mode">وضع القائمة</string>
<string name="settings">إعدادات</string>
<string name="remote_sources">المصادر البعيدة</string>
<string name="chapters">فصول</string>
<string name="favourites">المفضلة</string>
<string name="network_error">‌خطاء في الشبكة</string>
<string name="loading_">جار التحميل…</string>
<string name="chapter_d_of_d">فصل %1$d في %2$d</string>
<string name="close">غلق</string>
<string name="try_again">حاول مجدداً</string>
<string name="computing_">جاري الحوسبة …</string>
<string name="local_storage">التخزين المحلي</string>
<string name="history">سجل</string>
<string name="list">قائمة</string>
<string name="clear_history">محو سجل</string>
<string name="add_to_favourites">ضع هذا في المفضلة</string>
<string name="add">أضف</string>
<string name="save">حفظ</string>
<string name="history_is_empty">لا سجل بعد</string>
<string name="downloads">التحميلات</string>
<string name="by_name">اسم</string>
<string name="newest">الأحدث</string>
<string name="by_rating">تقييم</string>
<string name="pages">صفحات</string>
<string name="read">اقرأ</string>
<string name="share">شارك</string>
<string name="nothing_found">لم يتم عثور على اي شيء</string>
<string name="you_have_not_favourites_yet">لا مفضلة بعد</string>
<string name="search">بحث</string>
<string name="search_manga">البحث في المانجا</string>
<string name="manga_downloading_">جاري التنزيل…</string>
<string name="create_shortcut">انشاء اختصار…</string>
<string name="theme">مظهر</string>
<string name="automatic">حسب النظام</string>
<string name="share_s">شارك %s</string>
<string name="processing_">في طور معالجة…</string>
<string name="updated">محدث</string>
<string name="filter">فلتر</string>
<string name="sort_order">ترتيب الفرز</string>
<string name="light">ضوء</string>
<string name="dark">داكن</string>
<string name="clear">أزل</string>
<string name="remove">ازالة</string>
<string name="popular">شائع</string>
<string name="add_new_category">قائمة جديدة</string>
<string name="download_complete">تم التنزيل</string>
<string name="text_clear_history_prompt">هل تريد محو سجل القراءة بالكامل بشكل دائم؟</string>
<string name="save_page">احفظ الصفحة</string>
<string name="page_saved">حفظت</string>
<string name="standard">اساسي</string>
<string name="no_description">لا يوجد وصف</string>
<string name="clear_pages_cache">مسح ذاكرة التخزين المؤقت للصفحة</string>
<string name="webtoon">ويبتون</string>
<string name="read_mode">وضع القراءة</string>
<string name="search_on_s">بحث على %s</string>
<string name="delete_manga">حذف المانغا</string>
<string name="text_delete_local_manga">حذف \"%s\" من الجهاز نهائيا؟</string>
<string name="reader_settings">إعدادات القراءة</string>
<string name="switch_pages">تغییر صفحات</string>
<string name="delete">حذف</string>
<string name="share_image">شارك الصورة</string>
<string name="text_file_not_supported">إما أن تختار ملف ZIP أو CBZ.</string>
<string name="_import">استورد</string>
<string name="operation_not_supported">هذا خيار غير مدعم</string>
<string name="grid_size">حجم الشبكة</string>
<string name="volume_buttons">أزرار الصوت</string>
<string name="taps_on_edges">النقر على حواف الشاشة</string>
<string name="_continue">يكمل</string>
<string name="error">خطاء</string>
<string name="clear_search_history">مسح تاريخ البحث</string>
</resources>
<string name="detailed_list">تفاصيل القائمة</string>
<string name="error_occurred">حدث خطأ</string>
<string name="details">تفاصيل</string>
<string name="grid">شبكة</string>
<string name="list_mode">وضع القائمة</string>
<string name="settings">إعدادات</string>
<string name="remote_sources">مصادر المانجا</string>
<string name="chapters">فصول</string>
<string name="favourites">المفضلة</string>
<string name="network_error">‌خطاء في الشبكة</string>
<string name="loading_">جار التحميل…</string>
<string name="chapter_d_of_d">فصل %1$d في %2$d</string>
<string name="close">غلق</string>
<string name="try_again">حاول مجدداً</string>
<string name="computing_">جاري الحوسبة …</string>
<string name="local_storage">التخزين المحلي</string>
<string name="history">سجل</string>
<string name="list">قائمة</string>
<string name="clear_history">محو سجل</string>
<string name="add_to_favourites">ضع هذا في المفضلة</string>
<string name="add">أضف</string>
<string name="save">حفظ</string>
<string name="history_is_empty">لا سجل بعد</string>
<string name="downloads">التحميلات</string>
<string name="by_name">اسم</string>
<string name="newest">الأحدث</string>
<string name="by_rating">تقييم</string>
<string name="pages">صفحات</string>
<string name="read">اقرأ</string>
<string name="share">شارك</string>
<string name="nothing_found">لم يتم عثور على اي شيء</string>
<string name="you_have_not_favourites_yet">لا مفضلة بعد</string>
<string name="search">بحث</string>
<string name="search_manga">البحث في المانجا</string>
<string name="manga_downloading_">جاري التنزيل…</string>
<string name="create_shortcut">انشاء اختصار…</string>
<string name="theme">مظهر</string>
<string name="automatic">حسب النظام</string>
<string name="share_s">شارك %s</string>
<string name="processing_">في طور معالجة…</string>
<string name="updated">محدث</string>
<string name="filter">فلتر</string>
<string name="sort_order">ترتيب الفرز</string>
<string name="light">ضوء</string>
<string name="dark">داكن</string>
<string name="clear">أزل</string>
<string name="remove">ازالة</string>
<string name="popular">شائع</string>
<string name="add_new_category">قائمة جديدة</string>
<string name="download_complete">تم التنزيل</string>
<string name="text_clear_history_prompt">هل تريد محو سجل القراءة بالكامل بشكل دائم؟</string>
<string name="save_page">احفظ الصفحة</string>
<string name="page_saved">حفظت</string>
<string name="standard">اساسي</string>
<string name="no_description">لا يوجد وصف</string>
<string name="clear_pages_cache">مسح ذاكرة التخزين المؤقت للصفحة</string>
<string name="webtoon">ويبتون</string>
<string name="read_mode">وضع القراءة</string>
<string name="search_on_s">بحث على %s</string>
<string name="delete_manga">حذف المانغا</string>
<string name="text_delete_local_manga">حذف \"%s\" من الجهاز نهائيا؟</string>
<string name="reader_settings">إعدادات القراءة</string>
<string name="switch_pages">تغییر صفحات</string>
<string name="delete">حذف</string>
<string name="share_image">شارك الصورة</string>
<string name="text_file_not_supported">إما أن تختار ملف ZIP أو CBZ.</string>
<string name="_import">استورد</string>
<string name="operation_not_supported">هذا خيار غير مدعم</string>
<string name="grid_size">حجم الشبكة</string>
<string name="volume_buttons">أزرار الصوت</string>
<string name="taps_on_edges">النقر على حواف الشاشة</string>
<string name="_continue">يكمل</string>
<string name="error">خطاء</string>
<string name="clear_search_history">مسح تاريخ البحث</string>
</resources>

View File

@@ -179,7 +179,7 @@
<string name="text_history_holder_secondary">Вы можаце знайсці, што пачытаць, у бакавым меню.</string>
<string name="text_history_holder_primary">Тут будзе паказана манга, якую вы чытаеце</string>
<string name="text_search_holder_secondary">Паспрабуйце перафармуляваць запыт.</string>
<string name="text_empty_holder_primary">Тут неяк пуста…</string>
<string name="text_empty_holder_primary">Неяк тут пуста…</string>
<string name="chapter_is_missing">Глава адсутнічае</string>
<string name="queued">У чарзе</string>
<string name="about_app_translation_summary">Дапамагчы з перакладам праграмы</string>
@@ -191,7 +191,7 @@
<string name="state_finished">Завершана</string>
<string name="state_ongoing">Ангоінг</string>
<string name="system_default">Па змаўчанні</string>
<string name="exclude_nsfw_from_history">Не паказваць NSFW мангу з гісторыі</string>
<string name="exclude_nsfw_from_history">Выключыць NSFW мангу з гісторыі</string>
<string name="show_pages_numbers">Паказваць нумары старонак</string>
<string name="enabled_sources">Уключаныя крыніцы</string>
<string name="available_sources">Даступныя крыніцы</string>
@@ -199,11 +199,11 @@
<string name="screenshots_allow">Дазваляць</string>
<string name="screenshots_policy">Палітыка скрыншотаў</string>
<string name="screenshots_block_all">Заўсёды блакуйце</string>
<string name="screenshots_block_nsfw">Блок на NSFW</string>
<string name="screenshots_block_nsfw">Забараніць для NSFW</string>
<string name="filter_load_error">Немагчыма загрузіць спіс жанраў</string>
<string name="disabled">Адключаны</string>
<string name="enabled">Уключаны</string>
<string name="exclude_nsfw_from_suggestions">Не прапануйце мангу NSFW</string>
<string name="exclude_nsfw_from_suggestions">Ня прапаноўваць NSFW мангу</string>
<string name="text_suggestion_holder">Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы</string>
<string name="suggestions_info">Усе даныя аналізуюцца толькі лакальна на гэтай прыладзе і нікуды не адпраўляюцца.</string>
<string name="suggestions_summary">Прапануеце мангу, заснаваную на вашых перавагах</string>
@@ -240,7 +240,7 @@
<string name="removed_from_history">Выдалена з гісторыі</string>
<string name="dns_over_https">DNS праз HTTPS</string>
<string name="detect_reader_mode">Аўтавызначэнне рэжыму чытання</string>
<string name="detect_reader_mode_summary">Аўтаматычна вызначае, ці зяўляецца манга вэбтунам</string>
<string name="detect_reader_mode_summary">Аўтаматычна вызначае, ці з\'яўляецца манга вэб-коміксам</string>
<string name="new_sources_text">Даступныя новыя крыніцы мангі</string>
<string name="download_slowdown">Запавольванне спампоўкі</string>
<string name="suggestions_excluded_genres">Выключыць жанры</string>
@@ -299,7 +299,7 @@
<string name="different_languages">Розныя мовы</string>
<string name="network_unavailable">Сетка недаступная</string>
<string name="network_unavailable_hint">Каб чытаць мангу онлайн, уключыце Wi-Fi або мабільную сетку</string>
<string name="webtoon_zoom">Webtoon зум</string>
<string name="webtoon_zoom">Маштабаванне ў рэжыме манхвы</string>
<string name="theme_name_dynamic">Дынамічны</string>
<string name="color_theme">Каляровая гама</string>
<string name="language">Мова</string>
@@ -426,9 +426,21 @@
<string name="show_pages_numbers_summary">Паказаць нумары старонак у ніжнім куце</string>
<string name="network">Сетка</string>
<string name="data_and_privacy">Дадзеныя і канфідэнцыяльнасць</string>
<string name="webtoon_zoom_summary">Дазволіць жэст для павелічэння ў рэжыме webtoon</string>
<string name="webtoon_zoom_summary">Уключыць жэст павелічэння маштабу ў рэжыме манхвы</string>
<string name="details_button_tip">Націсніце і ўтрымлівайце кнопку \"Чытаць\", каб убачыць дадатковыя параметры</string>
<string name="restore_summary">Аднавіць раней створаную рэзервовую копію</string>
<string name="reader_info_bar_summary">Паказаць бягучы час і ход чытання ў верхняй частцы экрана</string>
<string name="pages_animation_summary">Анімацыя перагортвання старонак</string>
<string name="clear_source_cookies_summary">Выдаліць файлы cookie толькі для вызначанага дамена. У большасці выпадкаў гэта робіць аўтарызацыю несапраўднай</string>
<string name="download_option_whole_manga">Манга цалкам</string>
<string name="local_manga_directories">Лакальныя каталогі мангі</string>
<string name="download_option_all_chapters">Усе раздзелы з перакладам %s</string>
<string name="download_option_first_n_chapters">Першыя %s</string>
<string name="download_option_all_unread_b">Усе непрачытаныя раздзелы (%s)</string>
<string name="download_option_all_unread">Усе непрачытаныя раздзелы</string>
<string name="download_option_manual_selection">Выбірайце раздзелы ўручную</string>
<string name="pick_custom_directory">Выберыце карыстальніцкі каталог</string>
<string name="download_option_next_unread_n_chapters">Наступная непрачытаная %s</string>
<string name="custom_directory">Карыстацкі каталог</string>
<string name="no_access_to_file">У вас няма доступу да гэтага файла або каталога</string>
</resources>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="items">
<item quantity="one">%1$d předmět</item>
<item quantity="few">%1$d předměty</item>
<item quantity="other">%1$d předměty</item>
</plurals>
<plurals name="new_chapters">
<item quantity="one">%1$d nová kapitola</item>
<item quantity="few">%1$d nové kapitoly</item>
<item quantity="other">%1$d nové kapitoly</item>
</plurals>
<plurals name="chapters">
<item quantity="one">%1$d kapitola</item>
<item quantity="few">%1$d kapitoly</item>
<item quantity="other">%1$d kapitoly</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="one">%1$d před minutou</item>
<item quantity="few">%1$d před pár minutami</item>
<item quantity="other">%1$d před několika minutami</item>
</plurals>
<plurals name="hours_ago">
<item quantity="one">%1$d před hodinou</item>
<item quantity="few">%1$d před pár hodinamy</item>
<item quantity="other">%1$d před několika hodinamy</item>
</plurals>
<plurals name="days_ago">
<item quantity="one">%1$d před dnem</item>
<item quantity="few">%1$d před pár dny</item>
<item quantity="other">%1$d před několika dny</item>
</plurals>
</resources>

View File

@@ -0,0 +1,446 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="network_error">Problém s připojením</string>
<string name="details">Detaily</string>
<string name="chapters">Kapitoly</string>
<string name="list">Seznam</string>
<string name="detailed_list">Podrobný seznam</string>
<string name="grid">Mřížka</string>
<string name="list_mode">Režim seznamu</string>
<string name="settings">Nastavení</string>
<string name="remote_sources">Zdroje mang</string>
<string name="loading_">Načítání…</string>
<string name="computing_">Vypočítávání…</string>
<string name="chapter_d_of_d">Kapitola %1$d z %2$d</string>
<string name="close">Zavřít</string>
<string name="try_again">Zkusit znovu</string>
<string name="clear_history">Vyčistit historii</string>
<string name="nothing_found">Nic nenalezeno</string>
<string name="history_is_empty">Zatím žádná historie</string>
<string name="read">Číst</string>
<string name="you_have_not_favourites_yet">Zatím žádné oblíbené</string>
<string name="add_to_favourites">Oblíbit toto</string>
<string name="add">Přidat</string>
<string name="share">Sdílet</string>
<string name="create_shortcut">Vytvořit zkratku…</string>
<string name="share_s">Sdílet %s</string>
<string name="processing_">Zpracovávání…</string>
<string name="download_complete">Staženo</string>
<string name="downloads">Stažené</string>
<string name="by_name">Název</string>
<string name="sort_order">Pořadí řazení</string>
<string name="filter">Filtr</string>
<string name="theme">Téma</string>
<string name="light">Světlé</string>
<string name="dark">Tmavé</string>
<string name="automatic">Následovat systém</string>
<string name="remove">Odstranit</string>
<string name="_s_deleted_from_local_storage">\"%s\" smazáno z místního uložiště</string>
<string name="share_image">Sdílet obrázek</string>
<string name="_import">Importovat</string>
<string name="delete">Smazat</string>
<string name="text_file_not_supported">Vyberte buď soubor ZIP či CBZ.</string>
<string name="clear_pages_cache">Vyčistit mezipaměť stran</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">Standardní</string>
<string name="webtoon">Webtoon</string>
<string name="read_mode">Režim čtení</string>
<string name="grid_size">Velikost mřížky</string>
<string name="delete_manga">Smazat mangu</string>
<string name="text_delete_local_manga">Smazat \"%s\" permanentně z tohoto zařízení\?</string>
<string name="reader_settings">Nastavení čtečky</string>
<string name="switch_pages">Prohodit strany</string>
<string name="taps_on_edges">Rohová kliknutí</string>
<string name="volume_buttons">Tlačítka hlasitosti</string>
<string name="_continue">Pokračovat</string>
<string name="error">Chyba</string>
<string name="clear_thumbs_cache">Vyčistit mezipamět náhledů</string>
<string name="clear_search_history">Vyčisit historii vyhledávání</string>
<string name="domain">Doména</string>
<string name="app_update_available">Je dostupná nová verze této aplikace</string>
<string name="open_in_browser">Otevřít v prohlížeči</string>
<string name="large_manga_save_confirm">Tato manga má %s. Uložit všechny\?</string>
<string name="save_manga">Uložit</string>
<string name="notifications">Oznámení</string>
<string name="text_empty_holder_primary">Je tu nějak prázdno…</string>
<string name="text_search_holder_secondary">Zkuste přeformulovat dotaz.</string>
<string name="text_history_holder_primary">To co čtete se zobrazí zde</string>
<string name="text_history_holder_secondary">Zjistěte co číst na boční nabídce.</string>
<string name="text_shelf_holder_primary">Vaše manga bude zobrazena zde</string>
<string name="text_shelf_holder_secondary">Zjistěte co číst v sekci «Prozkoumat»</string>
<string name="text_local_holder_primary">Nejdříve něco uložte</string>
<string name="text_local_holder_secondary">Uložte to z online zdrojů nebo importujte soubor.</string>
<string name="recent_manga">Nedávné</string>
<string name="manga_shelf">Polička</string>
<string name="pages_animation">Animace stránek</string>
<string name="manga_save_location">Složka pro stahování</string>
<string name="not_available">Není dostupné</string>
<string name="cannot_find_available_storage">Není dostupné uložiště</string>
<string name="other_storage">Další uložiště</string>
<string name="done">Hotovo</string>
<string name="all_favourites">Všechny oblíbené</string>
<string name="favourites_category_empty">Prázdná kategorie</string>
<string name="text_feed_holder">Nové kapitoly toho co čtete jsou zobrazeny zde</string>
<string name="search_results">Výsledky vyhledávání</string>
<string name="new_version_s">Nová verze: %s</string>
<string name="updates_feed_cleared">Vyčištěno</string>
<string name="rotate_screen">Otočit displej</string>
<string name="update">Aktualizovat</string>
<string name="dont_check">Nekontrolovat</string>
<string name="enter_password">Zadejte heslo</string>
<string name="wrong_password">Špatné heslo</string>
<string name="protect_application">Chránit aplikaci</string>
<string name="repeat_password">Zopakujte heslo</string>
<string name="about">O</string>
<string name="app_version">Verze %s</string>
<string name="check_for_updates">Zkontrolovat aktualizace</string>
<string name="no_update_available">Nejsou dostupné žádné aktualizace</string>
<string name="scale_mode">Režim měřítka</string>
<string name="zoom_mode_fit_center">Pasovat do středu</string>
<string name="zoom_mode_fit_height">Pasovat na výšku</string>
<string name="zoom_mode_fit_width">Pasovat na šířku</string>
<string name="zoom_mode_keep_start">Uchovat při zapnutí</string>
<string name="black_dark_theme">Černá</string>
<string name="black_dark_theme_summary">Využívá méně energie na AMOLED displejích</string>
<string name="backup_restore">Zálohovat a obnovit</string>
<string name="create_backup">Vytvořit zálohu dat</string>
<string name="restore_backup">Obnovit ze zálohy</string>
<string name="data_restored">Obnoveno</string>
<string name="preparing_">Připravuji…</string>
<string name="file_not_found">Soubor nebyl nalezen</string>
<string name="data_restored_success">Všechna data byla obnovena</string>
<string name="clear_updates_feed">Vyčistit frontu aktualizací</string>
<string name="feed_will_update_soon">Aktualizace fronty brzy začne</string>
<string name="just_now">Právě teď</string>
<string name="long_ago">Dávno</string>
<string name="group">Skupina</string>
<string name="today">Dnes</string>
<string name="tap_to_try_again">Klikněte pro zopakování</string>
<string name="reader_mode_hint">Zvolená konfigurace bude pro tuto mangu zapamatována</string>
<string name="silent">Tiché</string>
<string name="captcha_required">Vyžadována CAPTCHA</string>
<string name="captcha_solve">Vyřešit</string>
<string name="text_clear_updates_feed_prompt">Vyčistit permanentně všechnu historii aktualizací\?</string>
<string name="check_for_new_chapters">Zkontrolovat nové kapitoly</string>
<string name="reverse">Obráceně</string>
<string name="sign_in">Přihlásit se</string>
<string name="auth_required">Abyste mohli vidět tento kontent, je nutné se přihlásit</string>
<string name="default_s">Základní: %s</string>
<string name="next">Další</string>
<string name="protect_application_subtitle">Zadejte heslo pro spuštění aplikace s</string>
<string name="password_length_hint">Heslo musí obsahovat nejméně 4 nebo více znaků</string>
<string name="text_clear_search_history_prompt">Odstranit permanentně všechny nedávné vyhledávání\?</string>
<string name="welcome">Vítejte</string>
<string name="backup_saved">Záloha uložena</string>
<string name="queued">Ve frontě</string>
<string name="chapter_is_missing">Chybí kapitola</string>
<string name="about_app_translation">Překlad</string>
<string name="auth_complete">Autorizováno</string>
<string name="auth_not_supported_by">Přihlášení na %s není podporováno</string>
<string name="text_clear_cookies_prompt">Budete odhlášeni ze všech zdrojů</string>
<string name="genres">Žánry</string>
<string name="state_ongoing">Pokračující</string>
<string name="system_default">Základní</string>
<string name="exclude_nsfw_from_history">Vynechat všechny NSFW mangy z historie</string>
<string name="show_pages_numbers">Očíslované stránky</string>
<string name="enabled_sources">Použité zdroje</string>
<string name="screenshots_policy">Zásady snímku obrazovky</string>
<string name="screenshots_allow">Povolit</string>
<string name="screenshots_block_nsfw">Zakázat na NSFW</string>
<string name="screenshots_block_all">Vždy blokovat</string>
<string name="suggestions">Návrhy</string>
<string name="suggestions_enable">Zapnout návrhy</string>
<string name="suggestions_summary">Navrhovat mangy dle vašich preferencí</string>
<string name="suggestions_info">Všechna data jsou analyzována pouze lokálně na tomto zařízení a nikdy nejsou nikam odesílána.</string>
<string name="text_suggestion_holder">Začněte číst mangu a získejte personalizované návrhy</string>
<string name="exclude_nsfw_from_suggestions">Nenavrhovat NSFW mangu</string>
<string name="enabled">Zapnuto</string>
<string name="disabled">Vypnuto</string>
<string name="filter_load_error">Nelze načíst seznam žánrů</string>
<string name="reset_filter">Resetovat filtr</string>
<string name="onboard_text">Vyberte jazyk ve kterém si přejete číst mangu. Můžete jej později změnit v nastavení.</string>
<string name="never">Nikdy</string>
<string name="only_using_wifi">Pouze na Wi-Fi</string>
<string name="always">Vždy</string>
<string name="preload_pages">Přednačíst stránky</string>
<string name="logged_in_as">Přihlášen jako %s</string>
<string name="nsfw">18+</string>
<string name="various_languages">Různé jazyky</string>
<string name="search_chapters">Najít kapitolu</string>
<string name="chapters_empty">Žádné kapitoly v této manze</string>
<string name="percent_string_pattern">%1$s%%</string>
<string name="appearance">Vzhled</string>
<string name="suggestions_excluded_genres_summary">Specifikujte žánry které nechcete vidět v návrzích</string>
<string name="text_delete_local_manga_batch">Permanentně odstranit vybrané předměty ze zařízení\?</string>
<string name="removal_completed">Odstraňování dokončeno</string>
<string name="download_slowdown">Zpomalení stahování</string>
<string name="local_manga_processing">Zpracovávání uložených mang</string>
<string name="chapters_will_removed_background">Kapitoly budou na pozadí odstraněny</string>
<string name="account_already_exists">Účet již existuje</string>
<string name="back">Zpět</string>
<string name="sync_title">Synchronizujte svá data</string>
<string name="email_enter_hint">Pro pokračování zadejte svůj email</string>
<string name="hide">Schovat</string>
<string name="new_sources_text">Jsou dostupné nové zdroje mang</string>
<string name="check_new_chapters_title">Kontrolovat nové kapiroly a upozornit na ně</string>
<string name="show_notification_new_chapters_on">Dostanete oznámení o aktualizaci mang které čtete</string>
<string name="notifications_enable">Zapnout oznámení</string>
<string name="name">Jméno</string>
<string name="edit">Upravit</string>
<string name="edit_category">Upravit kategorii</string>
<string name="tracking">Sledování</string>
<string name="empty_favourite_categories">Žádné oblíbené kategorie</string>
<string name="logout">Odhlásit se</string>
<string name="bookmark_add">Přidat záložku</string>
<string name="bookmark_remove">Odstranit záložku</string>
<string name="bookmarks">Záložky</string>
<string name="bookmark_added">Přidána záložka</string>
<string name="undo">Vrátit zpět</string>
<string name="removed_from_history">Odstraněno z historie</string>
<string name="dns_over_https">DNS přes HTTPS</string>
<string name="default_mode">Základní režim</string>
<string name="detect_reader_mode">Automaticky detekovat režim čtení</string>
<string name="detect_reader_mode_summary">Automaticky detekovat zda je manga webtoon</string>
<string name="disable_battery_optimization">Vypnout optimalizaci baterie</string>
<string name="disable_battery_optimization_summary">Pomáhá s kontrolou nových kapitol</string>
<string name="send">Odeslat</string>
<string name="status_planned">Plánované</string>
<string name="status_reading">Čtení</string>
<string name="status_re_reading">Znovu-čtení</string>
<string name="status_completed">Dokončeno</string>
<string name="status_on_hold">Pozastaveno</string>
<string name="status_dropped">Zahozeno</string>
<string name="disable_all">Vypnout vše</string>
<string name="use_fingerprint">Pokud lze, použít otisk prstu</string>
<string name="appwidget_shelf_description">Manga z vašich oblíbených</string>
<string name="appwidget_recent_description">Vaše nedávno čtená manga</string>
<string name="report">Hlášení</string>
<string name="show_reading_indicators">Zobrazovat idikátory pokroku</string>
<string name="data_deletion">Smazání dat</string>
<string name="show_reading_indicators_summary">Zobrazovat procento čtení v historii a oblíbených</string>
<string name="exclude_nsfw_from_history_summary">Manga označená jako NSFW nebude nikdy přidána do historie a nebude ukládán váš pokrok</string>
<string name="clear_cookies_summary">Může pomoci v případě nekterých problémů. Všechny autorizace budou neplatné</string>
<string name="show_all">Zobrazit vše</string>
<string name="invalid_domain_message">Neplatná doména</string>
<string name="clear_all_history">Vyčistit celou historii</string>
<string name="history_cleared">Historie vyčištěna</string>
<string name="manage">Spravovat</string>
<string name="no_bookmarks_yet">Zatím žádné záložky</string>
<string name="bookmarks_removed">Záložky odstraněny</string>
<string name="no_manga_sources">Žádné zdroje mang</string>
<string name="no_manga_sources_text">Zapnout zdroje mang pro čtení online</string>
<string name="categories_delete_confirm">Jste si jisti že chcete smazat zvolené oblíbené kategorie\?
\nVšechny mangy v ní budou ztraceny a nelze jej vrátit zpět.</string>
<string name="reorder">Přeskupit</string>
<string name="empty">Prázdné</string>
<string name="explore">Prozkoumat</string>
<string name="confirm_exit">Stiskněte Zpět znovu pro ukončení</string>
<string name="exit_confirmation_summary">Stiskněte Zpět dvakrát pro ukončení aplikace</string>
<string name="exit_confirmation">Potvrzení ukončení</string>
<string name="saved_manga">Uložená manga</string>
<string name="pages_cache">Mezipaměť stránek</string>
<string name="other_cache">Další mezipamět</string>
<string name="storage_usage">Využívání uložiště</string>
<string name="available">Dostupné</string>
<string name="memory_usage_pattern">%s -%s</string>
<string name="removed_from_favourites">Odstraněno z oblíbených</string>
<string name="options">Možnosti</string>
<string name="not_found_404">Kontent nebyl nalezen nebo byl odstraněn</string>
<string name="incognito_mode">Režim inkognito</string>
<string name="no_chapters">Žádné kapitoly</string>
<string name="automatic_scroll">Automatické rolování</string>
<string name="reader_info_pattern">Kap. %1$d/%2$d Str. %3$d/%4$d</string>
<string name="reader_info_bar">Zobrazovat informační lištu v čtečce</string>
<string name="comics_archive">Archív komiksů</string>
<string name="folder_with_images">Složka s obrázky</string>
<string name="importing_manga">Importuji mangu</string>
<string name="import_will_start_soon">Importování brzy začne</string>
<string name="feed">Fronta</string>
<string name="manga_error_description_pattern">Podrobnosti chyby:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Zkuste &lt;a href=%2$s&gt;otveřít mangu v prohlížeči&lt;/a&gt; abyste se ujistili že je dostupná na zdroji&lt;br&gt;2. Ujistěte se že používáte &lt;a href=kotatsu://about&gt;nejnovější verzi Kotatsu&lt;/a&gt;&lt;br&gt;3. Pokud je dostupná, pošlete hlášení o chybě vývojářům.</string>
<string name="history_shortcuts">Zobrazovat zkratky nedávných mang</string>
<string name="history_shortcuts_summary">Udělejte nedávné mangy dostupné dlouhým kliknutím na ikonu aplikace</string>
<string name="reader_control_ltr_summary">Klikněte do pravého rohu nebo stisknutí pravého tlačítka vždy změní na další stranu</string>
<string name="reader_control_ltr">Ovládání ergonomické čtečky</string>
<string name="color_correction">Korekce barev</string>
<string name="brightness">Jas</string>
<string name="contrast">Kontrast</string>
<string name="reset">Resetovat</string>
<string name="color_correction_hint">Vybrané nastavení barev bude pro tuto mangu zapamatováno</string>
<string name="text_unsaved_changes_prompt">Uložit nebo zahodit neuložené změny\?</string>
<string name="discard">Zahodit</string>
<string name="error_no_space_left">Na zařízení není volné žádné místo</string>
<string name="reader_slider">Zobrazovat posuvník změny stran</string>
<string name="webtoon_zoom">Webtoon přiblížení</string>
<string name="different_languages">Různé jazyky</string>
<string name="network_unavailable">Připojení není dostupné</string>
<string name="network_unavailable_hint">Zapněte Wi-Fi nebo mobilní data abyste mohli číst mangu online</string>
<string name="server_error">Chyba na straně serveru (%1$d). Prosíme zkuste to znovu později</string>
<string name="clear_new_chapters_counters">Také vyčistit informace o nových kapitolách</string>
<string name="compact">Kompaktní</string>
<string name="source_disabled">Zdroj vypnut</string>
<string name="prefetch_content">Přednačítání kontentu</string>
<string name="mark_as_current">Označit jako aktuální</string>
<string name="language">Jazyk</string>
<string name="share_logs">Sdílet záznamy</string>
<string name="enable_logging">Zapnout zaznamenávání</string>
<string name="enable_logging_summary">Zaznamenat některé akce pro spravovací účely</string>
<string name="show_suspicious_content">Zobrazovat podezřelý kontent</string>
<string name="theme_name_dynamic">Dynamické</string>
<string name="color_theme">Schéma barev</string>
<string name="show_in_grid_view">Zobrazovat v zobrazení mřížek</string>
<string name="theme_name_miku">Miku</string>
<string name="theme_name_asuka">Asuka</string>
<string name="theme_name_mion">Mion</string>
<string name="theme_name_rikka">Rikka</string>
<string name="theme_name_sakura">Sakura</string>
<string name="theme_name_mamimi">Mamimi</string>
<string name="theme_name_kanade">Kanade</string>
<string name="nothing_here">Nic tu není</string>
<string name="scrobbling_empty_hint">Pro sledování pokroku čtení, vyberte Menu → Sledovat na displeji detailů mangy.</string>
<string name="services">Služby</string>
<string name="allow_unstable_updates">Povolit nestabilní aktualizace</string>
<string name="allow_unstable_updates_summary">Navrhnout beta aktualizace této aplikace</string>
<string name="download_started">Stahování začalo</string>
<string name="got_it">Mám to</string>
<string name="sources_reorder_tip">Klikněte a přidržte na předmětu pro přeskupení</string>
<string name="user_agent">UserAgent header</string>
<string name="settings_apply_restart_required">Prosíme restartujte aplikaci pro aplikování těchto změn</string>
<string name="comics_archive_import_description">Můžete vybrat jeden nebo vícs .cbz nebo .zip souborů, každý soubor bude znám jako samostatná manga.</string>
<string name="restore_backup_description">Importovat dřive vytvořenou zálohu uživatelských dat</string>
<string name="show_on_shelf">Zobrazovat na poličce</string>
<string name="sync_auth_hint">Můžete se přihlásit do již existujícího účtu nebo vytvořit nový</string>
<string name="find_similar">Najít podobné</string>
<string name="sync_settings">Nastavení synchronizace</string>
<string name="server_address">Adresa serveru</string>
<string name="ignore_ssl_errors">Ignorovat SSL chyby</string>
<string name="mirror_switching">Vybrat zrcadlo automaticky</string>
<string name="mirror_switching_summary">Automaticky prohodit doménu pro vzdálené zdroje při chybě pokud jsou zrcadla dostupná</string>
<string name="pause">Pozastavit</string>
<string name="resume">Vrátit</string>
<string name="paused">Pozastaveno</string>
<string name="remove_completed">Odstranění dokončeno</string>
<string name="downloads_wifi_only_summary">Zastavit stahování při měnění na mobilní data</string>
<string name="suggestion_manga">Doporučení: %s</string>
<string name="suggestions_notifications_summary">Občas zobrazit oznámení s navrženou mangou</string>
<string name="more">Více</string>
<string name="enable">Zapnout</string>
<string name="no_thanks">Ne, děkuji</string>
<string name="cancel_all_downloads_confirm">Všechna aktivní stahování budou zrušena, částečně stažená data budou ztracena</string>
<string name="remove_completed_downloads_confirm">Vaše historie stahování bude permanentně odstraněna</string>
<string name="text_downloads_list_holder">Nemáte žádná stažení</string>
<string name="downloads_resumed">Stahování bylo vráceno</string>
<string name="downloads_paused">Stahování bylo pozastaveno</string>
<string name="downloads_removed">Stažené soubory byly odstraněny</string>
<string name="downloads_cancelled">Stahování bylo zrušeno</string>
<string name="suggestions_enable_prompt">Chcete získávat personalizované návrhy mang\?</string>
<string name="translations">Překlady</string>
<string name="web_view_unavailable">WebView není dostupné: zkontrolovay jestli je provozovatel WebView nainstalovaný</string>
<string name="clear_network_cache">Vyčistit mezipaměť sítě</string>
<string name="type">Typ</string>
<string name="address">Adresa</string>
<string name="invalid_value_message">Neplatná hodnota</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="downloaded">Staženo</string>
<string name="images_proxy_title">Proxy pro optimalizaci obrázků</string>
<string name="images_procy_description">Použít službu wsrv.nl ke snížení využití provozu a urychlení načítání obrázků, pokud je to možné</string>
<string name="invert_colors">Invertovat barvy</string>
<string name="username">Uživatelské jméno</string>
<string name="password">Heslo</string>
<string name="authorization_optional">Autorizace (dobrovolné)</string>
<string name="invalid_port_number">Neplatné číslo portu</string>
<string name="network">Síť</string>
<string name="data_and_privacy">Data a soukromí</string>
<string name="restore_summary">Obnovit nedávno vytvořenou zálohu</string>
<string name="reader_info_bar_summary">Zobrazovat aktuální čas a pokrok čtení na vršku displeje</string>
<string name="show_pages_numbers_summary">Zobrazovat číslo strany ve spodním rohu</string>
<string name="pages_animation_summary">Animovat změny stran</string>
<string name="details_button_tip">Stiskněte a podržte tlačítko Číst pro více možností</string>
<string name="download_option_all_chapters">Všechny kapitoly s překladem %s</string>
<string name="download_option_whole_manga">Celá manga</string>
<string name="download_option_first_n_chapters">První %s</string>
<string name="download_option_next_unread_n_chapters">Další nepřečtené %s</string>
<string name="download_option_all_unread">Všechny nepřečtené kapitoly</string>
<string name="download_option_all_unread_b">Všechny nepřečtené kapitoly (%s)</string>
<string name="custom_directory">Vlastní adrešář</string>
<string name="pick_custom_directory">Vyberte vlastní adresář</string>
<string name="no_access_to_file">Nemáte žádný přístup k tomuto souboru nebo adresáři</string>
<string name="local_manga_directories">Lokální adresář mang</string>
<string name="local_storage">Místní uložiště</string>
<string name="favourites">Oblíbené</string>
<string name="history">Historie</string>
<string name="error_occurred">Vyskytla se chyba</string>
<string name="add_new_category">Nová kategorie</string>
<string name="save">Uložit</string>
<string name="search">Hledat</string>
<string name="manga_downloading_">Stahování…</string>
<string name="updated">Aktualizováno</string>
<string name="by_rating">Hodnocení</string>
<string name="search_manga">Hledat mangu</string>
<string name="popular">Populární</string>
<string name="newest">Nejnovější</string>
<string name="text_clear_history_prompt">Permanentně vyčistit celou historii čtení\?</string>
<string name="no_description">Žádný popis</string>
<string name="pages">Strany</string>
<string name="clear">Vyčistit</string>
<string name="save_page">Uložit stranu</string>
<string name="page_saved">Uloženo</string>
<string name="operation_not_supported">Tato operace není podporována</string>
<string name="search_on_s">Hledat na %s</string>
<string name="internal_storage">Interní uložiště</string>
<string name="enabled_d_of_d" tools:ignore="PluralsCandidate">%1$d z %2$d na</string>
<string name="notification_sound">Zvuk oznámení</string>
<string name="search_history_cleared">Vyčištěno</string>
<string name="gestures_only">Pouze gesta</string>
<string name="external_storage">Externí uložiště</string>
<string name="new_chapters">Nové kapitoly</string>
<string name="download">Stáhnout</string>
<string name="notifications_settings">Nastavení oznámení</string>
<string name="light_indicator">LED indikátor</string>
<string name="vibration">Vibrace</string>
<string name="favourites_categories">Oblíbené kategorie</string>
<string name="remove_category">Odstranit</string>
<string name="read_later">Přečíst později</string>
<string name="updates">Aktualizace</string>
<string name="size_s">Velikost: %s</string>
<string name="passwords_mismatch">Neodpovídající heslo</string>
<string name="track_sources">Hledat aktualizace</string>
<string name="protect_application_summary">Zeptat se na heslo při zapnutí Kotatsu</string>
<string name="yesterday">Včera</string>
<string name="right_to_left">Z prava doleva</string>
<string name="clear_feed">Vyčistit frontu</string>
<string name="create_category">Nová kategorie</string>
<string name="clear_cookies">Vyčistit cookies</string>
<string name="data_restored_with_errors">Data byla obnovena, ale vyskytly se chyby</string>
<string name="backup_information">Můžete vytvořit zálohu vaší historie a oblíbených a obnovit jej</string>
<string name="cookies_cleared">Všechny cookies byli odstraněny</string>
<string name="suggestions_updating">Aktualizují se návrhy</string>
<string name="tracker_warning">Některá zařízení mají jiné systémové chování, to může rozbít procesy na pozadí.</string>
<string name="read_more">Číst více</string>
<string name="about_app_translation_summary">Přeložte tuto aplikaci</string>
<string name="confirm">Potvrdit</string>
<string name="available_sources">Dostupné zdroje</string>
<string name="suggestions_excluded_genres">Vynechat žánry</string>
<string name="download_slowdown_summary">Pomohá předejít zablokování vaší IP adresy</string>
<string name="canceled">Zrušeno</string>
<string name="sync">Synchronizace</string>
<string name="show_notification_new_chapters_off">Nedostanete oznámení ale nové kapitoly budou v seznamu zvýrazněné</string>
<string name="bookmark_removed">Záložka odstraněna</string>
<string name="crash_text">Něco se pokazilo. Prosím odešlete hlášení o chybě vývojářům aby jste nám ji pomohli opravit.</string>
<string name="state_finished">Hotovo</string>
<string name="select_range">Vybrat rozsah</string>
<string name="last_2_hours">Poslední 2 hodiny</string>
<string name="no_bookmarks_summary">Můžete vytvořit záložku při čtení mangy</string>
<string name="random">Náhodné</string>
<string name="import_completed">Importování dokončeno</string>
<string name="import_completed_hint">Můžete odstranit originální soubor z uložiště abyste ušetřili místo</string>
<string name="folder_with_images_import_description">Můžete vybrat adresář s archivy nebo obrázky. Každý archiv (nebo podkategorie) bude znám jako kapitola.</string>
<string name="speed">Rychlost</string>
<string name="downloads_wifi_only">Stahovat pouze přes Wi-Fi</string>
<string name="sync_host_description">Můžete použít samostatně hostovaný synchronizační server nebo základní. Neměňte pokud si nejste jisti co děláte.</string>
<string name="cancel_all">Zrušit vše</string>
<string name="proxy">Proxy</string>
<string name="port">Port</string>
<string name="webtoon_zoom_summary">Povolit přiblížení v gestu ve webtoon režimu</string>
<string name="clear_source_cookies_summary">Vyčistit cookies pouze pro specifikované domény. Ve většině případech bude neplatná autorizace</string>
<string name="download_option_manual_selection">Vyberte kapitoly manuálně</string>
</resources>

View File

@@ -1,82 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="local_storage">Εσωτερικός χώρος</string>
<string name="favourites">Αγαπημένα</string>
<string name="history">Ιστορικό</string>
<string name="error_occurred">Προέκυψε σφάλμα</string>
<string name="try_again">Επανάληψη</string>
<string name="grid">Πλέγμα</string>
<string name="list_mode">Εμφάνιση ως λίστα</string>
<string name="settings">Ρυθμίσεις</string>
<string name="remote_sources">Απομακρυσμένες πηγές</string>
<string name="computing_">Επεξεργασία…</string>
<string name="close">Κλείσιμο</string>
<string name="clear_history">Εκκαθάριση ιστορικού</string>
<string name="nothing_found">Δεν βρέθηκε τίποτα</string>
<string name="history_is_empty">Κενό ιστορικό</string>
<string name="read">Διάβασε</string>
<string name="add_to_favourites">Προσθήκη στα αγαπημένα</string>
<string name="add_new_category">Νέα κατηγορία</string>
<string name="save">Αποθήκευση</string>
<string name="share">Κοινοποιήση</string>
<string name="create_shortcut">Δημιουργία συντόμευσης…</string>
<string name="share_s">Κοινοποίηση %s</string>
<string name="search">Αναζήτηση</string>
<string name="search_manga">Αναζήτηση μάνγκα</string>
<string name="manga_downloading_">Λήψη…</string>
<string name="download_complete">Κατεβασμένο</string>
<string name="downloads">Λήψεις</string>
<string name="updated">Ενημερωμένο</string>
<string name="newest">Νεότερο</string>
<string name="by_rating">Βαθμολογία</string>
<string name="filter">Φίλτρο</string>
<string name="dark">Σκοτεινό</string>
<string name="automatic">Όπως στο σύστημα</string>
<string name="clear">Εκκαθάριση</string>
<string name="text_clear_history_prompt">Να διαγράψετε μόνιμα όλο το ιστορικό ανάγνωσης;</string>
<string name="remove">Διαγραφή</string>
<string name="save_page">Αποθήκευση σελίδας</string>
<string name="page_saved">Αποθηκευμένα</string>
<string name="share_image">Κοινή χρήση εικόνας</string>
<string name="_import">Εισαγωγή</string>
<string name="delete">Διαγραφή</string>
<string name="text_file_not_supported">Επιλέξτε ένα αρχείο ZIP ή CBZ.</string>
<string name="no_description">Χωρίς περιγραφή</string>
<string name="clear_pages_cache">Εκκαθάριση μνήμης cache της σελίδας</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">Τυπικό</string>
<string name="webtoon">Μάνχγουα</string>
<string name="search_on_s">Αναζήτηση στο %s</string>
<string name="delete_manga">Διαγραφή μάνγκα</string>
<string name="text_delete_local_manga">Μόνιμη διαγραφή του \"%s\" από τη συσκευή;</string>
<string name="reader_settings">Ρυθμίσεις λειτουργίας ανάγνωσης</string>
<string name="switch_pages">Αλλαγή σελίδων</string>
<string name="network_error">Αδυναμία σύνδεσης στο ίντερνετ</string>
<string name="chapters">Κεφάλαια</string>
<string name="details">Πληροφορίες</string>
<string name="list">Λίστα</string>
<string name="detailed_list">Λεπτομερής λίστα</string>
<string name="loading_">Φόρτωση…</string>
<string name="chapter_d_of_d">Κεφάλαιο%1$d από %2$d</string>
<string name="you_have_not_favourites_yet">Δεν υπάρχουν αγαπημένα</string>
<string name="add">Προσθήκη</string>
<string name="processing_">Επεξεργασία…</string>
<string name="by_name">Όνομα</string>
<string name="popular">Δημοφιλή</string>
<string name="sort_order">Τρόπος Ταξινόμησης</string>
<string name="theme">Θέμα</string>
<string name="light">Φωτεινό</string>
<string name="pages">Σελίδες</string>
<string name="_s_deleted_from_local_storage">Το \"%s\" διαγράφηκε από τον τοπικό χώρο αποθήκευσης</string>
<string name="operation_not_supported">Αυτή η λειτουργία δεν υποστηρίζεται</string>
<string name="read_mode">Λειτουργία ανάγνωσης</string>
<string name="grid_size">Μέγεθος πλέγματος</string>
<string name="theme_name_mamimi">Μαμίμι</string>
<string name="theme_name_kanade">Κανάντε</string>
<string name="nothing_here">Δεν υπάρχει τίποτα εδώ</string>
<string name="theme_name_miku">Μίκου</string>
<string name="theme_name_asuka">Άσουκα</string>
<string name="theme_name_mion">Μιόν</string>
<string name="theme_name_rikka">Ρίκκα</string>
<string name="theme_name_sakura">Σακούρα</string>
</resources>
<string name="local_storage">Εσωτερικός χώρος</string>
<string name="favourites">Αγαπημένα</string>
<string name="history">Ιστορικό</string>
<string name="error_occurred">Προέκυψε σφάλμα</string>
<string name="try_again">Επανάληψη</string>
<string name="grid">Πλέγμα</string>
<string name="list_mode">Εμφάνιση ως λίστα</string>
<string name="settings">Ρυθμίσεις</string>
<string name="remote_sources">Πηγές μάνγκα</string>
<string name="computing_">Επεξεργασία…</string>
<string name="close">Κλείσιμο</string>
<string name="clear_history">Εκκαθάριση ιστορικού</string>
<string name="nothing_found">Δεν βρέθηκε τίποτα</string>
<string name="history_is_empty">Κενό ιστορικό</string>
<string name="read">Διάβασε</string>
<string name="add_to_favourites">Προσθήκη στα αγαπημένα</string>
<string name="add_new_category">Νέα κατηγορία</string>
<string name="save">Αποθήκευση</string>
<string name="share">Κοινοποιήση</string>
<string name="create_shortcut">Δημιουργία συντόμευσης…</string>
<string name="share_s">Κοινοποίηση %s</string>
<string name="search">Αναζήτηση</string>
<string name="search_manga">Αναζήτηση μάνγκα</string>
<string name="manga_downloading_">Λήψη…</string>
<string name="download_complete">Κατεβασμένο</string>
<string name="downloads">Λήψεις</string>
<string name="updated">Ενημερωμένο</string>
<string name="newest">Νεότερο</string>
<string name="by_rating">Βαθμολογία</string>
<string name="filter">Φίλτρο</string>
<string name="dark">Σκοτεινό</string>
<string name="automatic">Όπως στο σύστημα</string>
<string name="clear">Εκκαθάριση</string>
<string name="text_clear_history_prompt">Να διαγράψετε μόνιμα όλο το ιστορικό ανάγνωσης;</string>
<string name="remove">Διαγραφή</string>
<string name="save_page">Αποθήκευση σελίδας</string>
<string name="page_saved">Αποθηκευμένα</string>
<string name="share_image">Κοινή χρήση εικόνας</string>
<string name="_import">Εισαγωγή</string>
<string name="delete">Διαγραφή</string>
<string name="text_file_not_supported">Επιλέξτε ένα αρχείο ZIP ή CBZ.</string>
<string name="no_description">Χωρίς περιγραφή</string>
<string name="clear_pages_cache">Εκκαθάριση μνήμης cache της σελίδας</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
<string name="standard">Τυπικό</string>
<string name="webtoon">Μάνχγουα</string>
<string name="search_on_s">Αναζήτηση στο %s</string>
<string name="delete_manga">Διαγραφή μάνγκα</string>
<string name="text_delete_local_manga">Μόνιμη διαγραφή του \"%s\" από τη συσκευή;</string>
<string name="reader_settings">Ρυθμίσεις λειτουργίας ανάγνωσης</string>
<string name="switch_pages">Αλλαγή σελίδων</string>
<string name="network_error">Σφάλμα δικτύου</string>
<string name="chapters">Κεφάλαια</string>
<string name="details">Πληροφορίες</string>
<string name="list">Λίστα</string>
<string name="detailed_list">Λεπτομερής λίστα</string>
<string name="loading_">Φόρτωση…</string>
<string name="chapter_d_of_d">Κεφάλαιο%1$d από %2$d</string>
<string name="you_have_not_favourites_yet">Δεν υπάρχουν αγαπημένα</string>
<string name="add">Προσθήκη</string>
<string name="processing_">Επεξεργασία…</string>
<string name="by_name">Όνομα</string>
<string name="popular">Δημοφιλή</string>
<string name="sort_order">Τρόπος Ταξινόμησης</string>
<string name="theme">Θέμα</string>
<string name="light">Φωτεινό</string>
<string name="pages">Σελίδες</string>
<string name="_s_deleted_from_local_storage">Το \"%s\" διαγράφηκε από τον τοπικό χώρο αποθήκευσης</string>
<string name="operation_not_supported">Αυτή η λειτουργία δεν υποστηρίζεται</string>
<string name="read_mode">Λειτουργία ανάγνωσης</string>
<string name="grid_size">Μέγεθος πλέγματος</string>
<string name="theme_name_mamimi">Μαμίμι</string>
<string name="theme_name_kanade">Κανάντε</string>
<string name="nothing_here">Δεν υπάρχει τίποτα εδώ</string>
<string name="theme_name_miku">Μίκου</string>
<string name="theme_name_asuka">Άσουκα</string>
<string name="theme_name_mion">Μιόν</string>
<string name="theme_name_rikka">Ρίκκα</string>
<string name="theme_name_sakura">Σακούρα</string>
</resources>

View File

@@ -431,4 +431,16 @@
<string name="details_button_tip">Manten pulsado el botón Leer para ver más opciones</string>
<string name="restore_summary">Restaurar una copia de seguridad creada anteriormente</string>
<string name="reader_info_bar_summary">Muestra la hora actual y el progreso de la lectura en la parte superior de la pantalla</string>
<string name="clear_source_cookies_summary">Borrar las cookies solo para el dominio especificado. En la mayoría de los casos invalidará la autorización</string>
<string name="download_option_whole_manga">El manga completo</string>
<string name="download_option_first_n_chapters">Primero %s</string>
<string name="download_option_all_unread">Todos los capítulos sin leer</string>
<string name="download_option_all_unread_b">Todos los capítulos sin leer (%s)</string>
<string name="download_option_manual_selection">Selección manual de los capítulos</string>
<string name="download_option_all_chapters">Todos los capítulos con traducción %s</string>
<string name="download_option_next_unread_n_chapters">Siguiente %s sin leer</string>
<string name="pick_custom_directory">Elegir un directorio personalizado</string>
<string name="no_access_to_file">No tienes acceso a este archivo o directorio</string>
<string name="custom_directory">Directorio personalizado</string>
<string name="local_manga_directories">Directorios locales del manga</string>
</resources>

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