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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +