diff --git a/app/build.gradle b/app/build.gradle
index 71d5901ea..0f2d3fad3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
- versionCode 503
- versionName '4.0.3'
+ versionCode 508
+ versionName '4.1.1'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -83,7 +83,7 @@ afterEvaluate {
}
}
dependencies {
- implementation('com.github.KotatsuApp:kotatsu-parsers:bf8a1f3db2') {
+ implementation('com.github.KotatsuApp:kotatsu-parsers:c4acb9725f') {
exclude group: 'org.json', module: 'json'
}
@@ -91,7 +91,7 @@ dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.activity:activity-ktx:1.6.1'
- implementation 'androidx.fragment:fragment-ktx:1.5.4'
+ implementation 'androidx.fragment:fragment-ktx:1.5.5'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-service:2.5.1'
@@ -118,35 +118,35 @@ dependencies {
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.44"
- kapt "com.google.dagger:hilt-compiler:2.44"
+ implementation 'com.google.dagger:hilt-android:2.44.2'
+ kapt 'com.google.dagger:hilt-compiler:2.44.2'
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.2.2'
implementation 'io.coil-kt:coil-svg:2.2.2'
- implementation 'com.github.KotatsuApp:subsampling-scale-image-view:f8a38b08fe'
+ implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
implementation 'com.github.solkin:disk-lru-cache:1.4'
- implementation 'ch.acra:acra-http:5.9.6'
- implementation 'ch.acra:acra-dialog:5.9.6'
+ implementation 'ch.acra:acra-http:5.9.7'
+ implementation 'ch.acra:acra-dialog:5.9.7'
- debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
+ debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20220924'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
- androidTestImplementation 'androidx.test:runner:1.4.0'
- androidTestImplementation 'androidx.test:rules:1.4.0'
- androidTestImplementation 'androidx.test:core-ktx:1.4.0'
- androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
+ androidTestImplementation 'androidx.test:runner:1.5.1'
+ androidTestImplementation 'androidx.test:rules:1.5.0'
+ androidTestImplementation 'androidx.test:core-ktx:1.5.0'
+ androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.4'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.room:room-testing:2.4.3'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
- androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44'
- kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44'
+ androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44.2'
+ kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44.2'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4ca5eaf80..be81140ca 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,6 +29,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
+ android:localeConfig="@xml/locales"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
@@ -247,6 +248,9 @@
+
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt
index 32c9c2904..e7b6fc6db 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt
@@ -31,6 +31,9 @@ abstract class CoroutineIntentService : BaseService() {
processIntent(startId, intent)
}
}
+ } catch (e: Throwable) {
+ e.printStackTraceDebug()
+ onError(startId, e)
} finally {
stopSelf(startId)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt
index 3f7abea0b..d210c6991 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/SectionedSelectionController.kt
@@ -2,20 +2,20 @@ package org.koitharu.kotatsu.base.ui.list
import android.app.Activity
import android.os.Bundle
-import android.util.ArrayMap
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
+import androidx.collection.ArrayMap
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner
-import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
+import kotlin.coroutines.EmptyCoroutineContext
private const val PROVIDER_NAME = "selection_decoration_sectioned"
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScroller.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScroller.kt
index 23c79d7a2..e5cb94dd4 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScroller.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/list/fastscroll/FastScroller.kt
@@ -516,6 +516,6 @@ class FastScroller @JvmOverloads constructor(
interface SectionIndexer {
- fun getSectionText(context: Context, position: Int): CharSequence
+ fun getSectionText(context: Context, position: Int): CharSequence?
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt
index 264134e52..c6ad65f5f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt
@@ -4,12 +4,12 @@ import android.graphics.Bitmap
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.HttpUrl.Companion.toHttpUrl
-import org.koitharu.kotatsu.core.network.AndroidCookieJar
+import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
private const val CF_CLEARANCE = "cf_clearance"
class CloudFlareClient(
- private val cookieJar: AndroidCookieJar,
+ private val cookieJar: MutableCookieJar,
private val callback: CloudFlareCallback,
private val targetUrl: String,
) : WebViewClient() {
@@ -42,4 +42,4 @@ class CloudFlareClient(
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
.find { it.name == CF_CLEARANCE }?.value
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt
index dc18898b3..c2359ad91 100644
--- a/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt
@@ -12,13 +12,13 @@ import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
-import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
+import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
import org.koitharu.kotatsu.utils.ext.stringArgument
import org.koitharu.kotatsu.utils.ext.withArgs
+import javax.inject.Inject
@AndroidEntryPoint
class CloudFlareDialog : AlertDialogFragment(), CloudFlareCallback {
@@ -27,7 +27,7 @@ class CloudFlareDialog : AlertDialogFragment(), Cloud
private val pendingResult = Bundle(1)
@Inject
- lateinit var cookieJar: AndroidCookieJar
+ lateinit var cookieJar: MutableCookieJar
override fun onInflateView(
inflater: LayoutInflater,
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt
index 459ac9e8b..576e5f5a6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/AppModule.kt
@@ -4,6 +4,7 @@ import android.app.Application
import android.content.Context
import android.provider.SearchRecentSuggestions
import android.text.Html
+import android.util.AndroidRuntimeException
import androidx.collection.arraySetOf
import androidx.room.InvalidationTracker
import coil.ComponentRegistry
@@ -25,6 +26,10 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.*
+import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
+import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
+import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -39,6 +44,7 @@ import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.settings.backup.BackupObserver
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.utils.IncognitoModeIndicator
+import org.koitharu.kotatsu.utils.ext.connectivityManager
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.image.CoilImageGetter
import org.koitharu.kotatsu.widget.WidgetUpdater
@@ -50,7 +56,7 @@ import javax.inject.Singleton
interface AppModule {
@Binds
- fun bindCookieJar(androidCookieJar: AndroidCookieJar): CookieJar
+ fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
@Binds
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
@@ -60,6 +66,17 @@ interface AppModule {
companion object {
+ @Provides
+ @Singleton
+ fun provideCookieJar(
+ @ApplicationContext context: Context
+ ): MutableCookieJar = try {
+ AndroidCookieJar()
+ } catch (e: AndroidRuntimeException) {
+ // WebView is not available
+ PreferencesCookieJar(context)
+ }
+
@Provides
@Singleton
fun provideOkHttpClient(
@@ -81,6 +98,12 @@ interface AppModule {
}.build()
}
+ @Provides
+ @Singleton
+ fun provideNetworkState(
+ @ApplicationContext context: Context
+ ) = NetworkState(context.connectivityManager)
+
@Provides
@Singleton
fun provideMangaDatabase(
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt
index ee8255dfc..efca0e36b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/MangaDao.kt
@@ -14,11 +14,11 @@ abstract class MangaDao {
abstract suspend fun find(id: Long): MangaWithTags?
@Transaction
- @Query("SELECT * FROM manga WHERE title LIKE :query OR alt_title LIKE :query LIMIT :limit")
+ @Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
abstract suspend fun searchByTitle(query: String, limit: Int): List
@Transaction
- @Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source LIMIT :limit")
+ @Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List
@Insert(onConflict = OnConflictStrategy.IGNORE)
@@ -47,4 +47,4 @@ abstract class MangaDao {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt
index 8ae3565e5..b7147aadd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt
@@ -1,9 +1,9 @@
package org.koitharu.kotatsu.core.exceptions.resolve
-import android.util.ArrayMap
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
+import androidx.collection.ArrayMap
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.suspendCancellableCoroutine
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/AndroidCookieJar.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt
similarity index 73%
rename from app/src/main/java/org/koitharu/kotatsu/core/network/AndroidCookieJar.kt
rename to app/src/main/java/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt
index bb221d743..5b0c3d822 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/network/AndroidCookieJar.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/AndroidCookieJar.kt
@@ -1,19 +1,17 @@
-package org.koitharu.kotatsu.core.network
+package org.koitharu.kotatsu.core.network.cookies
import android.webkit.CookieManager
-import javax.inject.Inject
-import javax.inject.Singleton
+import androidx.annotation.WorkerThread
+import okhttp3.Cookie
+import okhttp3.HttpUrl
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
-import okhttp3.Cookie
-import okhttp3.CookieJar
-import okhttp3.HttpUrl
-@Singleton
-class AndroidCookieJar @Inject constructor() : CookieJar {
+class AndroidCookieJar : MutableCookieJar {
private val cookieManager = CookieManager.getInstance()
+ @WorkerThread
override fun loadForRequest(url: HttpUrl): List {
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
return rawCookie.split(';').mapNotNull {
@@ -21,6 +19,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
}
}
+ @WorkerThread
override fun saveFromResponse(url: HttpUrl, cookies: List) {
if (cookies.isEmpty()) {
return
@@ -31,7 +30,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
}
}
- suspend fun clear() = suspendCoroutine { continuation ->
+ override suspend fun clear() = suspendCoroutine { continuation ->
cookieManager.removeAllCookies(continuation::resume)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/CookieWrapper.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/CookieWrapper.kt
new file mode 100644
index 000000000..6254d720b
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/CookieWrapper.kt
@@ -0,0 +1,84 @@
+package org.koitharu.kotatsu.core.network.cookies
+
+import android.util.Base64
+import okhttp3.Cookie
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+
+
+class CookieWrapper(
+ val cookie: Cookie,
+) {
+
+ constructor(encodedString: String) : this(
+ ObjectInputStream(ByteArrayInputStream(Base64.decode(encodedString, Base64.NO_WRAP))).use {
+ val name = it.readUTF()
+ val value = it.readUTF()
+ val expiresAt = it.readLong()
+ val domain = it.readUTF()
+ val path = it.readUTF()
+ val secure = it.readBoolean()
+ val httpOnly = it.readBoolean()
+ val persistent = it.readBoolean()
+ val hostOnly = it.readBoolean()
+ 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()
+ }
+ }.build()
+ },
+ )
+
+ fun encode(): String {
+ val output = ByteArrayOutputStream()
+ ObjectOutputStream(output).use {
+ it.writeUTF(cookie.name)
+ it.writeUTF(cookie.value)
+ it.writeLong(cookie.expiresAt)
+ it.writeUTF(cookie.domain)
+ it.writeUTF(cookie.path)
+ it.writeBoolean(cookie.secure)
+ it.writeBoolean(cookie.httpOnly)
+ it.writeBoolean(cookie.persistent)
+ it.writeBoolean(cookie.hostOnly)
+ }
+ return Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
+ }
+
+ fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
+
+ fun key(): String {
+ return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as CookieWrapper
+
+ if (cookie != other.cookie) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return cookie.hashCode()
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt
new file mode 100644
index 000000000..9059e5a6f
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/MutableCookieJar.kt
@@ -0,0 +1,17 @@
+package org.koitharu.kotatsu.core.network.cookies
+
+import androidx.annotation.WorkerThread
+import okhttp3.Cookie
+import okhttp3.CookieJar
+import okhttp3.HttpUrl
+
+interface MutableCookieJar : CookieJar {
+
+ @WorkerThread
+ override fun loadForRequest(url: HttpUrl): List
+
+ @WorkerThread
+ override fun saveFromResponse(url: HttpUrl, cookies: List)
+
+ suspend fun clear(): Boolean
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt b/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt
new file mode 100644
index 000000000..cce51f827
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/network/cookies/PreferencesCookieJar.kt
@@ -0,0 +1,89 @@
+package org.koitharu.kotatsu.core.network.cookies
+
+import android.content.Context
+import androidx.annotation.WorkerThread
+import androidx.collection.ArrayMap
+import androidx.core.content.edit
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import okhttp3.Cookie
+import okhttp3.HttpUrl
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
+
+private const val PREFS_NAME = "cookies"
+
+class PreferencesCookieJar(
+ context: Context,
+) : MutableCookieJar {
+
+ private val cache = ArrayMap()
+ private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ private var isLoaded = false
+
+ @WorkerThread
+ override fun loadForRequest(url: HttpUrl): List {
+ loadPersistent()
+ val expired = HashSet()
+ val result = ArrayList()
+ for ((key, cookie) in cache) {
+ if (cookie.isExpired()) {
+ expired += key
+ } else if (cookie.cookie.matches(url)) {
+ result += cookie.cookie
+ }
+ }
+ if (expired.isNotEmpty()) {
+ cache.removeAll(expired)
+ removePersistent(expired)
+ }
+ return result
+ }
+
+ @WorkerThread
+ override fun saveFromResponse(url: HttpUrl, cookies: List) {
+ val wrapped = cookies.map { CookieWrapper(it) }
+ prefs.edit(commit = true) {
+ for (cookie in wrapped) {
+ val key = cookie.key()
+ cache[key] = cookie
+ if (cookie.cookie.persistent) {
+ putString(key, cookie.encode())
+ }
+ }
+ }
+ }
+
+ override suspend fun clear(): Boolean {
+ cache.clear()
+ withContext(Dispatchers.IO) {
+ prefs.edit(commit = true) { clear() }
+ }
+ return true
+ }
+
+ @Synchronized
+ private fun loadPersistent() {
+ if (!isLoaded) {
+ val map = prefs.all
+ cache.ensureCapacity(map.size)
+ for ((k, v) in map) {
+ val cookie = try {
+ CookieWrapper(v as String)
+ } catch (e: Exception) {
+ e.printStackTraceDebug()
+ continue
+ }
+ cache[k] = cookie
+ }
+ isLoaded = true
+ }
+ }
+
+ private fun removePersistent(keys: Collection) {
+ prefs.edit(commit = true) {
+ for (key in keys) {
+ remove(key)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt
new file mode 100644
index 000000000..207886065
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkState.kt
@@ -0,0 +1,46 @@
+package org.koitharu.kotatsu.core.os
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkRequest
+import kotlinx.coroutines.flow.first
+import org.koitharu.kotatsu.utils.MediatorStateFlow
+import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
+
+class NetworkState(
+ private val connectivityManager: ConnectivityManager,
+) : MediatorStateFlow(connectivityManager.isNetworkAvailable) {
+
+ private val callback = NetworkCallbackImpl()
+
+ override fun onActive() {
+ invalidate()
+ val request = NetworkRequest.Builder().build()
+ connectivityManager.registerNetworkCallback(request, callback)
+ }
+
+ override fun onInactive() {
+ connectivityManager.unregisterNetworkCallback(callback)
+ }
+
+ suspend fun awaitForConnection() {
+ if (value) {
+ return
+ }
+ first { it }
+ }
+
+ private fun invalidate() {
+ publishValue(connectivityManager.isNetworkAvailable)
+ }
+
+ private inner class NetworkCallbackImpl : NetworkCallback() {
+
+ override fun onAvailable(network: Network) = invalidate()
+
+ override fun onLost(network: Network) = invalidate()
+
+ override fun onUnavailable() = invalidate()
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt
deleted file mode 100644
index 8450028e9..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/core/os/NetworkStateObserver.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.koitharu.kotatsu.core.os
-
-import android.content.Context
-import android.net.ConnectivityManager.NetworkCallback
-import android.net.Network
-import android.net.NetworkRequest
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.channels.ProducerScope
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.channels.onSuccess
-import kotlinx.coroutines.channels.trySendBlocking
-import kotlinx.coroutines.flow.FlowCollector
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.first
-import org.koitharu.kotatsu.utils.ext.connectivityManager
-import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class NetworkStateObserver @Inject constructor(
- @ApplicationContext context: Context,
-) : StateFlow {
-
- private val connectivityManager = context.connectivityManager
-
- override val replayCache: List
- get() = listOf(value)
-
- override val value: Boolean
- get() = connectivityManager.isNetworkAvailable
-
- override suspend fun collect(collector: FlowCollector): Nothing {
- collector.emit(value)
- while (true) {
- observeImpl().collect(collector)
- }
- }
-
- suspend fun awaitForConnection(): Unit {
- if (value) {
- return
- }
- first { it }
- }
-
- private fun observeImpl() = callbackFlow {
- val request = NetworkRequest.Builder().build()
- val callback = FlowNetworkCallback(this)
- connectivityManager.registerNetworkCallback(request, callback)
- awaitClose {
- connectivityManager.unregisterNetworkCallback(callback)
- }
- }
-
- private inner class FlowNetworkCallback(
- private val producerScope: ProducerScope,
- ) : NetworkCallback() {
-
- private var prevValue = value
-
- override fun onAvailable(network: Network) = update()
-
- override fun onLost(network: Network) = update()
-
- override fun onUnavailable() = update()
-
- private fun update() {
- val newValue = connectivityManager.isNetworkAvailable
- if (newValue != prevValue) {
- producerScope.trySendBlocking(newValue).onSuccess {
- prevValue = newValue
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt
index 1d4d9784f..4bf8e8b7b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/MangaLoaderContextImpl.kt
@@ -6,25 +6,25 @@ import android.util.Base64
import android.webkit.WebView
import androidx.core.os.LocaleListCompat
import dagger.hilt.android.qualifiers.ApplicationContext
-import java.util.*
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
-import org.koitharu.kotatsu.core.network.AndroidCookieJar
+import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.toList
+import java.util.*
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
@Singleton
class MangaLoaderContextImpl @Inject constructor(
override val httpClient: OkHttpClient,
- override val cookieJar: AndroidCookieJar,
+ override val cookieJar: MutableCookieJar,
@ApplicationContext private val androidContext: Context,
) : MangaLoaderContext() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt
index 4a3dd8ed5..fd38f1c04 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/zip/ZipOutput.kt
@@ -52,10 +52,13 @@ class ZipOutput(
return if (entryNames.add(entry.name)) {
val zipEntry = ZipEntry(entry.name)
output.putNextEntry(zipEntry)
- other.getInputStream(entry).use { input ->
- input.copyTo(output)
+ try {
+ other.getInputStream(entry).use { input ->
+ input.copyTo(output)
+ }
+ } finally {
+ output.closeEntry()
}
- output.closeEntry()
true
} else {
false
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
index 7a14f2403..91c534ea4 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
@@ -228,18 +228,18 @@ class DetailsActivity :
}
}
- private fun onHistoryChanged(info: HistoryInfo?) {
+ private fun onHistoryChanged(info: HistoryInfo) {
with(binding.buttonRead) {
- if (info?.history != null) {
+ if (info.history != null) {
setText(R.string._continue)
- setIconResource(R.drawable.ic_play)
+ setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
} else {
setText(R.string.read)
- setIconResource(R.drawable.ic_read)
+ setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
}
}
val text = when {
- info == null -> getString(R.string.loading_)
+ !info.isValid -> getString(R.string.loading_)
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
info.totalChapters == 0 -> getString(R.string.no_chapters)
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
index c3de6fa13..6d2203632 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
@@ -18,7 +18,6 @@ import coil.request.ImageRequest
import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
@@ -27,9 +26,9 @@ import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
-import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
+import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
@@ -45,8 +44,20 @@ import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSize
-import org.koitharu.kotatsu.utils.ext.*
+import org.koitharu.kotatsu.utils.ext.computeSize
+import org.koitharu.kotatsu.utils.ext.crossfade
+import org.koitharu.kotatsu.utils.ext.drawableTop
+import org.koitharu.kotatsu.utils.ext.enqueueWith
+import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty
+import org.koitharu.kotatsu.utils.ext.measureHeight
+import org.koitharu.kotatsu.utils.ext.referer
+import org.koitharu.kotatsu.utils.ext.resolveDp
+import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
+import org.koitharu.kotatsu.utils.ext.textAndVisible
+import org.koitharu.kotatsu.utils.ext.toFileOrNull
+import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
+import javax.inject.Inject
@AndroidEntryPoint
class DetailsFragment :
@@ -75,7 +86,7 @@ class DetailsFragment :
binding.chipsTags.onChipClickListener = this
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
- viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
+ viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
@@ -123,12 +134,14 @@ class DetailsFragment :
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_finished)
}
}
+
MangaState.ONGOING -> {
infoLayout.textViewState.apply {
textAndVisible = resources.getString(R.string.state_ongoing)
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_ongoing)
}
}
+
else -> infoLayout.textViewState.isVisible = false
}
if (manga.source == MangaSource.LOCAL) {
@@ -178,8 +191,8 @@ class DetailsFragment :
}
}
- private fun onHistoryChanged(history: MangaHistory?) {
- binding.progressView.setPercent(history?.percent ?: PROGRESS_NONE, animate = true)
+ private fun onHistoryChanged(history: HistoryInfo) {
+ binding.progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true)
}
private fun onLoadingStateChanged(isLoading: Boolean) {
@@ -229,6 +242,7 @@ class DetailsFragment :
),
)
}
+
R.id.textView_source -> {
startActivity(
MangaListActivity.newIntent(
@@ -237,6 +251,7 @@ class DetailsFragment :
),
)
}
+
R.id.imageView_cover -> {
startActivity(
ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }),
@@ -249,7 +264,7 @@ class DetailsFragment :
override fun onLongClick(v: View): Boolean {
when (v.id) {
R.id.button_read -> {
- if (viewModel.readingHistory.value == null) {
+ if (viewModel.historyInfo.value?.history == null) {
return false
}
val menu = PopupMenu(v.context, v)
@@ -271,12 +286,14 @@ class DetailsFragment :
)
true
}
+
else -> false
}
}
menu.show()
return true
}
+
else -> return false
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index 62f340539..b49b1adf8 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -13,10 +13,18 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
@@ -46,6 +54,7 @@ import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
+import java.io.IOException
class DetailsViewModel @AssistedInject constructor(
@Assisted intent: MangaIntent,
@@ -91,17 +100,18 @@ class DetailsViewModel @AssistedInject constructor(
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
-
- @Deprecated("")
- val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
- val historyInfo = combine(
+ val historyInfo: LiveData = combine(
delegate.manga,
history,
- ) { m, h ->
- HistoryInfo(m, h)
- }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
+ settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
+ ) { m, h, im ->
+ HistoryInfo(m, h, im)
+ }.asFlowLiveData(
+ context = viewModelScope.coroutineContext + Dispatchers.Default,
+ defaultValue = HistoryInfo(null, null, false),
+ )
val bookmarks = delegate.manga.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt
index 766ffb0e3..7b91abef5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersAdapter.kt
@@ -42,7 +42,8 @@ class ChaptersAdapter(
}
}
- override fun getSectionText(context: Context, position: Int): CharSequence {
- return items[position].chapter.number.toString()
+ override fun getSectionText(context: Context, position: Int): CharSequence? {
+ val item = items.getOrNull(position) ?: return null
+ return item.chapter.number.toString()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt
index a3fd7f9fa..ae314be62 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt
@@ -7,8 +7,12 @@ class HistoryInfo(
val totalChapters: Int,
val currentChapter: Int,
val history: MangaHistory?,
+ val isIncognitoMode: Boolean,
) {
+ val isValid: Boolean
+ get() = totalChapters >= 0
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -18,6 +22,7 @@ class HistoryInfo(
if (totalChapters != other.totalChapters) return false
if (currentChapter != other.currentChapter) return false
if (history != other.history) return false
+ if (isIncognitoMode != other.isIncognitoMode) return false
return true
}
@@ -26,20 +31,21 @@ class HistoryInfo(
var result = totalChapters
result = 31 * result + currentChapter
result = 31 * result + (history?.hashCode() ?: 0)
+ result = 31 * result + isIncognitoMode.hashCode()
return result
}
}
-@Suppress("FunctionName")
-fun HistoryInfo(manga: Manga?, history: MangaHistory?): HistoryInfo? {
- val chapters = manga?.chapters ?: return null
+fun HistoryInfo(manga: Manga?, history: MangaHistory?, isIncognitoMode: Boolean): HistoryInfo {
+ val chapters = manga?.chapters
return HistoryInfo(
- totalChapters = chapters.size,
- currentChapter = if (history != null) {
+ totalChapters = chapters?.size ?: -1,
+ currentChapter = if (history != null && !chapters.isNullOrEmpty()) {
chapters.indexOfFirst { it.id == history.chapterId }
} else {
-1
},
history = history,
+ isIncognitoMode = isIncognitoMode,
)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
index bcb83c411..98086832c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreFragment.kt
@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.explore.ui
import android.os.Bundle
import android.view.LayoutInflater
+import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
@@ -11,11 +13,12 @@ import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.util.RecyclerViewOwner
+import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity
import org.koitharu.kotatsu.databinding.FragmentExploreBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
@@ -31,6 +34,7 @@ import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import javax.inject.Inject
@AndroidEntryPoint
class ExploreFragment :
@@ -67,6 +71,7 @@ class ExploreFragment :
}
viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.onOpenManga.observe(viewLifecycleOwner, ::onOpenManga)
+ viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
}
override fun onDestroyView() {
@@ -95,6 +100,7 @@ class ExploreFragment :
viewModel.openRandom()
return
}
+
else -> return
}
startActivity(intent)
@@ -105,6 +111,14 @@ class ExploreFragment :
startActivity(intent)
}
+ override fun onItemLongClick(item: ExploreItem.Source, view: View): Boolean {
+ val menu = PopupMenu(view.context, view)
+ menu.inflate(R.menu.popup_source)
+ menu.setOnMenuItemClickListener(SourceMenuListener(item))
+ menu.show()
+ return true
+ }
+
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() = onManageClick(requireView())
@@ -124,6 +138,37 @@ class ExploreFragment :
startActivity(intent)
}
+ private fun onActionDone(action: ReversibleAction) {
+ val handle = action.handle
+ val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
+ val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length)
+ if (handle != null) {
+ snackbar.setAction(R.string.undo) { handle.reverseAsync() }
+ }
+ snackbar.anchorView = (activity as? BottomNavOwner)?.bottomNav
+ snackbar.show()
+ }
+
+ private inner class SourceMenuListener(
+ private val sourceItem: ExploreItem.Source,
+ ) : PopupMenu.OnMenuItemClickListener {
+
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.action_settings -> {
+ startActivity(SettingsActivity.newSourceSettingsIntent(requireContext(), sourceItem.source))
+ }
+
+ R.id.action_hide -> {
+ viewModel.hideSource(sourceItem.source)
+ }
+
+ else -> return false
+ }
+ return true
+ }
+ }
+
companion object {
fun newInstance() = ExploreFragment()
diff --git a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt
index 6d2657368..7783ebac0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/explore/ui/ExploreViewModel.kt
@@ -4,11 +4,17 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.*
+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
+import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.base.ui.BaseViewModel
+import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreItem
@@ -16,6 +22,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
+import javax.inject.Inject
@HiltViewModel
class ExploreViewModel @Inject constructor(
@@ -24,6 +31,7 @@ class ExploreViewModel @Inject constructor(
) : BaseViewModel() {
val onOpenManga = SingleLiveEvent()
+ val onActionDone = SingleLiveEvent()
val content: LiveData> = isLoading.asFlow().flatMapLatest { loading ->
if (loading) {
@@ -40,6 +48,16 @@ class ExploreViewModel @Inject constructor(
}
}
+ fun hideSource(source: MangaSource) {
+ launchJob(Dispatchers.Default) {
+ settings.hiddenSources += source.name
+ val rollback = ReversibleHandle {
+ settings.hiddenSources -= source.name
+ }
+ onActionDone.postCall(ReversibleAction(R.string.source_disabled, rollback))
+ }
+ }
+
private fun createContentFlow() = settings.observe()
.filter {
it == AppSettings.KEY_SOURCES_HIDDEN ||
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt
index b44f5db90..4f0805690 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListAdapter.kt
@@ -14,14 +14,14 @@ class HistoryListAdapter(
listener: MangaListListener
) : MangaListAdapter(coil, lifecycleOwner, listener), FastScroller.SectionIndexer {
- override fun getSectionText(context: Context, position: Int): CharSequence {
+ override fun getSectionText(context: Context, position: Int): CharSequence? {
val list = items
for (i in (0..position).reversed()) {
- val item = list[i]
+ val item = list.getOrNull(i) ?: continue
if (item is DateTimeAgo) {
return item.format(context.resources)
}
}
- return ""
+ return null
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt b/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt
index f8321b597..e78628f9d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/data/PagesCache.kt
@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable
+import org.koitharu.kotatsu.utils.ext.takeIfWriteable
import java.io.File
import java.io.InputStream
import javax.inject.Inject
@@ -18,9 +19,14 @@ import javax.inject.Singleton
@Singleton
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
- private val cacheDir = context.externalCacheDir ?: context.cacheDir
+ private val cacheDir = checkNotNull(findSuitableDir(context)) {
+ val dirs = (context.externalCacheDirs + context.cacheDir).joinToString(";") {
+ it.absolutePath
+ }
+ "Cannot find any suitable directory for PagesCache: [$dirs]"
+ }
private val lruCache = createDiskLruCacheSafe(
- dir = cacheDir.subdir(CacheDir.PAGES.dir),
+ dir = cacheDir,
size = FileSize.MEGABYTES.convert(200, FileSize.BYTES),
)
@@ -29,7 +35,7 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
}
suspend fun put(url: String, inputStream: InputStream): File = withContext(Dispatchers.IO) {
- val file = File(cacheDir, url.longHashCode().toString())
+ val file = File(cacheDir.parentFile, url.longHashCode().toString())
try {
file.outputStream().use { out ->
inputStream.copyToSuspending(out)
@@ -50,3 +56,10 @@ private fun createDiskLruCacheSafe(dir: File, size: Long): DiskLruCache {
DiskLruCache.create(dir, size)
}
}
+
+private fun findSuitableDir(context: Context): File? {
+ val dirs = context.externalCacheDirs + context.cacheDir
+ return dirs.firstNotNullOfOrNull {
+ it.subdir(CacheDir.PAGES.dir).takeIfWriteable()
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/DirMangaImporter.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/DirMangaImporter.kt
index d569af6c3..b117f1395 100644
--- a/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/DirMangaImporter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/importer/DirMangaImporter.kt
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.RATING_UNKNOWN
+import org.koitharu.kotatsu.utils.AlphanumComparator
import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.longOf
@@ -58,7 +59,7 @@ class DirMangaImporter(
private suspend fun addPages(output: CbzMangaOutput, root: DocumentFile, path: String, state: State) {
var number = 0
- for (file in root.listFiles()) {
+ for (file in root.listFiles().sortedWith(compareBy(AlphanumComparator()) { it.name.orEmpty() })) {
when {
file.isDirectory -> {
addPages(output, file, path + "/" + file.name, state)
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
index 7d35e99cf..423b88fbb 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
+import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
@@ -34,9 +35,12 @@ class MainViewModel @Inject constructor(
val onOpenReader = SingleLiveEvent()
- val isResumeEnabled = historyRepository
- .observeHasItems()
- .asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
+ val isResumeEnabled = combine(
+ historyRepository.observeHasItems(),
+ settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
+ ) { hasItems, incognito ->
+ hasItems && !incognito
+ }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
val isFeedAvailable = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default,
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
index feb64b21b..d059e1325 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt
@@ -313,7 +313,7 @@ class ReaderViewModel @AssistedInject constructor(
} ?: ReaderState(manga, preselectedBranch)
}
- val branch = chapters[currentState.value?.chapterId ?: 0L].branch
+ val branch = chapters[currentState.value?.chapterId ?: 0L]?.branch
mangaData.value = manga.filterChapters(branch)
readerMode.postValue(mode)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt
index 03380ff51..a5f032ff6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BasePageHolder.kt
@@ -5,7 +5,7 @@ import androidx.annotation.CallSuper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.LayoutPageInfoBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -14,12 +14,12 @@ abstract class BasePageHolder(
protected val binding: B,
loader: PageLoader,
settings: ReaderSettings,
- networkStateObserver: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
) : RecyclerView.ViewHolder(binding.root), PageHolderDelegate.Callback {
@Suppress("LeakingThis")
- protected val delegate = PageHolderDelegate(loader, settings, this, networkStateObserver, exceptionResolver)
+ protected val delegate = PageHolderDelegate(loader, settings, this, networkState, exceptionResolver)
protected val bindingInfo = LayoutPageInfoBinding.bind(binding.root)
val context: Context
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
index 1a914b41e..3144ef655 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/BaseReaderAdapter.kt
@@ -5,7 +5,7 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.utils.ext.resetTransformations
@@ -16,7 +16,7 @@ import kotlin.coroutines.suspendCoroutine
abstract class BaseReaderAdapter>(
private val loader: PageLoader,
private val readerSettings: ReaderSettings,
- private val networkState: NetworkStateObserver,
+ private val networkState: NetworkState,
private val exceptionResolver: ExceptionResolver,
) : RecyclerView.Adapter() {
@@ -70,7 +70,7 @@ abstract class BaseReaderAdapter>(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
): H
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt
index 27431e75b..2bb76f37f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt
@@ -17,10 +17,11 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
+import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.io.File
import java.io.IOException
@@ -28,7 +29,7 @@ class PageHolderDelegate(
private val loader: PageLoader,
private val readerSettings: ReaderSettings,
private val callback: Callback,
- private val networkState: NetworkStateObserver,
+ private val networkState: NetworkState,
private val exceptionResolver: ExceptionResolver,
) : DefaultOnImageEventListener, Observer {
@@ -138,6 +139,7 @@ class PageHolderDelegate(
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
+ e.printStackTraceDebug()
state = State.ERROR
error = e
callback.onError(e)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
index 8e686ae81..2348965a0 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPageHolder.kt
@@ -3,22 +3,24 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.graphics.PointF
import android.view.Gravity
import android.widget.FrameLayout
+import androidx.lifecycle.LifecycleOwner
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
class ReversedPageHolder(
+ owner: LifecycleOwner,
binding: ItemPageBinding,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
-) : PageHolder(binding, loader, settings, networkState, exceptionResolver) {
+) : PageHolder(owner, binding, loader, settings, networkState, exceptionResolver) {
init {
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt
index d68f39334..e81b1cfec 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedPagesAdapter.kt
@@ -2,17 +2,19 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class ReversedPagesAdapter(
+ private val lifecycleOwner: LifecycleOwner,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter(loader, settings, networkState, exceptionResolver) {
@@ -20,9 +22,10 @@ class ReversedPagesAdapter(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
) = ReversedPageHolder(
+ owner = lifecycleOwner,
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader,
settings = settings,
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt
index df4a739ac..d8f7b9b99 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/reversed/ReversedReaderFragment.kt
@@ -8,7 +8,7 @@ import android.view.ViewGroup
import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -26,7 +26,7 @@ import kotlin.math.absoluteValue
class ReversedReaderFragment : BaseReader() {
@Inject
- lateinit var networkStateObserver: NetworkStateObserver
+ lateinit var networkState: NetworkState
private var pagerAdapter: ReversedPagesAdapter? = null
@@ -39,10 +39,11 @@ class ReversedReaderFragment : BaseReader() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagerAdapter = ReversedPagesAdapter(
- viewModel.pageLoader,
- viewModel.readerSettings,
- networkStateObserver,
- exceptionResolver,
+ lifecycleOwner = viewLifecycleOwner,
+ loader = viewModel.pageLoader,
+ settings = viewModel.readerSettings,
+ networkState = networkState,
+ exceptionResolver = exceptionResolver,
)
with(binding.pager) {
adapter = pagerAdapter
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
index b56163f54..efa8e4dcd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PageHolder.kt
@@ -5,12 +5,13 @@ import android.graphics.PointF
import android.net.Uri
import android.view.View
import androidx.core.view.isVisible
+import androidx.lifecycle.LifecycleOwner
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -19,15 +20,17 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.*
open class PageHolder(
+ owner: LifecycleOwner,
binding: ItemPageBinding,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
) : BasePageHolder(binding, loader, settings, networkState, exceptionResolver),
View.OnClickListener {
init {
+ binding.ssiv.bindToLifecycle(owner)
binding.ssiv.isEagerLoadingEnabled = !isLowRamDevice(context)
binding.ssiv.addOnImageEventListener(delegate)
@Suppress("LeakingThis")
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
index 889f5189f..af20be177 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagerReaderFragment.kt
@@ -8,7 +8,7 @@ import android.view.ViewGroup
import androidx.core.view.children
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.FragmentReaderStandardBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -25,7 +25,7 @@ import kotlin.math.absoluteValue
class PagerReaderFragment : BaseReader() {
@Inject
- lateinit var networkStateObserver: NetworkStateObserver
+ lateinit var networkState: NetworkState
private var pagesAdapter: PagesAdapter? = null
@@ -38,10 +38,11 @@ class PagerReaderFragment : BaseReader() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagesAdapter = PagesAdapter(
- viewModel.pageLoader,
- viewModel.readerSettings,
- networkStateObserver,
- exceptionResolver,
+ lifecycleOwner = viewLifecycleOwner,
+ loader = viewModel.pageLoader,
+ settings = viewModel.readerSettings,
+ networkState = networkState,
+ exceptionResolver = exceptionResolver,
)
with(binding.pager) {
adapter = pagesAdapter
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt
index 293ca6273..0c562ae4d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/standard/PagesAdapter.kt
@@ -2,27 +2,30 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class PagesAdapter(
+ private val lifecycleOwner: LifecycleOwner,
loader: PageLoader,
settings: ReaderSettings,
- networkStateObserver: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
-) : BaseReaderAdapter(loader, settings, networkStateObserver, exceptionResolver) {
+) : BaseReaderAdapter(loader, settings, networkState, exceptionResolver) {
override fun onCreateViewHolder(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
) = PageHolder(
+ owner = lifecycleOwner,
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
loader = loader,
settings = settings,
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt
index 6d92ff321..607f33ad6 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonAdapter.kt
@@ -2,17 +2,19 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class WebtoonAdapter(
+ private val lifecycleOwner: LifecycleOwner,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
) : BaseReaderAdapter(loader, settings, networkState, exceptionResolver) {
@@ -20,9 +22,10 @@ class WebtoonAdapter(
parent: ViewGroup,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
) = WebtoonHolder(
+ owner = lifecycleOwner,
binding = ItemPageWebtoonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt
index 086081097..c8cfa3714 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonHolder.kt
@@ -3,12 +3,13 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.net.Uri
import android.view.View
import androidx.core.view.isVisible
+import androidx.lifecycle.LifecycleOwner
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
@@ -22,10 +23,11 @@ import org.koitharu.kotatsu.utils.ext.setProgressCompat
import org.koitharu.kotatsu.utils.ext.showCompat
class WebtoonHolder(
+ owner: LifecycleOwner,
binding: ItemPageWebtoonBinding,
loader: PageLoader,
settings: ReaderSettings,
- networkState: NetworkStateObserver,
+ networkState: NetworkState,
exceptionResolver: ExceptionResolver,
) : BasePageHolder(binding, loader, settings, networkState, exceptionResolver),
View.OnClickListener {
@@ -34,6 +36,7 @@ class WebtoonHolder(
private val goneOnInvisibleListener = GoneOnInvisibleListener(bindingInfo.progressBar)
init {
+ binding.ssiv.bindToLifecycle(owner)
binding.ssiv.regionDecoderFactory = SkiaPooledImageRegionDecoder.Factory()
binding.ssiv.addOnImageEventListener(delegate)
bindingInfo.buttonRetry.setOnClickListener(this)
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
index 7fe5fb1f4..072e42169 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/webtoon/WebtoonReaderFragment.kt
@@ -7,7 +7,7 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.async
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.databinding.FragmentReaderWebtoonBinding
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.pager.BaseReader
@@ -22,7 +22,7 @@ import javax.inject.Inject
class WebtoonReaderFragment : BaseReader() {
@Inject
- lateinit var networkStateObserver: NetworkStateObserver
+ lateinit var networkState: NetworkState
private val scrollInterpolator = AccelerateDecelerateInterpolator()
private var webtoonAdapter: WebtoonAdapter? = null
@@ -35,10 +35,11 @@ class WebtoonReaderFragment : BaseReader() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
webtoonAdapter = WebtoonAdapter(
- viewModel.pageLoader,
- viewModel.readerSettings,
- networkStateObserver,
- exceptionResolver,
+ lifecycleOwner = viewLifecycleOwner,
+ loader = viewModel.pageLoader,
+ settings = viewModel.readerSettings,
+ networkState = networkState,
+ exceptionResolver = exceptionResolver,
)
with(binding.recyclerView) {
setHasFixedSize(true)
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
index fe7d50cdb..b8f4ee56e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt
@@ -12,7 +12,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
-import org.koitharu.kotatsu.core.network.AndroidCookieJar
+import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.CacheDir
@@ -45,7 +45,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
lateinit var malRepository: MALRepository
@Inject
- lateinit var cookieJar: AndroidCookieJar
+ lateinit var cookieJar: MutableCookieJar
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
index 4542b1fca..91561b2cd 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt
@@ -86,7 +86,6 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
}.onSuccess { username ->
preference.title = getString(R.string.logged_in_as, username)
}.onFailure { error ->
- preference.isEnabled = error is AuthRequiredException
when {
error is AuthRequiredException -> Unit
ExceptionResolver.canResolve(error) -> {
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
index a56d848ea..78cb8050a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.model.FavouriteCategory
-import org.koitharu.kotatsu.core.os.NetworkStateObserver
+import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow
@@ -46,14 +46,14 @@ class ShelfViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
- networkStateObserver: NetworkStateObserver,
+ networkState: NetworkState,
) : BaseViewModel(), ListExtraProvider {
val onActionDone = SingleLiveEvent()
val content: LiveData> = combine(
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
- networkStateObserver,
+ networkState,
repository.observeShelfContent(),
) { sections, isConnected, content ->
mapList(content, sections, isConnected)
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt
index 691db99ac..1a19dbb17 100644
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt
@@ -46,9 +46,9 @@ class ShelfAdapter(
.addDelegate(errorStateListAD(listener))
}
- override fun getSectionText(context: Context, position: Int): CharSequence {
- val item = items.getOrNull(position) as? ShelfSectionModel
- return item?.getTitle(context.resources) ?: ""
+ override fun getSectionText(context: Context, position: Int): CharSequence? {
+ val item = items.getOrNull(position) as? ShelfSectionModel ?: return null
+ return item.getTitle(context.resources)
}
private class DiffCallback : DiffUtil.ItemCallback() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt
index 9dff00923..c0c00aeab 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt
@@ -5,16 +5,17 @@ import android.accounts.AccountManager
import android.content.ContentResolver
import android.content.Context
import android.os.Bundle
-import android.util.ArrayMap
+import androidx.collection.ArrayMap
import androidx.room.InvalidationTracker
import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-import javax.inject.Singleton
-import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
@@ -22,6 +23,9 @@ import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Singleton
@Singleton
class SyncController @Inject constructor(
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt b/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt
index 5f4f12fda..859e7e391 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/CompositeMutex.kt
@@ -1,14 +1,14 @@
package org.koitharu.kotatsu.utils
-import android.util.ArrayMap
-import java.util.*
-import kotlin.coroutines.coroutineContext
-import kotlin.coroutines.resume
+import androidx.collection.ArrayMap
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.isActive
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import java.util.LinkedList
+import kotlin.coroutines.coroutineContext
+import kotlin.coroutines.resume
class CompositeMutex : Set {
@@ -27,7 +27,7 @@ class CompositeMutex : Set {
}
override fun isEmpty(): Boolean {
- return data.isEmpty()
+ return data.isEmpty
}
override fun iterator(): Iterator {
@@ -59,7 +59,7 @@ class CompositeMutex : Set {
private suspend fun waitForRemoval(element: T) {
val list = data[element] ?: return
- suspendCancellableCoroutine { continuation ->
+ suspendCancellableCoroutine { continuation ->
list.add(continuation)
continuation.invokeOnCancellation {
list.remove(continuation)
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt b/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt
new file mode 100644
index 000000000..01c637b38
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/MediatorStateFlow.kt
@@ -0,0 +1,39 @@
+package org.koitharu.kotatsu.utils
+
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import java.util.concurrent.atomic.AtomicInteger
+
+abstract class MediatorStateFlow(initialValue: T) : StateFlow {
+
+ private val delegate = MutableStateFlow(initialValue)
+ private val collectors = AtomicInteger(0)
+
+ final override val replayCache: List
+ get() = delegate.replayCache
+
+ final override val value: T
+ get() = delegate.value
+
+ final override suspend fun collect(collector: FlowCollector): Nothing {
+ try {
+ if (collectors.getAndIncrement() == 0) {
+ onActive()
+ }
+ delegate.collect(collector)
+ } finally {
+ if (collectors.decrementAndGet() == 0) {
+ onInactive()
+ }
+ }
+ }
+
+ protected fun publishValue(v: T) {
+ delegate.value = v
+ }
+
+ abstract fun onActive()
+
+ abstract fun onInactive()
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt b/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt
index e95e0fb96..ddb42ab45 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/VoiceInputContract.kt
@@ -5,12 +5,16 @@ import android.content.Context
import android.content.Intent
import android.speech.RecognizerIntent
import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.os.ConfigurationCompat
+import java.util.Locale
class VoiceInputContract : ActivityResultContract() {
override fun createIntent(context: Context, input: String?): Intent {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
+ val locale = ConfigurationCompat.getLocales(context.resources.configuration).get(0) ?: Locale.getDefault()
+ intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, locale.toLanguageTag())
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, input)
return intent
}
@@ -23,4 +27,4 @@ class VoiceInputContract : ActivityResultContract() {
null
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt
index cab41519f..c40f4e01c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt
@@ -23,6 +23,8 @@ fun File.subdir(name: String) = File(this, name).also {
fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
+fun File.takeIfWriteable() = takeIf { it.exists() && it.canWrite() }
+
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {
it.readText()
}
@@ -74,4 +76,4 @@ private fun computeSizeInternal(file: File): Long {
} else {
return file.length()
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/menu/popup_source.xml b/app/src/main/res/menu/popup_source.xml
new file mode 100644
index 000000000..60e497a39
--- /dev/null
+++ b/app/src/main/res/menu/popup_source.xml
@@ -0,0 +1,13 @@
+
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 0354d02f7..060dc6671 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -70,7 +70,7 @@
Liste
Kapitel
Einzelheiten
- Keine Verbindung zum Internet möglich
+ Netzwerkfehler
Es ist ein Fehler aufgetreten
Verlauf
Favoriten
@@ -372,4 +372,23 @@
Import abgeschlossen
Du kannst die Originaldatei aus dem Speicher löschen, um Platz zu sparen
Import wird bald beginnen
+ Serverseitiger Fehler (%1$d). Bitte versuchen Sie es später noch einmal
+ Kompakt
+ Kontrast
+ Schalten Sie Wi-Fi oder ein mobiles Netzwerk ein, um Manga online zu lesen
+ Auch klare Informationen über neue Kapitel
+ Ungespeicherte Änderungen speichern oder verwerfen\?
+ Verwerfen
+ Zurücksetzen
+ Helligkeit
+ Die gewählten Farbeinstellungen werden für diesen Manga in Erinnerung bleiben
+ Farbkorrektur
+ Kein Platz mehr auf dem Gerät
+ Verschiedene Sprachen
+ Netzwerk ist nicht verfügbar
+ Vergrößerungs-/Verkleinerungsgesten im Webtoon-Modus zulassen (beta)
+ Ergonomische Leserkontrolle
+ Tippe auf den rechten Rand oder drücke die rechte Taste, um immer zur nächsten Seite zu wechseln
+ Seitenwechsel-Schieberegler anzeigen
+ Quelle deaktiviert
\ No newline at end of file
diff --git a/app/src/main/res/values-el/plurals.xml b/app/src/main/res/values-el/plurals.xml
new file mode 100644
index 000000000..497f12234
--- /dev/null
+++ b/app/src/main/res/values-el/plurals.xml
@@ -0,0 +1,35 @@
+
+
+
+ - %1$d μέρα πριν
+ - %1$d μέρες πριν
+
+
+ - %1$dστοιχείο
+ - %1$dστοιχεία
+
+
+ - %1$dνέο κεφάλαιο
+ - %1$dνέα κεφάλαια
+
+
+ - %1$dκεφάλαιο%2$d
+ - %1$dκεφάλαια%2$d
+
+
+ - %1$dώρα πριν
+ - %1$dώρες πριν
+
+
+ - %1$dκεφάλαιο
+ - %1$dκεφάλαια
+
+
+ - Σύνολο%1$dσελίδα
+ - Σύνολο%1$dσελίδες
+
+
+ - %1$dλεπτό πριν
+ - %1$d λεπτά πριν
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
new file mode 100644
index 000000000..43914a6a4
--- /dev/null
+++ b/app/src/main/res/values-el/strings.xml
@@ -0,0 +1,81 @@
+
+
+ Εσωτερικός χώρος
+ Κλείσιμο μενού
+ Άνοιγμα μενού
+ Αγαπημένα
+ Ιστορικό
+ Προέκυψε σφάλμα
+ Επανάληψη
+ Πλέγμα
+ Εμφάνιση ως λίστα
+ Ρυθμίσεις
+ Απομακρυσμένες πηγές
+ Επεξεργασία…
+ Κλείσιμο
+ Εκκαθάριση ιστορικού
+ Δεν βρέθηκε τίποτα
+ Κενό ιστορικό
+ Διάβασε
+ Προσθήκη στα αγαπημένα
+ Νέα κατηγορία
+ Αποθήκευση
+ Κοινοποιήση
+ Δημιουργία συντόμευσης…
+ Κοινοποίηση %s
+ Αναζήτηση
+ Αναζήτηση μάνγκα
+ Λήψη…
+ Κατεβασμένο
+ Λήψεις
+ Ενημερωμένο
+ Νεότερο
+ Βαθμολογία
+ Φίλτρο
+ Σκοτεινό
+ Όπως στο σύστημα
+ Εκκαθάριση
+ Να διαγράψετε μόνιμα όλο το ιστορικό ανάγνωσης;
+ Διαγραφή
+ Αποθήκευση σελίδας
+ Αποθηκευμένα
+ Κοινή χρήση εικόνας
+ Εισαγωγή
+ Διαγραφή
+ Επιλέξτε ένα αρχείο ZIP ή CBZ.
+ Χωρίς περιγραφή
+ Ιστορικό και μνήμη cache
+ Εκκαθάριση μνήμης cache της σελίδας
+ Προσωρινή Μνήμη
+ B|kB|MB|GB|TB
+ Τυπικό
+ Μάνχγουα
+ Αναζήτηση στο %s
+ Διαγραφή μάνγκα
+ Μόνιμη διαγραφή του \"%s\" από τη συσκευή;
+ Ρυθμίσεις λειτουργίας ανάγνωσης
+ Αλλαγή σελίδων
+ Αδυναμία σύνδεσης στο ίντερνετ
+ Κεφάλαια
+ Πληροφορίες
+ Λίστα
+ Λεπτομερής λίστα
+ Φόρτωση…
+ Κεφάλαιο%1$d από %2$d
+ Δεν υπάρχουν αγαπημένα
+ Προσθήκη
+ Εισαγωγή ονόματος κατηγορίας
+ Επεξεργασία…
+ Όνομα
+ Δημοφιλή
+ Τρόπος Ταξινόμησης
+ Το \"%s\" αφαιρέθηκε από το ιστορικό
+ Θέμα
+ Φωτεινό
+ Σελίδες
+ Περιμένετε να ολοκληρωθεί η φόρτωση…
+ Το \"%s\" διαγράφηκε από τον τοπικό χώρο αποθήκευσης
+ Αυτή η λειτουργία δεν υποστηρίζεται
+ Λειτουργία ανάγνωσης
+ Μέγεθος πλέγματος
+
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 150053d37..40f11e537 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -6,7 +6,7 @@
Favoritos
Historial
Ocurrió un error
- No se pudo conectar a Internet
+ Error en la red
Detalles
Capítulos
Lista
@@ -266,7 +266,7 @@
Excluir géneros
Especifica los géneros que no quieres ver en las sugerencias
Remoción completada
- ¿Estás seguro de que quieres descargar todos los manga seleccionados con todos sus capítulos\? Esta acción puede consumir mucho tráfico y almacenamiento
+ ¿Descargar todos los mangas seleccionados y sus capítulos\? Esto puede consumir mucho tráfico y almacenamiento.
¿Eliminar elementos seleccionados del dispositivo de forma permanente\?
Ocultar
Ralentización de la descarga
@@ -375,7 +375,7 @@
Detalles del error:<br><tt>%1$s</tt><br><br>1. Intenta <a href=%2$s>abrir el manga en un navegador web</a> para asegurarte de que está disponible en tu fuente<br>2. Si está disponible, envía un informe de error a los desarrolladores.
Hacer que los mangas recientes estén disponibles mediante una pulsación larga en el icono de la aplicación
Los ajustes de color elegidos serán recordados para este manga
- Feed
+ Fuente
Descargando manga
Mostrar los accesos directos a los mangas recientes
Tocando el borde derecho o pulsando la tecla derecha se pasa siempre a la página siguiente
@@ -384,10 +384,17 @@
Brillo
Contraste
Restablecer
- Tienes cambios sin guardar. ¿Quieres guardarlos o descartarlos\?
+ ¿Guardar o descartar los cambios no guardados\?
Descartar
Sin espacio en dispositivo
Zoom de webtoon
Permitir el gesto de acercamiento/alejamiento en modo webtoon (beta)
Mostrar el deslizador de cambio de página
+ Error del servidor (%1$d). Vuelva a intentarlo más tarde
+ Información clara sobre los nuevos capítulos
+ Diferentes idiomas
+ La red no está disponible
+ Compacta
+ Enciende la Wi-Fi o la red móvil para leer los mangas en línea
+ Fuente desactivada
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index cd3975284..939a3fb19 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -202,7 +202,7 @@
Liste
Chapitres
Détails
- Impossible de se connecter à Internet
+ Erreur réseau
Une erreur s\'est produite
Historique
Favoris
@@ -395,4 +395,5 @@
Compact
Erreur côté serveur (%1$d). Veuillez réessayer plus tard
Effacer aussi les informations sur les nouveaux chapitres
+ Source désactivée
\ No newline at end of file
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 0f98d4f67..4e8b326f2 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -102,15 +102,15 @@
Suara pemberitahuan
Kategori…
Ubah Nama
- "Hapus kategori \"%s\" dari favorit Anda\?
-\nSemua manga disana akan hilang."
+ Hapus kategori \"%s\" dari favorit Anda\?
+\nSemua manga di situ akan hilang.
Sepi juga di sini…
Anda bisa menggunakan kategori untuk mengelola favorit Anda. Tekan «+» untuk membuat kategori
Apa yang Anda baca akan ditampilkan di sini
Cari apa untuk di baca di bilah samping.
Simpan sesuatu dulu
Rak
- Terbaru
+ Baru-baru ini
Animasi halaman
Folder untuk unduhan
Tidak tersedia
@@ -298,7 +298,7 @@
Selesai
Dibatalkan
Sinkronisasi data Anda
- Masukkan email Anda untuk melanjutkan
+ Masukkan surel Anda untuk melanjutkan
Pelacakan
Keluar
Sinkronisasi
@@ -329,7 +329,7 @@
Tekan Kembali dua kali untuk keluar dari aplikasi
Konfirmasi keluar
Tembolok halaman
- Cache lainnya
+ Tembolok lainnya
Penggunaan penyimpanan
Tersedia
Mode Incognito
@@ -356,14 +356,14 @@
Ada sesuatu yang salah. Mohon untuk mengirim laporan kutu (bug) ke pengembang untuk membantu kami memperbaikinya.
Lapor
Manga yang ditandai sebagai NSFW tidak akan ditambahkan ke riwayat dan progres Anda tidak akan disimpan
- Bisa membantu dalam beberapa masalah. Seluruh otorisasi akan menjadi tidak valid.
+ Bisa membantu dalam beberapa masalah. Seluruh otorisasi akan menjadi tidak valid
Kelola
Aktifkan sumber manga untuk membaca manga daring
Apakah Anda yakin ingin menghapus kategori favorit yang dipilih\?
\n Semua manga di sana akan hilang dan ini tidak bisa diurungkan.
Atur Ulang
Manga tersimpan
- Tekan lagi untuk keluar
+ Tekan Kembali lagi untuk keluar
Tidak ada bab
Tampilkan pintasan manga baru-baru ini
Buat manga baru-baru ini tersedia dengan menekan panjang pada ikon aplikasi
@@ -372,4 +372,5 @@
Hanya gestur
DNS melalui HTTPS
Istirahat
+ Mati
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index cfcbf91fb..eaeb7ef74 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -320,4 +320,6 @@
Dominio non valido
Seleziona l\'intervallo
Contenuto non trovato o rimosso
+ Compatto
+ Fonte disabilitata
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 1117570fe..28773c9a2 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -63,7 +63,7 @@
ダークテーマ
ページ
テーマ
- インターネットに接続出来ませんでした
+ ネットワークエラー
カテゴリー名を入力してください
アップデート
キャッシュ
@@ -395,4 +395,5 @@
Wi-Fiまたはモバイルネットワークをオンにして、オンラインでマンガを読むことができます
Webtoonズーム
ウェブトゥーンモードでズームイン/ズームアウトのジェスチャーを可能にする(ベータ版)
+ コンパクト
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/plurals.xml b/app/src/main/res/values-ko/plurals.xml
new file mode 100644
index 000000000..a6b3daec9
--- /dev/null
+++ b/app/src/main/res/values-ko/plurals.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
new file mode 100644
index 000000000..69701e235
--- /dev/null
+++ b/app/src/main/res/values-ko/strings.xml
@@ -0,0 +1,108 @@
+
+
+ 정렬 기준
+ 불러오기
+ 네트워크 오류
+ 목록
+ 저장
+ 공유하기
+ %s 공유
+ 검색하기
+ 경고
+ 내부 저장소
+ 외부 저장소
+ 도메인
+ 새 버전이 존재합니다
+ 웹 브라우저에서 열기
+ 저장
+ 알림
+ 처음부터 읽기
+ LED 표시
+ 진동
+ 이름 바꾸기
+ 즐겨찾기에서 \"%s\" 카테고리를 제거하시겠습니까\?
+\n포함된 모든 만화가 지워집니다.
+ 지우기
+ 쿼리를 재구성하십시오.
+ 사이드 메뉴에서 만화를 탐색해보세요.
+ 만화가 여기에 표시됩니다
+ «탐색» 섹션에서 만화를 탐색해보세요
+ 페이지 전환 효과
+ 사용 가능한 저장소 없음
+ 완료
+ 빈 카테고리
+ 업데이트
+ 새 버전: %s
+ 네트워크 연결을 기다리는 중…
+ 업데이트 피드 지우기
+ 메뉴 닫기
+ 메뉴 열기
+ 내장 메모리
+ 즐겨찾기
+ 지우기
+ 설정
+ 불러오는 중…
+ 닫기
+ 다시 시도
+ 즐겨찾기가 비어있음
+ 필터링
+ 밝게
+ 어둡게
+ 페이지
+ 지금 읽기
+ 이름 순
+ 인기 순
+ %2$d화 중 %1$d화
+ 다운로드
+ 평점 순
+ 페이지 저장
+ 저장됨
+ 이미지 공유하기
+ ZIP 혹은 CBZ 파일을 선택하세요.
+ 기록 및 캐시
+ 캐시
+ 만화 제거
+ 볼륨 키
+ 결과 없음
+ 즐겨찾기 추가
+ 다운로드 완료
+ 새 카테고리
+ 만화를 검색하세요
+ 다운로드 중…
+ 처리중…
+ 최근 업데이트 순
+ 최근 발간 순
+ 시스템 설정
+ 지우기
+ 잠시만 기다려주세요…
+ 바이트|kB|MB|GB|TB
+ 페이지 캐시 지우기
+ 읽기 모드
+ 격자 크기
+ %s에서 검색
+ 장치에서 \"%s\"를 영구적으로 삭제하시겠습니까\?
+ 페이지 전환
+ 가장자리 탭
+ 웹툰
+ 검색 기록 지우기
+ 읽기 모드
+ 이 동작은 많은 데이터 사용을
+ 썸네일 캐시 지우기
+ 다시 묻지 않음
+ 취소 중…
+ 오류
+ 업데이트 확인
+ 업데이트 가능 시 알림 설정
+ 이 만화에는 %s가 있습니다. 모두 저장하시겠습니까\?
+ 즐겨찾기 카테고리
+ 다운로드
+ 알림 설정
+ 알림음
+ 카테고리…
+ 읽은 내용이 여기에 표시됩니다
+ 사용할 수 없음
+ 모든 즐겨찾기
+ 나중에 읽기
+ 검색 결과
+ 크기: %s
+
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/plurals.xml b/app/src/main/res/values-pl/plurals.xml
new file mode 100644
index 000000000..3682362dc
--- /dev/null
+++ b/app/src/main/res/values-pl/plurals.xml
@@ -0,0 +1,43 @@
+
+
+
+ - %1$d nowy rozdział
+ - %1$d nowe rozdziały
+ - %1$d nowych rozdziałów
+
+
+ - %1$d minutę temu
+ - %1$d minuty temu
+ - %1$d minut temu
+
+
+ - Łącznie %1$d strona
+ - Łącznie %1$d strony
+ - Łącznie %1$d stron
+
+
+ - %1$d godzinę temu
+ - %1$d godziny temu
+ - %1$d godzin temu
+
+
+ - %1$d dzień temu
+ - %1$d dni temu
+ - %1$d dni temu
+
+
+ - %1$d przedmiot
+ - %1$d przedmioty
+ - %1$d przedmiotów
+
+
+ - %1$d rozdział z %2$d
+ - %1$d rozdziały z %2$d
+ - %1$d rozdziałów z %2$d
+
+
+ - %1$d rozdział
+ - %1$d rozdziały
+ - %1$d rozdziałów
+
+
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
new file mode 100644
index 000000000..8eaf3aab0
--- /dev/null
+++ b/app/src/main/res/values-pl/strings.xml
@@ -0,0 +1,397 @@
+
+
+ Ulubione
+ Historia
+ Napotkano błąd
+ Szczegółowy
+ Rozdziały
+ Lista
+ Lista szczegółowa
+ Siatka
+ Tryb listy
+ Ustawienia
+ Ładowanie…
+ Rozdział %1$d z %2$d
+ Zamknij
+ Wyczyść historię
+ Dodaj
+ Zapisz
+ Udostępnij
+ Szukaj
+ Szukaj mang
+ Pobieranie…
+ Pobrano
+ Pobrane
+ Nazwa
+ Popularność
+ Najnowsze
+ Ocena
+ Filtry
+ Jasny
+ Ciemny
+ Strony
+ Wyczyść
+ Usuń
+ Udostępnij zdjęcie
+ Usuń
+ Brak opisu
+ Tryb czytania
+ Błąd sieci
+ Obliczanie…
+ Spróbuj ponownie
+ Nic nie znaleziono
+ Brak historii
+ Czytaj
+ Brak ulubionych
+ Dodaj do ulubionych
+ Nowa kategoria
+ Stwórz skrót
+ Udostępnij %s
+ Przetwarzanie…
+ Zaktualizowane
+ „%s” usunięte z historii
+ Zapisz stronę
+ Zapisano
+ Wibracje
+ Biblioteka
+ Ostatnie
+ Tryb czarny
+ Przygotowywanie…
+ Plik nieznaleziony
+ Wczoraj
+ Dawno temu
+ Grupa
+ Dzisiaj
+ Zaloguj
+ Dalej
+ Potwierdź
+ Witaj
+ Skończone
+ W trakcie
+ Zezwól
+ Proponowane
+ Włącz propozycje
+ Włączone
+ Wyłączone
+ Nigdy
+ Zawsze
+ Znajdź rozdział
+ %1$s%%
+ Wygląd
+ Schowaj
+ Synchronizacja
+ Synchronizuj swoje dane
+ Nazwa
+ Edytuj
+ Wyloguj
+ Cofnij
+ Wyślij
+ Planowane
+ Czytane
+ Czytane ponownie
+ Skończone
+ Pokaż wszystkie
+ Wybierz zakres
+ Wyczyść całą historię
+ Ostatnie 2 godziny
+ Historia wyczyszczona
+ Zarządzaj
+ Losowe
+ Puste
+ Lista zmian
+ Przeglądaj
+ Dostępne
+ Ustawienia
+ Źródło wyłączone
+ Kompaktowy
+ Błąd po stronie serwera (%1$d). Sprónuj ponownie później
+ Sieć niedostępna
+ Inne języki
+ Odrzuć
+ Jasność
+ Kontrast
+ Korekcja kolorów
+ %ss
+ Wyłącz
+ Automatyczne przewijanie
+ Brak rozdziałów
+ Tryb incognito
+ Pobieranie mangi
+ Usunięto z ulubionych
+ Wprowadź swój email aby kontynuować
+ Wykorzystana pamięć
+ Zapisane mangi
+ Brak zakładek
+ Możesz tworzyć zakładki w trakcie czytania mangi
+ Zakładki usunięte
+ Twoje ostatnio czytane mangi
+ Wyłącz wszystkie
+ Wyłącz optymalizację baterii
+ Autowykrywanie trybu czytania
+ Usunięte z historii
+ Dodano zakładkę
+ Usunięto zakładkę
+ Zakładki
+ Usuń zakładkę
+ Dodaj zakładkę
+ Brak ulubionych kategorii
+ Edytuj kategorię
+ Włącz powiadomienia
+ Wróć
+ Konto już istnieje
+ Anulowano
+ Zwolnienie pobierania
+ Brak rozdziałów w tej mandze
+ Różne języki
+ Tylko na Wi-Fi
+ Zawsze blokuj
+ Format daty
+ Gatunki
+ Znajdź gatunek
+ Czytaj więcej
+ Inne
+ Rozwiąż
+ Wymagane CAPTCHA
+ Cichy
+ Dotknij aby spróbować ponownie
+ Teraz
+ Przywrócone
+ Dopasuj do szerokości
+ Dopasuj do wysokości
+ Dopasuj do środka
+ Nowa kategoria
+ Brak nowych aktualizacji
+ Sprawdź dostępność aktualizacji
+ Sprawdzanie aktualizacji…
+ Wersja %s
+ O aplikacji
+ Kategorie…
+ Zmień nazwę
+ Usuń
+ Jest tu dosyć pusto…
+ Ulubione kategorie
+ Powiadomienie LED
+ Nowe rozdziały
+ Zamknij kartę
+ Otwórz kartę
+ Pamięć wewnętrzna
+ Tutaj będą wyświetlane Twoje mangi
+ Znajdź materiały do czytania w zakładce „Przeglądaj”
+ W tym miejscu pojawią się powiadomienia o nowych rozdziałach z mang które czytasz
+ Strony w pamięci podręcznej
+ Animacja przewracania strony
+ Inne rzeczy w pamięci podręcznej
+ Otwórz w przeglądarce
+ Numerowane strony
+ Powiadomienia
+ Dźwięk powiadomień
+ Ustawienia powiadomień
+ Zewnętrzne źródła
+ Motyw
+ Systemowy
+ Historia i pamięć podręczna
+ Wyczyść pamięć podręczną stron
+ Pamięć podręczna
+ B|kB|MB|GB|TB
+ Wielkość siatki
+ Szukaj na %s
+ Usuń mangę
+ Dalej
+ Nie pytaj ponownie
+ Anulowanie…
+ Błąd
+ Wyczyszczone
+ Pamięć wewnętrzna
+ Pamięć zewnętrzna
+ Domena
+ Sprawdź dostępność nowej wersji aplikacji
+ Nowa wersja aplikacji jest dostępna
+ Pokaż powiadomienie gdy nowa wersja jest dostępna
+ Ta manga ma %s. Zapisać wszystko?
+ Zapisz
+ Pobierz
+ Czytaj od początku
+ Usunąć kategorię „%s” z Twoich ulubionych? Wszystkie mangi w niej będą z niej usunięte.
+ Możesz użyć kategorii do organizowania swoich ulubionych. Kliknij «+» aby stworzyć kategorię
+ Najpierw coś zapisz
+ Niedostępne
+ Zapisz
+ Wszystkie ulubione
+ Pusta kategoria
+ Czytaj później
+ Aktualizacje
+ Nowa wersja: %s
+ Wielkość: %s
+ Czekanie na sieć…
+ Obróć ekran
+ Odśwież
+ Szukaj aktualizacji
+ Nie sprawdzaj
+ Wprowadź hasło
+ Złe hasło
+ Chroń aplikację
+ Pytaj o hasło przy starcie Kotatsu
+ Wprowadź ponownie hasło
+ Zużywa mniej prądu na ekranach AMOLED
+ Kopia zapasowa i przywracanie
+ Utwórz kopię zapasową danych
+ Przywróć z kopii zapasowej
+ 18+
+ %1$d na %2$d włączone
+ Wprowadź nazwę kategorii
+ Standardowy
+ Webtoon
+ Ustawienia czytnika
+ Zmiana strony
+ Przyciski głośności
+ Uwaga
+ Dotknięcie krawędzi
+ Wyczyszczone
+ Tryb skalowania
+ Wyczyść ciasteczka
+ Wszystkie ciasteczka wyczyszczone
+ Szukaj tylko na %s
+ Przetłumacz tą aplikację
+ Tłumaczenie
+ Musisz wpisać nazwę
+ Dostępne źródła
+ Motyw dynamiczny
+ Tylko gesty
+ Brak dostępnej pamięci
+ Inny
+ Wyniki wyszukiwania
+ Szukaj podobnych
+ Wszystkie dane zostały przywrócone
+ Dane zostały przywrócone, ale z błędami
+ Od tyłu
+ Brak aktywnych pobrań
+ Domyślny
+ Polityka zrzutów ekranu
+ Wyklucz gatunki
+ Określ gatunki, których nie chcesz widzieć w sugestiach
+ Zalogowano jako %s
+ Wybierz języki, w których chcesz czytać mangi. Możesz zmienić to później w ustawieniach.
+ Zgłoś
+ Usuwanie danych
+ Nieważna domena
+ Zmień kolejność
+ Potwierdzenie wyjścia
+ %s - %s
+ Rozdz. %1$d/%2$d Str. %3$d/%4$d
+ Włącz Wi-Fi lub sieć komórkową, aby czytać mangę online
+ Importuj
+ Wybierz plik ZIP lub CBZ.
+ Uruchom ponownie
+ Wyczyść historię wyszukiwania
+ Ta operacja nie jest obsługiwana
+ Poczekaj na zakończenie ładowania…
+ Tryb sortowania
+ Treści
+ Nie można załadować listy gatunków
+ Wstrzymane
+ Porzucone
+ Użyj odcisku palca, jeśli jest dostępny
+ Mangi z Twoich ulubionych
+ Pokaż wskaźniki postępu czytania
+ Pokaż procent przeczytania w historii i ulubionych
+ Manga oznaczona jako NSFW nigdy nie zostanie dodana do historii, a Twoje postępy nie zostaną zapisane
+ DNS przez HTTPS
+ Tryb domyślny
+ Trwale wyczyścić całą historię czytania?
+ „%s” usunięte z pamięci lokalnej
+ Wyczyść tablicę aktualizacji
+ Tablica
+ Usunąć trwale „%s” z urządzenia?
+ Może to spowodować przeniesienie dużej ilości danych
+ Wyczyść pamięć podręczną miniatur
+ Spróbuj przeformułować zapytanie.
+ To co czytasz będzie wyświetlane tutaj
+ Znajdź to, co warto przeczytać, w menu bocznym.
+ Zapisz ze źródeł online lub zaimportuj pliki.
+ Folder pobranych
+ Aktualizacja tablicy rozpocznie się wkrótce
+ Niezgodne hasła
+ Nie można wyszukać aktualizacji
+ Od prawej do lewej
+ Trzymaj na starcie
+ Utwórz problem na GitHubie
+ Możesz utworzyć kopię zapasową swojej historii i ulubionych oraz przywrócić ją
+ Wybrana konfiguracja zostanie zapamiętana dla tej mangi
+ Sprawdzanie nowych rozdziałów: %1$d z %2$d
+ Wyczyść tablicę
+ Wyczyścić trwale całą historię aktualizacji?
+ Szukanie nowych rozdziałów
+ Zaloguj się, aby wyświetlić tę zawartość
+ Domyślnie: %s
+ …i jeszcze %1$d
+ Wprowadź hasło, aby uruchomić aplikację
+ Hasło musi mieć co najmniej 4 znaki
+ Trwale usunąć wszystkie ostatnie zapytania wyszukiwania?
+ Zapisano kopię zapasową
+ Systemy niektórych urządzeń inaczej się zachowują. Może to zakłócać wykonywanie zadań w tle.
+ W kolejce
+ Pobierz lub przeczytaj ten brakujący rozdział online.
+ Brak rozdziału
+ Komentarz
+ Temat na 4PDA
+ Uprawniony
+ Logowanie na %s nie jest obsługiwane
+ Zostaniesz wylogowany ze wszystkich źródeł
+ Wyklucz mangi NSFW z historii
+ Wykorzystane źródła
+ Stosuje motyw utworzony na podstawie schematu kolorów Twojej tapety
+ Importowanie mangi: %1$d z %2$d
+ Zablokuj na NSFW
+ Proponuj mangi na podstawie Twoich preferencji
+ Wszystkie dane są analizowane lokalnie na tym urządzeniu. Twoje dane osobowe nie są przekazywane do żadnych usług
+ Zacznij czytać mangę, a otrzymasz spersonalizowane sugestie
+ Nie proponuj mang NSFW
+ Zresetuj filtr
+ Ładuj wstępnie strony
+ Aktualizowanie sugestii
+ Trwale usunąć wybrane elementy z urządzenia?
+ Usuwanie zakończone
+ Pobrać wszystkie wybrane mangi i ich rozdziały? Może to zużyć dużo danych i pamięci.
+ Pobieranie równoległe
+ Pomaga uniknąć blokowania Twojego adresu IP
+ Przetwarzanie zapisanej mangi
+ Rozdziały zostaną usunięte w tle. Może to zająć trochę czasu
+ Wpisz swój adres e-mail, aby kontynuować
+ Dostępne są nowe źródła mang
+ Sprawdzaj dostępność nowych rozdziałów i informuj o nich
+ Będziesz otrzymywać powiadomienia o aktualizacjach mang, które czytasz
+ Nie będziesz otrzymywać powiadomień, ale nowe rozdziały będą podświetlane na listach
+ Śledzenie
+ Automatycznie wykryj, czy manga to webtoon
+ Pomaga w sprawdzaniu aktualizacji w tle
+ Coś poszło nie tak. Zgłoś błąd programistom, aby pomóc nam go naprawić.
+ Może pomóc w przypadku niektórych problemów. Wszystkie autoryzacje zostaną unieważnione
+ Brak źródeł mang
+ Włącz źródła mang do czytania mang online
+ Czy na pewno chcesz usunąć wybrane ulubione kategorie? Wszystkie w nich mangi zostaną usunięte i nie będzie można tego cofnąć.
+ Naciśnij ponownie Wstecz, aby wyjść
+ Naciśnij dwukrotnie przycisk Wstecz, aby wyjść z aplikacji
+ Usunięto z „%s”
+ Treść nie została znaleziona lub została usunięta
+ Dostępna aktualizacja aplikacji: %s
+ Pokaż pasek informacji w czytniku
+ Archiwum komiksów
+ Folder z obrazami
+ Importowanie mangi
+ Importowanie zakończone
+ Możesz usunąć oryginalny plik z pamięci, aby zaoszczędzić miejsce
+ Import rozpocznie się wkrótce
+ Wybrane ustawienia kolorów zostaną zapamiętane dla tej mangi
+ Pokaż ostatnie skróty do mang
+ Pokaż ostatnie mangi po długim naciśnięciu ikony aplikacji
+ Stuknięcie w prawą krawędź lub naciśnięcie prawego klawisza zawsze powoduje przejście do następnej strony
+ Ergonomiczne sterowanie czytnikiem
+ Zapisać czy odrzucić niezapisane zmiany?
+ Brak miejsca w urządzeniu
+ Pokaż suwak przełączania stron
+ Powiększanie webtoon
+ Zezwalaj na gest powiększania/pomniejszania w trybie webtoon (beta)
+ Wyczyść też informacje o nowych rozdziałach
+ Resetuj
+ Szczegóły błędu:<br><tt>%1$s</tt><br><br>1. Spróbuj <a href=%2$s>otworzyć mangę w przeglądarce internetowej</a> aby upewnić się, że jest dostępna w źródle<br>2. Jeśli jest dostępna, wyślij raport o błędzie do programistów.
+
diff --git a/app/src/main/res/values-ru/plurals.xml b/app/src/main/res/values-ru/plurals.xml
index 779694d73..ca03bb638 100644
--- a/app/src/main/res/values-ru/plurals.xml
+++ b/app/src/main/res/values-ru/plurals.xml
@@ -1,44 +1,44 @@
-
- - Всего %1$d страница
- - Всего %1$d страницы
- - Всего %1$d страниц
-
-
- - %1$d элемент
- - %1$d элемента
- - %1$d элементов
-
-
- - %1$d новая глава
- - %1$d новых главы
- - %1$d новых глав
-
-
- - %1$d глава
- - %1$d главы
- - %1$d глав
-
-
- - %1$d глава из %2$d
- - %1$d главы из %2$d
- - %1$d глав из %2$d
-
-
-
- - %1$d минуту назад
- - %1$d минуты назад
- - %1$d минут назад
-
-
- - %1$d час назад
- - %1$d часа назад
- - %1$d часов назад
-
-
- - %1$d день назад
- - %1$d дня назад
- - %1$d дней назад
-
+
+ - Всего %1$d страница
+ - Всего %1$d страницы
+ - Всего %1$d страниц
+
+
+ - %1$d элемент
+ - %1$d элемента
+ - %1$d элементов
+
+
+ - %1$d новая глава
+ - %1$d новые главы
+ - %1$d новых глав
+ - %1$d новых глав
+
+
+ - %1$d глава
+ - %1$d главы
+ - %1$d глав
+
+
+ - %1$d глава из %2$d
+ - %1$d главы из %2$d
+ - %1$d глав из %2$d
+
+
+ - %1$d минуту назад
+ - %1$d минуты назад
+ - %1$d минут назад
+
+
+ - %1$d час назад
+ - %1$d часа назад
+ - %1$d часов назад
+
+
+ - %1$d день назад
+ - %1$d дня назад
+ - %1$d дней назад
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 3d4a096bf..b160b14b3 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -6,7 +6,7 @@
Избранное
История
Произошла ошибка
- Не удалось подключиться к интернету
+ Ошибка сети
Подробности
Главы
Список
@@ -396,4 +396,5 @@
Также очистить информацию о новых главах
Внутренняя ошибка сервера (%1$d). Повторите попытку позже
Компактно
+ Источник отключен
\ No newline at end of file
diff --git a/app/src/main/res/values-sr/plurals.xml b/app/src/main/res/values-sr/plurals.xml
new file mode 100644
index 000000000..bedee0fcd
--- /dev/null
+++ b/app/src/main/res/values-sr/plurals.xml
@@ -0,0 +1,43 @@
+
+
+
+ - Тотално %1$d странa
+ - Тотално %1$d странице
+ - Тотално %1$d странице
+
+
+ - %1$d ставке
+ - %1$d ставки
+ - %1$d ставка
+
+
+ - %1$d поглавља од %2$d
+ - %1$d поглавља од %2$d
+ - %1$d поглавља од %2$d
+
+
+ - пре %1$d минута
+ - пре %1$d минута
+ - пре %1$d минута
+
+
+ - пре %1$d сата
+ - пре %1$d сата
+ - пре %1$d сата
+
+
+ - пре %1$d дана
+ - пре %1$d дана
+ - пре %1$d дана
+
+
+ - %1$d нова поглавља
+ - %1$d нових поглавља
+ - %1$d нових поглавља
+
+
+ - %1$d поглављe
+ - %1$d поглавља
+ - %1$d поглавља
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
new file mode 100644
index 000000000..85ea48c5d
--- /dev/null
+++ b/app/src/main/res/values-sr/strings.xml
@@ -0,0 +1,15 @@
+
+
+ Локално складиште
+ Затвори мени
+ Грешка се појавила
+ Отвори мени
+ Фаворити
+ Историја
+ Неуспешно повезивање са интернетом
+ Детаљи
+ Поглавља
+ Листа
+ Детаљна листа
+ Табла
+
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 8834a5dee..be29d252e 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -1,6 +1,6 @@
- İnternete bağlı olduğunuzdan emin olunuz
+ Ağ hatası
Menüyü kapat
Menüyü aç
Dahili Depolama
@@ -395,4 +395,5 @@
Kaydedilen mangalar
Uygulama simgesine uzun basarak son mangaları kullanılabilir hale getirin
Sağ kenara dokunulduğunda veya sağ tuşa basıldığında her zaman bir sonraki sayfaya geçilir
+ Kaynak devre dışı
\ No newline at end of file
diff --git a/app/src/main/res/values-v33/bools.xml b/app/src/main/res/values-v33/bools.xml
new file mode 100644
index 000000000..7b34e7f9c
--- /dev/null
+++ b/app/src/main/res/values-v33/bools.xml
@@ -0,0 +1,4 @@
+
+
+ false
+
diff --git a/app/src/main/res/values-vi/plurals.xml b/app/src/main/res/values-vi/plurals.xml
index 79cfd3ceb..ba58d3cb8 100644
--- a/app/src/main/res/values-vi/plurals.xml
+++ b/app/src/main/res/values-vi/plurals.xml
@@ -15,4 +15,10 @@
- %1$d ngày trước
+
+ - %1$d chương từ %2$d
+
+
+ - Tổng %1$d trang
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index d38116018..ed8da9133 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -5,7 +5,7 @@
喜欢
历史
发生了一个错误
- 未能连接到互联网
+ 网络错误
章节
列表
数据被恢复了,但有错误
@@ -31,7 +31,7 @@
清除cookies
检查新的章节: %1$d/%2$d
你必须输入一个名称
- 新的漫画来源
+ 有新的漫画源可用
根据你的喜好推荐漫画
所有的数据都在这个设备上进行本地分析. 您的个人数据不会被转移到任何服务机构
从不
@@ -48,7 +48,7 @@
详细列表
网格
列表模式
- 远程资源
+ 远程源
加载中…
计算中…
%1$d/%2$d章节
@@ -230,7 +230,7 @@
关于4PDA主题
授权
不支持在%s上登录
- 你将被从所有来源中注销
+ 你将退出登录所有来源
类型
连载中
已完结
@@ -395,4 +395,5 @@
同样清除新章节信息
服务器端错误 (%1$d)。请稍后再试
紧凑
+ 已禁用图源
\ No newline at end of file
diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml
index 5cb31b523..79442ec80 100644
--- a/app/src/main/res/values/bools.xml
+++ b/app/src/main/res/values/bools.xml
@@ -3,4 +3,5 @@
false
true
false
-
\ No newline at end of file
+ true
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6e34f6fa9..404c9e564 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7,7 +7,7 @@
Favourites
History
An error occurred
- Could not connect to the Internet
+ Network error
Details
Chapters
List
@@ -399,4 +399,5 @@
Also clear information about new chapters
Compact
MyAnimeList
+ Source disabled
diff --git a/app/src/main/res/xml/locales.xml b/app/src/main/res/xml/locales.xml
new file mode 100644
index 000000000..0e10b2cc8
--- /dev/null
+++ b/app/src/main/res/xml/locales.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+