Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6881c22453 | ||
|
|
b5cd92fb5f | ||
|
|
08e5c148fd | ||
|
|
8323d399ff | ||
|
|
5108f45111 | ||
|
|
bf0d34e9cf | ||
|
|
3778a9e1d4 | ||
|
|
71ecd9d8e2 | ||
|
|
7cba8d2dc7 | ||
|
|
79c2927da2 | ||
|
|
a4a28c7342 | ||
|
|
43a92bdf08 | ||
|
|
51ff1ff7b7 | ||
|
|
2e0eb5de54 | ||
|
|
4f68e7d0e6 |
@@ -15,8 +15,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 544
|
||||
versionName '5.1'
|
||||
versionCode 547
|
||||
versionName '5.1.3'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -78,16 +78,16 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:cae7073f87') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:fa7ea5b16a') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.7.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.7.2'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.7'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||
@@ -96,7 +96,7 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
@@ -136,21 +136,21 @@ dependencies {
|
||||
implementation 'ch.acra:acra-http:5.9.7'
|
||||
implementation 'ch.acra:acra-dialog:5.9.7'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20230227'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0'
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
|
||||
|
||||
androidTestImplementation 'androidx.room:room-testing:2.5.1'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.14.0'
|
||||
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
|
||||
|
||||
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
|
||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.EnumSet
|
||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain("", null)
|
||||
get() = ConfigKey.Domain()
|
||||
|
||||
override val sortOrders: Set<SortOrder>
|
||||
get() = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
@@ -8,19 +8,22 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
if (recyclerView.hasPendingAdapterUpdates()) {
|
||||
return
|
||||
}
|
||||
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
if (firstVisibleItemPosition == RecyclerView.NO_POSITION) {
|
||||
return
|
||||
}
|
||||
if (firstVisibleItemPosition <= offsetTop) {
|
||||
onScrolledToStart(recyclerView)
|
||||
}
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) {
|
||||
onScrolledToEnd(recyclerView)
|
||||
}
|
||||
if (firstVisibleItemPosition <= offsetTop) {
|
||||
onScrolledToStart(recyclerView)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onScrolledToStart(recyclerView: RecyclerView)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.koitharu.kotatsu.base.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class ScrollListenerInvalidationObserver(
|
||||
private val recyclerView: RecyclerView,
|
||||
private val scrollListener: RecyclerView.OnScrollListener,
|
||||
) : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
override fun onChanged() {
|
||||
super.onChanged()
|
||||
invalidateScroll()
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
invalidateScroll()
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeRemoved(positionStart, itemCount)
|
||||
invalidateScroll()
|
||||
}
|
||||
|
||||
private fun invalidateScroll() {
|
||||
recyclerView.post {
|
||||
scrollListener.onScrolled(recyclerView, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@@ -24,7 +25,9 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
|
||||
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
|
||||
return
|
||||
}
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||
|
||||
@@ -23,6 +23,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import okhttp3.Cache
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
@@ -88,14 +89,19 @@ interface AppModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
fun provideHttpCache(
|
||||
localStorageManager: LocalStorageManager,
|
||||
): Cache = localStorageManager.createHttpCache()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
cache: Cache,
|
||||
commonHeadersInterceptor: CommonHeadersInterceptor,
|
||||
mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||
cookieJar: CookieJar,
|
||||
settings: AppSettings,
|
||||
): OkHttpClient {
|
||||
val cache = localStorageManager.createHttpCache()
|
||||
return OkHttpClient.Builder().apply {
|
||||
connectTimeout(20, TimeUnit.SECONDS)
|
||||
readTimeout(60, TimeUnit.SECONDS)
|
||||
@@ -106,6 +112,7 @@ interface AppModule {
|
||||
bypassSSLErrors()
|
||||
}
|
||||
cache(cache)
|
||||
addNetworkInterceptor(CacheLimitInterceptor())
|
||||
addInterceptor(GZipInterceptor())
|
||||
addInterceptor(commonHeadersInterceptor)
|
||||
addInterceptor(CloudFlareInterceptor())
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import androidx.collection.LruCache
|
||||
|
||||
class DeferredLruCache<T>(maxSize: Int) : LruCache<ContentCache.Key, SafeDeferred<T>>(maxSize)
|
||||
33
app/src/main/java/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt
vendored
Normal file
33
app/src/main/java/org/koitharu/kotatsu/core/cache/ExpiringLruCache.kt
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ExpiringLruCache<T>(
|
||||
val maxSize: Int,
|
||||
private val lifetime: Long,
|
||||
private val timeUnit: TimeUnit,
|
||||
) {
|
||||
|
||||
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
|
||||
|
||||
operator fun get(key: ContentCache.Key): T? {
|
||||
val value = cache.get(key) ?: return null
|
||||
if (value.isExpired) {
|
||||
cache.remove(key)
|
||||
}
|
||||
return value.get()
|
||||
}
|
||||
|
||||
operator fun set(key: ContentCache.Key, value: T) {
|
||||
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
cache.evictAll()
|
||||
}
|
||||
|
||||
fun trimToSize(size: Int) {
|
||||
cache.trimToSize(size)
|
||||
}
|
||||
}
|
||||
34
app/src/main/java/org/koitharu/kotatsu/core/cache/ExpiringValue.kt
vendored
Normal file
34
app/src/main/java/org/koitharu/kotatsu/core/cache/ExpiringValue.kt
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import android.os.SystemClock
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ExpiringValue<T>(
|
||||
private val value: T,
|
||||
lifetime: Long,
|
||||
timeUnit: TimeUnit,
|
||||
) {
|
||||
|
||||
private val expiresAt = SystemClock.elapsedRealtime() + timeUnit.toMillis(lifetime)
|
||||
|
||||
val isExpired: Boolean
|
||||
get() = SystemClock.elapsedRealtime() >= expiresAt
|
||||
|
||||
fun get(): T? = if (isExpired) null else value
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ExpiringValue<*>
|
||||
|
||||
if (value != other.value) return false
|
||||
return expiresAt == other.expiresAt
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = value?.hashCode() ?: 0
|
||||
result = 31 * result + expiresAt.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.content.res.Configuration
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
||||
|
||||
@@ -13,8 +14,8 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
||||
application.registerComponentCallbacks(this)
|
||||
}
|
||||
|
||||
private val detailsCache = DeferredLruCache<Manga>(4)
|
||||
private val pagesCache = DeferredLruCache<List<MangaPage>>(4)
|
||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
||||
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
||||
|
||||
override val isCachingEnabled: Boolean = true
|
||||
|
||||
@@ -23,7 +24,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
||||
}
|
||||
|
||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||
detailsCache.put(ContentCache.Key(source, url), details)
|
||||
detailsCache[ContentCache.Key(source, url)] = details
|
||||
}
|
||||
|
||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||
@@ -31,7 +32,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
||||
}
|
||||
|
||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||
pagesCache.put(ContentCache.Key(source, url), pages)
|
||||
pagesCache[ContentCache.Key(source, url)] = pages
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||
@@ -43,17 +44,17 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
||||
trimCache(pagesCache, level)
|
||||
}
|
||||
|
||||
private fun trimCache(cache: DeferredLruCache<*>, level: Int) {
|
||||
private fun trimCache(cache: ExpiringLruCache<*>, level: Int) {
|
||||
when (level) {
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
|
||||
ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
|
||||
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.evictAll()
|
||||
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.clear()
|
||||
|
||||
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
|
||||
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1)
|
||||
|
||||
else -> cache.trimToSize(cache.maxSize() / 2)
|
||||
else -> cache.trimToSize(cache.maxSize / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
@@ -82,6 +85,15 @@ class FileLogger(
|
||||
flushImpl()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun flushBlocking() {
|
||||
if (!isEnabled) {
|
||||
return
|
||||
}
|
||||
runBlockingSafe { flushJob?.cancelAndJoin() }
|
||||
runBlockingSafe { flushImpl() }
|
||||
}
|
||||
|
||||
private fun postFlush() {
|
||||
if (flushJob?.isActive == true) {
|
||||
return
|
||||
@@ -96,10 +108,10 @@ class FileLogger(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun flushImpl() {
|
||||
private suspend fun flushImpl() = withContext(NonCancellable) {
|
||||
mutex.withLock {
|
||||
if (buffer.isEmpty()) {
|
||||
return
|
||||
return@withContext
|
||||
}
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
if (file.length() > MAX_SIZE_BYTES) {
|
||||
@@ -131,4 +143,9 @@ class FileLogger(
|
||||
}
|
||||
bakFile.delete()
|
||||
}
|
||||
|
||||
private inline fun runBlockingSafe(crossinline block: suspend () -> Unit) = try {
|
||||
runBlocking(NonCancellable) { block() }
|
||||
} catch (_: InterruptedException) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class CacheLimitInterceptor : Interceptor {
|
||||
|
||||
private val defaultMaxAge = TimeUnit.HOURS.toSeconds(1)
|
||||
private val defaultCacheControl = CacheControl.Builder()
|
||||
.maxAge(defaultMaxAge.toInt(), TimeUnit.SECONDS)
|
||||
.build()
|
||||
.toString()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
val responseCacheControl = CacheControl.parse(response.headers)
|
||||
if (responseCacheControl.noStore || responseCacheControl.maxAgeSeconds <= defaultMaxAge) {
|
||||
return response
|
||||
}
|
||||
return response.newBuilder()
|
||||
.header(CommonHeaders.CACHE_CONTROL, defaultCacheControl)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ object CommonHeaders {
|
||||
const val CONTENT_ENCODING = "Content-Encoding"
|
||||
const val ACCEPT_ENCODING = "Accept-Encoding"
|
||||
const val AUTHORIZATION = "Authorization"
|
||||
const val CACHE_CONTROL = "Cache-Control"
|
||||
|
||||
val CACHE_CONTROL_NO_STORE: CacheControl
|
||||
get() = CacheControl.Builder().noStore().build()
|
||||
|
||||
@@ -345,6 +345,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_SOURCES_HIDDEN = "sources_hidden"
|
||||
const val KEY_TRAFFIC_WARNING = "traffic_warning"
|
||||
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
|
||||
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
|
||||
const val KEY_COOKIES_CLEAR = "cookies_clear"
|
||||
const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear"
|
||||
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
|
||||
|
||||
@@ -31,7 +31,8 @@ sealed class LocalMangaInput(
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
protected fun zipUri(file: File, entryName: String) = "cbz://${file.path}#$entryName"
|
||||
protected fun zipUri(file: File, entryName: String): String =
|
||||
Uri.fromParts("cbz", file.path, entryName).toString()
|
||||
|
||||
@JvmStatic
|
||||
protected fun Manga.copy2(
|
||||
|
||||
@@ -252,6 +252,7 @@ class ReaderViewModel @Inject constructor(
|
||||
val prevJob = stateChangeJob
|
||||
stateChangeJob = launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
loadingJob?.join()
|
||||
val pages = content.value?.pages ?: return@launchJob
|
||||
pages.getOrNull(position)?.let { page ->
|
||||
currentState.update { cs ->
|
||||
@@ -263,12 +264,12 @@ class ReaderViewModel @Inject constructor(
|
||||
return@launchJob
|
||||
}
|
||||
ensureActive()
|
||||
if (position <= BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||
}
|
||||
if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||
}
|
||||
if (position <= BOUNDS_PAGE_OFFSET) {
|
||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||
}
|
||||
if (pageLoader.isPrefetchApplicable()) {
|
||||
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
|
||||
}
|
||||
@@ -348,7 +349,9 @@ class ReaderViewModel @Inject constructor(
|
||||
|
||||
@AnyThread
|
||||
private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) {
|
||||
val prevJob = loadingJob
|
||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||
prevJob?.join()
|
||||
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
|
||||
content.emitValue(ReaderContent(chaptersLoader.snapshot(), null))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import okhttp3.Headers
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.utils.EditTextValidator
|
||||
|
||||
class HeaderValidator : EditTextValidator() {
|
||||
|
||||
private val headers = Headers.Builder()
|
||||
|
||||
override fun validate(text: String): ValidationResult {
|
||||
val trimmed = text.trim()
|
||||
if (trimmed.isEmpty()) {
|
||||
return ValidationResult.Success
|
||||
}
|
||||
return if (!validateImpl(trimmed)) {
|
||||
ValidationResult.Failed(context.getString(R.string.invalid_value_message))
|
||||
} else {
|
||||
ValidationResult.Success
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateImpl(value: String): Boolean = runCatching {
|
||||
headers[CommonHeaders.USER_AGENT] = value
|
||||
}.isSuccess
|
||||
}
|
||||
@@ -8,7 +8,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.Cache
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
@@ -39,6 +42,9 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
@Inject
|
||||
lateinit var cookieJar: MutableCookieJar
|
||||
|
||||
@Inject
|
||||
lateinit var cache: Cache
|
||||
|
||||
@Inject
|
||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||
|
||||
@@ -52,6 +58,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.PAGES)
|
||||
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.THUMBS)
|
||||
findPreference<Preference>(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindSummaryToHttpCacheSize()
|
||||
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
||||
viewLifecycleScope.launch {
|
||||
lifecycle.awaitStateAtLeast(Lifecycle.State.RESUMED)
|
||||
@@ -90,6 +97,11 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_HTTP_CACHE_CLEAR -> {
|
||||
clearHttpCache()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
||||
viewLifecycleScope.launch {
|
||||
trackerRepo.clearLogs()
|
||||
@@ -131,6 +143,32 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
summary = FileSize.BYTES.format(context, size)
|
||||
}
|
||||
|
||||
private fun Preference.bindSummaryToHttpCacheSize() = viewLifecycleScope.launch {
|
||||
val size = runInterruptible(Dispatchers.IO) { cache.size() }
|
||||
summary = FileSize.BYTES.format(context, size)
|
||||
}
|
||||
|
||||
private fun clearHttpCache() {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_HTTP_CACHE_CLEAR) ?: return
|
||||
val ctx = preference.context.applicationContext
|
||||
viewLifecycleScope.launch {
|
||||
try {
|
||||
preference.isEnabled = false
|
||||
val size = runInterruptible(Dispatchers.IO) {
|
||||
cache.evictAll()
|
||||
cache.size()
|
||||
}
|
||||
preference.summary = FileSize.BYTES.format(ctx, size)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
preference.summary = e.getDisplayMessage(ctx.resources)
|
||||
} finally {
|
||||
preference.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearSearchHistory(preference: Preference) {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.clear_search_history)
|
||||
|
||||
@@ -19,10 +19,12 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
|
||||
val preference: Preference = when (key) {
|
||||
is ConfigKey.Domain -> {
|
||||
val presetValues = key.presetValues
|
||||
if (presetValues.isNullOrEmpty()) {
|
||||
if (presetValues.size <= 1) {
|
||||
EditTextPreference(requireContext())
|
||||
} else {
|
||||
AutoCompleteTextViewPreference(requireContext()).apply { entries = presetValues }
|
||||
AutoCompleteTextViewPreference(requireContext()).apply {
|
||||
entries = presetValues.toStringArray()
|
||||
}
|
||||
}.apply {
|
||||
summaryProvider = EditTextDefaultSummaryProvider(key.defaultValue)
|
||||
setOnBindEditTextListener(
|
||||
@@ -44,7 +46,7 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
|
||||
EditTextBindListener(
|
||||
inputType = EditorInfo.TYPE_CLASS_TEXT,
|
||||
hint = key.defaultValue,
|
||||
validator = null,
|
||||
validator = HeaderValidator(),
|
||||
),
|
||||
)
|
||||
setTitle(R.string.user_agent)
|
||||
@@ -64,3 +66,7 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
|
||||
screen.addPreference(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Array<out String>.toStringArray(): Array<String> {
|
||||
return Array(size) { i -> this[i] as? String ?: "" }
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
||||
import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability
|
||||
import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
@@ -41,7 +42,9 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
|
||||
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
|
||||
return
|
||||
}
|
||||
val source = intent?.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource
|
||||
if (source == null) {
|
||||
finishAfterTransition()
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.contentValuesOf
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -22,7 +23,9 @@ import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.db.TABLE_MANGA
|
||||
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
|
||||
import org.koitharu.kotatsu.core.db.TABLE_TAGS
|
||||
import org.koitharu.kotatsu.core.logs.LoggersModule
|
||||
import org.koitharu.kotatsu.core.network.GZipInterceptor
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
|
||||
import org.koitharu.kotatsu.sync.data.SyncAuthApi
|
||||
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
|
||||
@@ -61,6 +64,7 @@ class SyncHelper(
|
||||
}
|
||||
private val defaultGcPeriod: Long // gc period if sync enabled
|
||||
get() = TimeUnit.DAYS.toMillis(4)
|
||||
private val logger = LoggersModule.provideSyncLogger(context, AppSettings(context))
|
||||
|
||||
fun syncFavourites(syncResult: SyncResult) {
|
||||
val data = JSONObject()
|
||||
@@ -71,7 +75,7 @@ class SyncHelper(
|
||||
.url("$baseUrl/resource/$TABLE_FAVOURITES")
|
||||
.post(data.toRequestBody())
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute().parseJsonOrNull()
|
||||
val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
|
||||
if (response != null) {
|
||||
val timestamp = response.getLong(FIELD_TIMESTAMP)
|
||||
val categoriesResult =
|
||||
@@ -93,7 +97,7 @@ class SyncHelper(
|
||||
.url("$baseUrl/resource/$TABLE_HISTORY")
|
||||
.post(data.toRequestBody())
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute().parseJsonOrNull()
|
||||
val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
|
||||
if (response != null) {
|
||||
val result = upsertHistory(
|
||||
json = response.getJSONArray(TABLE_HISTORY),
|
||||
@@ -105,6 +109,19 @@ class SyncHelper(
|
||||
gcHistory()
|
||||
}
|
||||
|
||||
fun onError(e: Throwable) {
|
||||
if (logger.isEnabled) {
|
||||
logger.log("Sync error", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSyncComplete(result: SyncResult) {
|
||||
if (logger.isEnabled) {
|
||||
logger.log("Sync finshed: ${result.toDebugString()}")
|
||||
logger.flushBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun upsertHistory(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
|
||||
val uri = uri(authorityHistory, TABLE_HISTORY)
|
||||
val operations = ArrayList<ContentProviderOperation>()
|
||||
@@ -298,4 +315,10 @@ class SyncHelper(
|
||||
private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject
|
||||
|
||||
private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray
|
||||
|
||||
private fun Response.log() = apply {
|
||||
if (logger.isEnabled) {
|
||||
logger.log("$code ${request.url}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,12 +84,14 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
binding.groupLogin.isVisible = false
|
||||
binding.groupPassword.isVisible = true
|
||||
pageBackCallback.update()
|
||||
binding.editPassword.requestFocus()
|
||||
}
|
||||
|
||||
R.id.button_back -> {
|
||||
binding.groupPassword.isVisible = false
|
||||
binding.groupLogin.isVisible = true
|
||||
pageBackCallback.update()
|
||||
binding.editEmail.requestFocus()
|
||||
}
|
||||
|
||||
R.id.button_done -> {
|
||||
@@ -200,6 +202,7 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
||||
override fun handleOnBackPressed() {
|
||||
binding.groupLogin.isVisible = true
|
||||
binding.groupPassword.isVisible = false
|
||||
binding.editEmail.requestFocus()
|
||||
update()
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.koitharu.kotatsu.core.db.*
|
||||
import org.koitharu.kotatsu.core.logs.FileLogger
|
||||
import org.koitharu.kotatsu.core.logs.SyncLogger
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
abstract class SyncProvider : ContentProvider() {
|
||||
@@ -24,7 +22,6 @@ abstract class SyncProvider : ContentProvider() {
|
||||
EntryPointAccessors.fromApplication(checkNotNull(context), SyncProviderEntryPoint::class.java)
|
||||
}
|
||||
private val database by lazy { entryPoint.database }
|
||||
private val logger by lazy { entryPoint.logger }
|
||||
|
||||
private val supportedTables = setOf(
|
||||
TABLE_FAVOURITES,
|
||||
@@ -52,7 +49,6 @@ abstract class SyncProvider : ContentProvider() {
|
||||
.selection(selection, selectionArgs)
|
||||
.orderBy(sortOrder)
|
||||
.create()
|
||||
logger.log("query: ${sqlQuery.sql} (${selectionArgs.contentToString()})")
|
||||
return database.openHelper.readableDatabase.query(sqlQuery)
|
||||
}
|
||||
|
||||
@@ -65,7 +61,6 @@ abstract class SyncProvider : ContentProvider() {
|
||||
if (values == null || table == null) {
|
||||
return null
|
||||
}
|
||||
logger.log { "insert: $table [$values]" }
|
||||
val db = database.openHelper.writableDatabase
|
||||
if (db.insert(table, SQLiteDatabase.CONFLICT_IGNORE, values) < 0) {
|
||||
db.update(table, values)
|
||||
@@ -75,7 +70,6 @@ abstract class SyncProvider : ContentProvider() {
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||
val table = getTableName(uri) ?: return 0
|
||||
logger.log { "delete: $table ($selection) : (${selectionArgs.contentToString()})" }
|
||||
return database.openHelper.writableDatabase.delete(table, selection, selectionArgs)
|
||||
}
|
||||
|
||||
@@ -84,7 +78,6 @@ abstract class SyncProvider : ContentProvider() {
|
||||
if (values == null || table == null) {
|
||||
return 0
|
||||
}
|
||||
logger.log { "update: $table ($selection) : (${selectionArgs.contentToString()}) [$values]" }
|
||||
return database.openHelper.writableDatabase
|
||||
.update(table, SQLiteDatabase.CONFLICT_IGNORE, values, selection, selectionArgs)
|
||||
}
|
||||
@@ -127,8 +120,5 @@ abstract class SyncProvider : ContentProvider() {
|
||||
interface SyncProviderEntryPoint {
|
||||
|
||||
val database: MangaDatabase
|
||||
|
||||
@get:SyncLogger
|
||||
val logger: FileLogger
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
|
||||
runCatchingCancellable {
|
||||
syncHelper.syncFavourites(syncResult)
|
||||
SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
|
||||
}.onFailure(syncResult::onError)
|
||||
}.onFailure { e ->
|
||||
syncResult.onError(e)
|
||||
syncHelper.onError(e)
|
||||
}
|
||||
syncHelper.onSyncComplete(syncResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context
|
||||
runCatchingCancellable {
|
||||
syncHelper.syncHistory(syncResult)
|
||||
SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
|
||||
}.onFailure(syncResult::onError)
|
||||
}.onFailure { e ->
|
||||
syncResult.onError(e)
|
||||
syncHelper.onError(e)
|
||||
}
|
||||
syncHelper.onSyncComplete(syncResult)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
@@ -84,6 +85,7 @@ class ShareHelper(private val context: Context) {
|
||||
fun shareLogs(loggers: Collection<FileLogger>) {
|
||||
val intentBuilder = ShareCompat.IntentBuilder(context)
|
||||
.setType(TYPE_TEXT)
|
||||
var hasLogs = false
|
||||
for (logger in loggers) {
|
||||
val logFile = logger.file
|
||||
if (!logFile.exists()) {
|
||||
@@ -91,8 +93,13 @@ class ShareHelper(private val context: Context) {
|
||||
}
|
||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", logFile)
|
||||
intentBuilder.addStream(uri)
|
||||
hasLogs = true
|
||||
}
|
||||
if (hasLogs) {
|
||||
intentBuilder.setChooserTitle(R.string.share_logs)
|
||||
intentBuilder.startChooser()
|
||||
} else {
|
||||
Toast.makeText(context, R.string.nothing_here, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
intentBuilder.setChooserTitle(R.string.share_logs)
|
||||
intentBuilder.startChooser()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.Window
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.IntegerRes
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
@@ -170,3 +171,18 @@ fun Context.findActivity(): Activity? = when (this) {
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> null
|
||||
}
|
||||
|
||||
inline fun Activity.catchingWebViewUnavailability(block: () -> Unit): Boolean {
|
||||
return try {
|
||||
block()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
if (e.isWebViewUnavailable()) {
|
||||
Toast.makeText(this, R.string.web_view_unavailable, Toast.LENGTH_LONG).show()
|
||||
finishAfterTransition()
|
||||
false
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.res.Resources
|
||||
import android.util.AndroidRuntimeException
|
||||
import androidx.collection.arraySetOf
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import okio.FileNotFoundException
|
||||
@@ -94,3 +95,8 @@ inline fun <R> runCatchingCancellable(block: () -> R): Result<R> {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun Throwable.isWebViewUnavailable(): Boolean {
|
||||
return (this is AndroidRuntimeException && message?.contains("WebView") == true) ||
|
||||
cause?.isWebViewUnavailable() == true
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@ var RecyclerView.firstVisibleItemPosition: Int
|
||||
}
|
||||
}
|
||||
|
||||
val RecyclerView.visibleItemCount: Int
|
||||
get() = (layoutManager as? LinearLayoutManager)?.run {
|
||||
findLastVisibleItemPosition() - findFirstVisibleItemPosition()
|
||||
} ?: 0
|
||||
|
||||
fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
|
||||
if (visibility != View.VISIBLE) {
|
||||
return false
|
||||
|
||||
@@ -410,4 +410,6 @@
|
||||
<string name="downloads_paused">Спампоўкі прыпыненыя</string>
|
||||
<string name="downloads_removed">Спампоўкі выдалены</string>
|
||||
<string name="downloads_cancelled">Спампоўкі былі адменены</string>
|
||||
<string name="translations">Пераклады</string>
|
||||
<string name="web_view_unavailable">WebView недаступны: праверце, ці ўсталяваны пастаўшчык WebView</string>
|
||||
</resources>
|
||||
@@ -68,7 +68,7 @@
|
||||
<string name="zoom_mode_keep_start">Am Anfang ausrichten</string>
|
||||
<string name="zoom_mode_fit_width">An Breite anpassen</string>
|
||||
<string name="zoom_mode_fit_height">An Höhe anpassen</string>
|
||||
<string name="black_dark_theme_summary">Spart Energie bei AMOLED-Bildschirme</string>
|
||||
<string name="black_dark_theme_summary">Spart Energie auf AMOLED-Bildschirmen</string>
|
||||
<string name="black_dark_theme">Reines Schwarz</string>
|
||||
<string name="right_to_left">Von rechts nach links</string>
|
||||
<string name="create_category">Neue Kategorie</string>
|
||||
@@ -91,17 +91,17 @@
|
||||
<string name="confirm">Bestätigen</string>
|
||||
<string name="protect_application_subtitle">Gib ein Passwort ein, mit dem die App gestartet werden soll</string>
|
||||
<string name="next">Weiter</string>
|
||||
<string name="text_clear_search_history_prompt">Möchtest du wirklich alle letzten Suchanfragen entfernen\?</string>
|
||||
<string name="text_clear_search_history_prompt">Möchtest du alle letzten Suchanfragen unwiderruflich entfernen\?</string>
|
||||
<string name="read_more">Mehr erfahren</string>
|
||||
<string name="tracker_warning">Einige Geräte haben ein anderes Systemverhalten, welches womöglich Hintergrundprozesse unterbricht.</string>
|
||||
<string name="backup_saved">Sicherung erfolgreich gespeichert</string>
|
||||
<string name="backup_saved">Backup gespeichert</string>
|
||||
<string name="welcome">Willkommen</string>
|
||||
<string name="remove_category">Entfernen</string>
|
||||
<string name="rotate_screen">Bildschirm drehen</string>
|
||||
<string name="size_s">Größe: %s</string>
|
||||
<string name="new_version_s">Neue Version: %s</string>
|
||||
<string name="search_results">Suchergebnisse</string>
|
||||
<string name="text_feed_holder">Hier siehst du die neuen Kapitel des Mangas, den du gerade liest</string>
|
||||
<string name="text_feed_holder">Hier siehst du neue Kapitel der Mangas, die du liest</string>
|
||||
<string name="read_later">Später lesen</string>
|
||||
<string name="favourites_category_empty">Diese Kategorie ist leer</string>
|
||||
<string name="all_favourites">Alle Favoriten</string>
|
||||
@@ -115,7 +115,7 @@
|
||||
<string name="text_local_holder_secondary">Speicher Manga aus Online-Quellen oder importiere Dateien.</string>
|
||||
<string name="text_local_holder_primary">Speichere erst etwas</string>
|
||||
<string name="text_history_holder_secondary">Was du lesen kannst, findest du im Seitenmenü.</string>
|
||||
<string name="text_search_holder_secondary">Versuche, die Abfrage umzuformulieren.</string>
|
||||
<string name="text_search_holder_secondary">Versuche, die Anfrage umzuformulieren.</string>
|
||||
<string name="favourites_categories">Favoriten-Kategorien</string>
|
||||
<string name="vibration">Vibration</string>
|
||||
<string name="light_indicator">LED-Anzeige</string>
|
||||
@@ -130,7 +130,7 @@
|
||||
<string name="open_in_browser">Im Browser öffnen</string>
|
||||
<string name="external_storage">Externer Speicher</string>
|
||||
<string name="internal_storage">Interner Speicher</string>
|
||||
<string name="search_history_cleared">Suchverlauf gelöscht</string>
|
||||
<string name="search_history_cleared">Gelöscht</string>
|
||||
<string name="clear_search_history">Suchverlauf löschen</string>
|
||||
<string name="clear_thumbs_cache">Miniaturansichten Cache löschen</string>
|
||||
<string name="error">Fehler</string>
|
||||
@@ -158,14 +158,14 @@
|
||||
<string name="clear">Löschen</string>
|
||||
<string name="taps_on_edges">Rand antippen</string>
|
||||
<string name="captcha_solve">Lösen</string>
|
||||
<string name="captcha_required">CAPTCHA ist erforderlich</string>
|
||||
<string name="captcha_required">CAPTCHA erforderlich</string>
|
||||
<string name="silent">Stumm</string>
|
||||
<string name="reader_mode_hint">Die gewählte Konfiguration wird für diesen Manga gespeichert</string>
|
||||
<string name="tap_to_try_again">Tippe, um es erneut zu versuchen</string>
|
||||
<string name="today">Heute</string>
|
||||
<string name="long_ago">Vor langer Zeit</string>
|
||||
<string name="yesterday">Gestern</string>
|
||||
<string name="backup_information">Du kannst eine Sicherung deines Verlaufs und deiner Favoriten erstellen und diese wiederherstellen</string>
|
||||
<string name="backup_information">Du kannst ein Backup deines Verlaufs und deiner Favoriten erstellen und wiederherstellen</string>
|
||||
<string name="data_restored_with_errors">Die Daten wurden wiederhergestellt, aber es gibt Fehler</string>
|
||||
<string name="data_restored_success">Alle Daten wiederhergestellt</string>
|
||||
<string name="zoom_mode_fit_center">An Zentrum anpassen</string>
|
||||
@@ -181,11 +181,11 @@
|
||||
<string name="dont_check">Nicht prüfen</string>
|
||||
<string name="domain">Domäne</string>
|
||||
<string name="gestures_only">Nur Gesten</string>
|
||||
<string name="chapter_is_missing">Kapitel fehlt</string>
|
||||
<string name="chapter_is_missing">Das Kapitel fehlt</string>
|
||||
<string name="chapter_is_missing_text">Lade dieses fehlende Kapitel herunter oder lese es online.</string>
|
||||
<string name="queued">In Warteschlange</string>
|
||||
<string name="about_app_translation">Übersetzung</string>
|
||||
<string name="about_app_translation_summary">Übersetze diese App</string>
|
||||
<string name="about_app_translation_summary">Übersetze die App</string>
|
||||
<string name="text_clear_cookies_prompt">Du wirst von allen Quellen abgemeldet</string>
|
||||
<string name="genres">Genres</string>
|
||||
<string name="auth_not_supported_by">Anmeldung bei %s wird nicht unterstützt</string>
|
||||
@@ -316,7 +316,7 @@
|
||||
<string name="reorder">Neu anordnen</string>
|
||||
<string name="empty">Leer</string>
|
||||
<string name="explore">Erkunden</string>
|
||||
<string name="confirm_exit">Drücke zum Verlassen erneut auf Zurück</string>
|
||||
<string name="confirm_exit">Drücke zum Beenden erneut Zurück</string>
|
||||
<string name="exit_confirmation_summary">Drücke zweimal Zurück, um die App zu beenden</string>
|
||||
<string name="feed">Feed</string>
|
||||
<string name="incognito_mode">Inkognito-Modus</string>
|
||||
@@ -398,16 +398,18 @@
|
||||
<string name="remove_completed">Entfernung abgeschlossen</string>
|
||||
<string name="cancel_all">Alle abbrechen</string>
|
||||
<string name="downloads_wifi_only">Nur über Wi-Fi herunterladen</string>
|
||||
<string name="downloads_wifi_only_summary">Beenden des Herunterladens beim Wechsel zu einem Mobilfunknetz</string>
|
||||
<string name="downloads_wifi_only_summary">Beende das Herunterladen beim Wechsel zu einem Mobilfunknetz</string>
|
||||
<string name="suggestions_notifications_summary">Zeige ab und zu Benachrichtigungen mit Manga-Vorschlägen</string>
|
||||
<string name="downloads_removed">Downloads wurden entfernt</string>
|
||||
<string name="suggestion_manga">Vorschlag: %s</string>
|
||||
<string name="more">Mehr</string>
|
||||
<string name="cancel_all_downloads_confirm">Alle Downloads werden abgebrochen, teilweise heruntergeladene Dateien gehen verloren</string>
|
||||
<string name="remove_completed_downloads_confirm">Dein Downloads-Verlauf wird unwiderruflich gelöscht sein</string>
|
||||
<string name="remove_completed_downloads_confirm">Dein Downloads-Verlauf wird unwiderruflich gelöscht</string>
|
||||
<string name="text_downloads_list_holder">Du hast keine Downloads</string>
|
||||
<string name="downloads_resumed">Downloads wurden fortgesetzt</string>
|
||||
<string name="downloads_paused">Downloads wurden pausiert</string>
|
||||
<string name="downloads_cancelled">Downloads wurden abgebrochen</string>
|
||||
<string name="suggestions_enable_prompt">Willst du personalisierte Manga-Vorschläge erhalten\?</string>
|
||||
<string name="translations">Übersetzungen</string>
|
||||
<string name="web_view_unavailable">WebView nicht verfügbar: überprüfe, ob WebView installiert ist</string>
|
||||
</resources>
|
||||
@@ -410,4 +410,6 @@
|
||||
<string name="downloads_removed">Las descargas se han eliminado</string>
|
||||
<string name="downloads_cancelled">Las descargas se han cancelado</string>
|
||||
<string name="suggestions_enable_prompt">¿Quieres recibir sugerencias sobre mangas personalizadas\?</string>
|
||||
<string name="translations">Traducciones</string>
|
||||
<string name="web_view_unavailable">WebView no está disponible: comprueba si el proveedor de WebView está instalado</string>
|
||||
</resources>
|
||||
@@ -263,7 +263,7 @@
|
||||
<string name="tracking">Pelacakan</string>
|
||||
<string name="logout">Keluar</string>
|
||||
<string name="sync">Sinkronisasi</string>
|
||||
<string name="send">Terkirim</string>
|
||||
<string name="send">Kirim</string>
|
||||
<string name="status_reading">Dibaca</string>
|
||||
<string name="status_on_hold">Ditunda</string>
|
||||
<string name="invalid_domain_message">Domain tidak valid</string>
|
||||
|
||||
@@ -4,4 +4,11 @@
|
||||
<string name="details">ଵିଵରଣୀ</string>
|
||||
<string name="settings">ସେଟିଂ</string>
|
||||
<string name="suggestion_manga">ପରାମର୍ଶ: %s</string>
|
||||
<string name="done">ହେଲା</string>
|
||||
<string name="services">ସେଵା</string>
|
||||
<string name="got_it">ବୁଝିଗଲି</string>
|
||||
<string name="black_dark_theme">କଳା</string>
|
||||
<string name="theme">ଥିମ୍</string>
|
||||
<string name="speed">ଵେଗ</string>
|
||||
<string name="download_started">ଡାଉନଲୋଡ୍ ଆରମ୍ଭ ହେଲା</string>
|
||||
</resources>
|
||||
@@ -410,4 +410,6 @@
|
||||
<string name="suggestions_notifications_summary">Иногда показывать уведомления с рекомендуемой мангой</string>
|
||||
<string name="remove_completed_downloads_confirm">Ваша история загрузок будет удалена</string>
|
||||
<string name="suggestions_enable_prompt">Хотите ли Вы получать персонализированные рекомендации манги\?</string>
|
||||
<string name="translations">Переводы</string>
|
||||
<string name="web_view_unavailable">WebView недоступен: проверьте, установлен ли провайдер WebView</string>
|
||||
</resources>
|
||||
@@ -410,4 +410,6 @@
|
||||
<string name="downloads_removed">Завантаження видалено</string>
|
||||
<string name="downloads_cancelled">Завантаження скасовано</string>
|
||||
<string name="suggestions_enable_prompt">Хочете отримувати персоналізовані пропозиції щодо манги\?</string>
|
||||
<string name="web_view_unavailable">WebView недоступний: перевірте, чи встановлено провайдер WebView</string>
|
||||
<string name="translations">Переклади</string>
|
||||
</resources>
|
||||
@@ -415,4 +415,7 @@
|
||||
<string name="downloads_cancelled">Downloads have been cancelled</string>
|
||||
<string name="suggestions_enable_prompt">Do you want to receive personalized manga suggestions?</string>
|
||||
<string name="translations">Translations</string>
|
||||
<string name="web_view_unavailable">WebView not available: check if WebView provider is installed</string>
|
||||
<string name="invalid_value_message">Invalid value</string>
|
||||
<string name="clear_network_cache">Clear network cache</string>
|
||||
</resources>
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
android:summary="@string/computing_"
|
||||
android:title="@string/clear_pages_cache" />
|
||||
|
||||
<Preference
|
||||
android:key="http_cache_clear"
|
||||
android:persistent="false"
|
||||
android:summary="@string/loading_"
|
||||
android:title="@string/clear_network_cache" />
|
||||
|
||||
<Preference
|
||||
android:key="cookies_clear"
|
||||
android:persistent="false"
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.util.EnumSet
|
||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain("localhost", null)
|
||||
get() = ConfigKey.Domain("localhost")
|
||||
|
||||
override val sortOrders: Set<SortOrder>
|
||||
get() = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
Reference in New Issue
Block a user