Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef9b16da0b | ||
|
|
5d1ef983e9 | ||
|
|
eb78a776cf | ||
|
|
6881c22453 | ||
|
|
b5cd92fb5f | ||
|
|
08e5c148fd | ||
|
|
8323d399ff | ||
|
|
5108f45111 | ||
|
|
bf0d34e9cf | ||
|
|
3778a9e1d4 | ||
|
|
71ecd9d8e2 | ||
|
|
7cba8d2dc7 | ||
|
|
79c2927da2 | ||
|
|
a4a28c7342 | ||
|
|
43a92bdf08 | ||
|
|
51ff1ff7b7 | ||
|
|
2e0eb5de54 | ||
|
|
4f68e7d0e6 |
@@ -15,8 +15,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 544
|
versionCode 548
|
||||||
versionName '5.1'
|
versionName '5.1.4'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -78,16 +78,16 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:cae7073f87') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:44e28b40d3') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.21'
|
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.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.core:core-ktx:1.10.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.fragment:fragment-ktx:1.5.7'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-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.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.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.preference:preference-ktx:1.2.0'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
@@ -115,8 +115,8 @@ dependencies {
|
|||||||
implementation 'androidx.room:room-ktx:2.5.1'
|
implementation 'androidx.room:room-ktx:2.5.1'
|
||||||
kapt 'androidx.room:room-compiler:2.5.1'
|
kapt 'androidx.room:room-compiler:2.5.1'
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||||
implementation 'com.squareup.okio:okio:3.3.0'
|
implementation 'com.squareup.okio:okio:3.3.0'
|
||||||
|
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||||
@@ -127,8 +127,8 @@ dependencies {
|
|||||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.3.0'
|
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.3.0'
|
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
@@ -136,21 +136,21 @@ dependencies {
|
|||||||
implementation 'ch.acra:acra-http:5.9.7'
|
implementation 'ch.acra:acra-http:5.9.7'
|
||||||
implementation 'ch.acra:acra-dialog: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 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20230227'
|
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:runner:1.5.2'
|
||||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
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 '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'
|
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
|
||||||
kaptAndroidTest 'com.google.dagger:hilt-android-compiler: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) {
|
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain("", null)
|
get() = ConfigKey.Domain()
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
override val sortOrders: Set<SortOrder>
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
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) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
|
if (recyclerView.hasPendingAdapterUpdates()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
|
val layoutManager = (recyclerView.layoutManager as? LinearLayoutManager) ?: return
|
||||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||||
if (firstVisibleItemPosition == RecyclerView.NO_POSITION) {
|
if (firstVisibleItemPosition == RecyclerView.NO_POSITION) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (firstVisibleItemPosition <= offsetTop) {
|
|
||||||
onScrolledToStart(recyclerView)
|
|
||||||
}
|
|
||||||
val visibleItemCount = layoutManager.childCount
|
val visibleItemCount = layoutManager.childCount
|
||||||
val totalItemCount = layoutManager.itemCount
|
val totalItemCount = layoutManager.itemCount
|
||||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) {
|
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - offsetBottom) {
|
||||||
onScrolledToEnd(recyclerView)
|
onScrolledToEnd(recyclerView)
|
||||||
}
|
}
|
||||||
|
if (firstVisibleItemPosition <= offsetTop) {
|
||||||
|
onScrolledToStart(recyclerView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onScrolledToStart(recyclerView: 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.base.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
|
||||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||||
|
import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@@ -24,7 +25,9 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
|
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import okhttp3.Cache
|
||||||
import okhttp3.CookieJar
|
import okhttp3.CookieJar
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
@@ -88,14 +89,19 @@ interface AppModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideOkHttpClient(
|
fun provideHttpCache(
|
||||||
localStorageManager: LocalStorageManager,
|
localStorageManager: LocalStorageManager,
|
||||||
|
): Cache = localStorageManager.createHttpCache()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOkHttpClient(
|
||||||
|
cache: Cache,
|
||||||
commonHeadersInterceptor: CommonHeadersInterceptor,
|
commonHeadersInterceptor: CommonHeadersInterceptor,
|
||||||
mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
mirrorSwitchInterceptor: MirrorSwitchInterceptor,
|
||||||
cookieJar: CookieJar,
|
cookieJar: CookieJar,
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
val cache = localStorageManager.createHttpCache()
|
|
||||||
return OkHttpClient.Builder().apply {
|
return OkHttpClient.Builder().apply {
|
||||||
connectTimeout(20, TimeUnit.SECONDS)
|
connectTimeout(20, TimeUnit.SECONDS)
|
||||||
readTimeout(60, TimeUnit.SECONDS)
|
readTimeout(60, TimeUnit.SECONDS)
|
||||||
@@ -106,6 +112,7 @@ interface AppModule {
|
|||||||
bypassSSLErrors()
|
bypassSSLErrors()
|
||||||
}
|
}
|
||||||
cache(cache)
|
cache(cache)
|
||||||
|
addNetworkInterceptor(CacheLimitInterceptor())
|
||||||
addInterceptor(GZipInterceptor())
|
addInterceptor(GZipInterceptor())
|
||||||
addInterceptor(commonHeadersInterceptor)
|
addInterceptor(commonHeadersInterceptor)
|
||||||
addInterceptor(CloudFlareInterceptor())
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
||||||
|
|
||||||
@@ -13,8 +14,8 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
|||||||
application.registerComponentCallbacks(this)
|
application.registerComponentCallbacks(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val detailsCache = DeferredLruCache<Manga>(4)
|
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
||||||
private val pagesCache = DeferredLruCache<List<MangaPage>>(4)
|
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
||||||
|
|
||||||
override val isCachingEnabled: Boolean = true
|
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>) {
|
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>? {
|
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>>) {
|
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
|
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||||
@@ -43,17 +44,17 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
|||||||
trimCache(pagesCache, level)
|
trimCache(pagesCache, level)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun trimCache(cache: DeferredLruCache<*>, level: Int) {
|
private fun trimCache(cache: ExpiringLruCache<*>, level: Int) {
|
||||||
when (level) {
|
when (level) {
|
||||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
|
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
|
||||||
ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
|
ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
|
||||||
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.evictAll()
|
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.clear()
|
||||||
|
|
||||||
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
|
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
|
||||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
|
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
|
||||||
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1)
|
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 androidx.annotation.WorkerThread
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||||
@@ -82,6 +85,15 @@ class FileLogger(
|
|||||||
flushImpl()
|
flushImpl()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
fun flushBlocking() {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runBlockingSafe { flushJob?.cancelAndJoin() }
|
||||||
|
runBlockingSafe { flushImpl() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun postFlush() {
|
private fun postFlush() {
|
||||||
if (flushJob?.isActive == true) {
|
if (flushJob?.isActive == true) {
|
||||||
return
|
return
|
||||||
@@ -96,10 +108,10 @@ class FileLogger(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun flushImpl() {
|
private suspend fun flushImpl() = withContext(NonCancellable) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
if (buffer.isEmpty()) {
|
if (buffer.isEmpty()) {
|
||||||
return
|
return@withContext
|
||||||
}
|
}
|
||||||
runInterruptible(Dispatchers.IO) {
|
runInterruptible(Dispatchers.IO) {
|
||||||
if (file.length() > MAX_SIZE_BYTES) {
|
if (file.length() > MAX_SIZE_BYTES) {
|
||||||
@@ -131,4 +143,9 @@ class FileLogger(
|
|||||||
}
|
}
|
||||||
bakFile.delete()
|
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 CONTENT_ENCODING = "Content-Encoding"
|
||||||
const val ACCEPT_ENCODING = "Accept-Encoding"
|
const val ACCEPT_ENCODING = "Accept-Encoding"
|
||||||
const val AUTHORIZATION = "Authorization"
|
const val AUTHORIZATION = "Authorization"
|
||||||
|
const val CACHE_CONTROL = "Cache-Control"
|
||||||
|
|
||||||
val CACHE_CONTROL_NO_STORE: CacheControl
|
val CACHE_CONTROL_NO_STORE: CacheControl
|
||||||
get() = CacheControl.Builder().noStore().build()
|
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_SOURCES_HIDDEN = "sources_hidden"
|
||||||
const val KEY_TRAFFIC_WARNING = "traffic_warning"
|
const val KEY_TRAFFIC_WARNING = "traffic_warning"
|
||||||
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
|
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_COOKIES_CLEAR = "cookies_clear"
|
||||||
const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear"
|
const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear"
|
||||||
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
|
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.sqlite.db.SupportSQLiteQuery
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.intellij.lang.annotations.Language
|
import org.intellij.lang.annotations.Language
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -71,12 +72,12 @@ abstract class FavouritesDao {
|
|||||||
)
|
)
|
||||||
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
||||||
|
|
||||||
suspend fun findCovers(categoryId: Long, order: SortOrder): List<String> {
|
suspend fun findCovers(categoryId: Long, order: SortOrder): List<Cover> {
|
||||||
val orderBy = getOrderBy(order)
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
@Language("RoomSql")
|
@Language("RoomSql")
|
||||||
val query = SimpleSQLiteQuery(
|
val query = SimpleSQLiteQuery(
|
||||||
"SELECT m.cover_url FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
|
"SELECT m.cover_url AS url, m.source AS source FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
|
||||||
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
|
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
|
||||||
arrayOf<Any>(categoryId),
|
arrayOf<Any>(categoryId),
|
||||||
)
|
)
|
||||||
@@ -145,7 +146,7 @@ abstract class FavouritesDao {
|
|||||||
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
||||||
|
|
||||||
@RawQuery
|
@RawQuery
|
||||||
protected abstract suspend fun findCoversImpl(query: SupportSQLiteQuery): List<String>
|
protected abstract suspend fun findCoversImpl(query: SupportSQLiteQuery): List<Cover>
|
||||||
|
|
||||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
|
||||||
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
|||||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||||
import org.koitharu.kotatsu.favourites.data.toManga
|
import org.koitharu.kotatsu.favourites.data.toManga
|
||||||
import org.koitharu.kotatsu.favourites.data.toMangaList
|
import org.koitharu.kotatsu.favourites.data.toMangaList
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||||
@@ -66,11 +67,11 @@ class FavouritesRepository @Inject constructor(
|
|||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<String>>> {
|
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<Cover>>> {
|
||||||
return db.favouriteCategoriesDao.observeAll()
|
return db.favouriteCategoriesDao.observeAll()
|
||||||
.map {
|
.map {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val res = LinkedHashMap<FavouriteCategory, List<String>>()
|
val res = LinkedHashMap<FavouriteCategory, List<Cover>>()
|
||||||
for (entity in it) {
|
for (entity in it) {
|
||||||
val cat = entity.toFavouriteCategory()
|
val cat = entity.toFavouriteCategory()
|
||||||
res[cat] = db.favouritesDao.findCovers(
|
res[cat] = db.favouritesDao.findCovers(
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ import org.koitharu.kotatsu.R
|
|||||||
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
|
|
||||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getAnimationDuration
|
||||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.source
|
||||||
|
|
||||||
fun categoryAD(
|
fun categoryAD(
|
||||||
coil: ImageLoader,
|
coil: ImageLoader,
|
||||||
@@ -53,10 +54,7 @@ fun categoryAD(
|
|||||||
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
|
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
|
||||||
val fallback = ColorDrawable(Color.TRANSPARENT)
|
val fallback = ColorDrawable(Color.TRANSPARENT)
|
||||||
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
|
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
|
||||||
val crossFadeDuration = (
|
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
|
||||||
context.resources.getInteger(R.integer.config_defaultAnimTime) *
|
|
||||||
context.animatorDurationScale
|
|
||||||
).toInt()
|
|
||||||
itemView.setOnClickListener(eventListener)
|
itemView.setOnClickListener(eventListener)
|
||||||
itemView.setOnLongClickListener(eventListener)
|
itemView.setOnLongClickListener(eventListener)
|
||||||
itemView.setOnTouchListener(eventListener)
|
itemView.setOnTouchListener(eventListener)
|
||||||
@@ -77,9 +75,11 @@ fun categoryAD(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
repeat(coverViews.size) { i ->
|
repeat(coverViews.size) { i ->
|
||||||
coverViews[i].newImageRequest(lifecycleOwner, item.covers.getOrNull(i))?.run {
|
val cover = item.covers.getOrNull(i)
|
||||||
|
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
|
||||||
placeholder(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(fallback)
|
fallback(fallback)
|
||||||
|
source(cover?.mangaSource)
|
||||||
crossfade(crossFadeDuration * (i + 1))
|
crossfade(crossFadeDuration * (i + 1))
|
||||||
error(R.drawable.ic_error_placeholder)
|
error(R.drawable.ic_error_placeholder)
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
class CategoryListModel(
|
class CategoryListModel(
|
||||||
val mangaCount: Int,
|
val mangaCount: Int,
|
||||||
val covers: List<String>,
|
val covers: List<Cover>,
|
||||||
val category: FavouriteCategory,
|
val category: FavouriteCategory,
|
||||||
val isReorderMode: Boolean,
|
val isReorderMode: Boolean,
|
||||||
) : ListModel {
|
) : ListModel {
|
||||||
@@ -21,9 +22,7 @@ class CategoryListModel(
|
|||||||
if (covers != other.covers) return false
|
if (covers != other.covers) return false
|
||||||
if (category.id != other.category.id) return false
|
if (category.id != other.category.id) return false
|
||||||
if (category.title != other.category.title) return false
|
if (category.title != other.category.title) return false
|
||||||
if (category.order != other.category.order) return false
|
return category.order == other.category.order
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -35,4 +34,4 @@ class CategoryListModel(
|
|||||||
result = 31 * result + category.order.hashCode()
|
result = 31 * result + category.order.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,10 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) {
|
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) {
|
||||||
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
||||||
try {
|
try {
|
||||||
file.sink(append = false).buffer().use {
|
val bytes = file.sink(append = false).buffer().use {
|
||||||
it.writeAllCancellable(source)
|
it.writeAllCancellable(source)
|
||||||
}
|
}
|
||||||
|
check(bytes != 0L) { "No data has been written" }
|
||||||
lruCache.get().put(url, file)
|
lruCache.get().put(url, file)
|
||||||
} finally {
|
} finally {
|
||||||
file.delete()
|
file.delete()
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ sealed class LocalMangaInput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@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
|
@JvmStatic
|
||||||
protected fun Manga.copy2(
|
protected fun Manga.copy2(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.reader.domain
|
package org.koitharu.kotatsu.reader.domain
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -8,6 +9,7 @@ import androidx.collection.LongSparseArray
|
|||||||
import androidx.collection.set
|
import androidx.collection.set
|
||||||
import dagger.hilt.android.ActivityRetainedLifecycle
|
import dagger.hilt.android.ActivityRetainedLifecycle
|
||||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
import dagger.hilt.android.scopes.ActivityRetainedScoped
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -30,8 +32,10 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
|
import org.koitharu.kotatsu.utils.FileSize
|
||||||
import org.koitharu.kotatsu.utils.RetainedLifecycleCoroutineScope
|
import org.koitharu.kotatsu.utils.RetainedLifecycleCoroutineScope
|
||||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.utils.ext.ramAvailable
|
||||||
import org.koitharu.kotatsu.utils.ext.withProgress
|
import org.koitharu.kotatsu.utils.ext.withProgress
|
||||||
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -44,9 +48,11 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
|
|
||||||
private const val PROGRESS_UNDEFINED = -1f
|
private const val PROGRESS_UNDEFINED = -1f
|
||||||
private const val PREFETCH_LIMIT_DEFAULT = 10
|
private const val PREFETCH_LIMIT_DEFAULT = 10
|
||||||
|
private const val PREFETCH_MIN_RAM_MB = 80L
|
||||||
|
|
||||||
@ActivityRetainedScoped
|
@ActivityRetainedScoped
|
||||||
class PageLoader @Inject constructor(
|
class PageLoader @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
lifecycle: ActivityRetainedLifecycle,
|
lifecycle: ActivityRetainedLifecycle,
|
||||||
private val okHttp: OkHttpClient,
|
private val okHttp: OkHttpClient,
|
||||||
private val cache: PagesCache,
|
private val cache: PagesCache,
|
||||||
@@ -75,7 +81,7 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isPrefetchApplicable(): Boolean {
|
fun isPrefetchApplicable(): Boolean {
|
||||||
return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled()
|
return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled() && !isLowRam()
|
||||||
}
|
}
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
@@ -116,6 +122,9 @@ class PageLoader @Inject constructor(
|
|||||||
|
|
||||||
suspend fun convertInPlace(file: File) {
|
suspend fun convertInPlace(file: File) {
|
||||||
convertLock.withLock {
|
convertLock.withLock {
|
||||||
|
if (context.ramAvailable < file.length() * 2) {
|
||||||
|
return@withLock
|
||||||
|
}
|
||||||
runInterruptible(Dispatchers.Default) {
|
runInterruptible(Dispatchers.Default) {
|
||||||
val image = BitmapFactory.decodeFile(file.absolutePath)
|
val image = BitmapFactory.decodeFile(file.absolutePath)
|
||||||
try {
|
try {
|
||||||
@@ -212,6 +221,10 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isLowRam(): Boolean {
|
||||||
|
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
||||||
|
}
|
||||||
|
|
||||||
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
private class InternalErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
|
||||||
CoroutineExceptionHandler {
|
CoroutineExceptionHandler {
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ class ReaderViewModel @Inject constructor(
|
|||||||
val prevJob = stateChangeJob
|
val prevJob = stateChangeJob
|
||||||
stateChangeJob = launchJob(Dispatchers.Default) {
|
stateChangeJob = launchJob(Dispatchers.Default) {
|
||||||
prevJob?.cancelAndJoin()
|
prevJob?.cancelAndJoin()
|
||||||
|
loadingJob?.join()
|
||||||
val pages = content.value?.pages ?: return@launchJob
|
val pages = content.value?.pages ?: return@launchJob
|
||||||
pages.getOrNull(position)?.let { page ->
|
pages.getOrNull(position)?.let { page ->
|
||||||
currentState.update { cs ->
|
currentState.update { cs ->
|
||||||
@@ -263,12 +264,12 @@ class ReaderViewModel @Inject constructor(
|
|||||||
return@launchJob
|
return@launchJob
|
||||||
}
|
}
|
||||||
ensureActive()
|
ensureActive()
|
||||||
if (position <= BOUNDS_PAGE_OFFSET) {
|
|
||||||
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
|
||||||
}
|
|
||||||
if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
if (position >= pages.lastIndex - BOUNDS_PAGE_OFFSET) {
|
||||||
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
loadPrevNextChapter(pages.last().chapterId, isNext = true)
|
||||||
}
|
}
|
||||||
|
if (position <= BOUNDS_PAGE_OFFSET) {
|
||||||
|
loadPrevNextChapter(pages.first().chapterId, isNext = false)
|
||||||
|
}
|
||||||
if (pageLoader.isPrefetchApplicable()) {
|
if (pageLoader.isPrefetchApplicable()) {
|
||||||
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
|
pageLoader.prefetch(pages.trySublist(position + 1, position + PREFETCH_LIMIT))
|
||||||
}
|
}
|
||||||
@@ -348,7 +349,9 @@ class ReaderViewModel @Inject constructor(
|
|||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) {
|
private fun loadPrevNextChapter(currentId: Long, isNext: Boolean) {
|
||||||
|
val prevJob = loadingJob
|
||||||
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
loadingJob = launchLoadingJob(Dispatchers.Default) {
|
||||||
|
prevJob?.join()
|
||||||
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
|
chaptersLoader.loadPrevNextChapter(mangaData.requireValue(), currentId, isNext)
|
||||||
content.emitValue(ReaderContent(chaptersLoader.snapshot(), null))
|
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 com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import okhttp3.Cache
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
@@ -39,6 +42,9 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var cookieJar: MutableCookieJar
|
lateinit var cookieJar: MutableCookieJar
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var cache: Cache
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||||
|
|
||||||
@@ -52,6 +58,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.PAGES)
|
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.PAGES)
|
||||||
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindSummaryToCacheSize(CacheDir.THUMBS)
|
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 ->
|
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
||||||
viewLifecycleScope.launch {
|
viewLifecycleScope.launch {
|
||||||
lifecycle.awaitStateAtLeast(Lifecycle.State.RESUMED)
|
lifecycle.awaitStateAtLeast(Lifecycle.State.RESUMED)
|
||||||
@@ -90,6 +97,11 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppSettings.KEY_HTTP_CACHE_CLEAR -> {
|
||||||
|
clearHttpCache()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
||||||
viewLifecycleScope.launch {
|
viewLifecycleScope.launch {
|
||||||
trackerRepo.clearLogs()
|
trackerRepo.clearLogs()
|
||||||
@@ -131,6 +143,32 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
summary = FileSize.BYTES.format(context, size)
|
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) {
|
private fun clearSearchHistory(preference: Preference) {
|
||||||
MaterialAlertDialogBuilder(context ?: return)
|
MaterialAlertDialogBuilder(context ?: return)
|
||||||
.setTitle(R.string.clear_search_history)
|
.setTitle(R.string.clear_search_history)
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
|
|||||||
val preference: Preference = when (key) {
|
val preference: Preference = when (key) {
|
||||||
is ConfigKey.Domain -> {
|
is ConfigKey.Domain -> {
|
||||||
val presetValues = key.presetValues
|
val presetValues = key.presetValues
|
||||||
if (presetValues.isNullOrEmpty()) {
|
if (presetValues.size <= 1) {
|
||||||
EditTextPreference(requireContext())
|
EditTextPreference(requireContext())
|
||||||
} else {
|
} else {
|
||||||
AutoCompleteTextViewPreference(requireContext()).apply { entries = presetValues }
|
AutoCompleteTextViewPreference(requireContext()).apply {
|
||||||
|
entries = presetValues.toStringArray()
|
||||||
|
}
|
||||||
}.apply {
|
}.apply {
|
||||||
summaryProvider = EditTextDefaultSummaryProvider(key.defaultValue)
|
summaryProvider = EditTextDefaultSummaryProvider(key.defaultValue)
|
||||||
setOnBindEditTextListener(
|
setOnBindEditTextListener(
|
||||||
@@ -44,7 +46,7 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
|
|||||||
EditTextBindListener(
|
EditTextBindListener(
|
||||||
inputType = EditorInfo.TYPE_CLASS_TEXT,
|
inputType = EditorInfo.TYPE_CLASS_TEXT,
|
||||||
hint = key.defaultValue,
|
hint = key.defaultValue,
|
||||||
validator = null,
|
validator = HeaderValidator(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
setTitle(R.string.user_agent)
|
setTitle(R.string.user_agent)
|
||||||
@@ -64,3 +66,7 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
|
|||||||
screen.addPreference(preference)
|
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.MangaParserAuthProvider
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
import org.koitharu.kotatsu.utils.TaggedActivityResult
|
||||||
|
import org.koitharu.kotatsu.utils.ext.catchingWebViewUnavailability
|
||||||
import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat
|
import org.koitharu.kotatsu.utils.ext.getSerializableExtraCompat
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.google.android.material.R as materialR
|
import com.google.android.material.R as materialR
|
||||||
@@ -41,7 +42,9 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
|
|||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
|
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val source = intent?.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource
|
val source = intent?.getSerializableExtraCompat(EXTRA_SOURCE) as? MangaSource
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
finishAfterTransition()
|
finishAfterTransition()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread
|
|||||||
import androidx.core.content.contentValuesOf
|
import androidx.core.content.contentValuesOf
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koitharu.kotatsu.R
|
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
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
|
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
|
||||||
import org.koitharu.kotatsu.core.db.TABLE_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.network.GZipInterceptor
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
|
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
|
||||||
import org.koitharu.kotatsu.sync.data.SyncAuthApi
|
import org.koitharu.kotatsu.sync.data.SyncAuthApi
|
||||||
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
|
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
|
||||||
@@ -61,6 +64,7 @@ class SyncHelper(
|
|||||||
}
|
}
|
||||||
private val defaultGcPeriod: Long // gc period if sync enabled
|
private val defaultGcPeriod: Long // gc period if sync enabled
|
||||||
get() = TimeUnit.DAYS.toMillis(4)
|
get() = TimeUnit.DAYS.toMillis(4)
|
||||||
|
private val logger = LoggersModule.provideSyncLogger(context, AppSettings(context))
|
||||||
|
|
||||||
fun syncFavourites(syncResult: SyncResult) {
|
fun syncFavourites(syncResult: SyncResult) {
|
||||||
val data = JSONObject()
|
val data = JSONObject()
|
||||||
@@ -71,7 +75,7 @@ class SyncHelper(
|
|||||||
.url("$baseUrl/resource/$TABLE_FAVOURITES")
|
.url("$baseUrl/resource/$TABLE_FAVOURITES")
|
||||||
.post(data.toRequestBody())
|
.post(data.toRequestBody())
|
||||||
.build()
|
.build()
|
||||||
val response = httpClient.newCall(request).execute().parseJsonOrNull()
|
val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
val timestamp = response.getLong(FIELD_TIMESTAMP)
|
val timestamp = response.getLong(FIELD_TIMESTAMP)
|
||||||
val categoriesResult =
|
val categoriesResult =
|
||||||
@@ -93,7 +97,7 @@ class SyncHelper(
|
|||||||
.url("$baseUrl/resource/$TABLE_HISTORY")
|
.url("$baseUrl/resource/$TABLE_HISTORY")
|
||||||
.post(data.toRequestBody())
|
.post(data.toRequestBody())
|
||||||
.build()
|
.build()
|
||||||
val response = httpClient.newCall(request).execute().parseJsonOrNull()
|
val response = httpClient.newCall(request).execute().log().parseJsonOrNull()
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
val result = upsertHistory(
|
val result = upsertHistory(
|
||||||
json = response.getJSONArray(TABLE_HISTORY),
|
json = response.getJSONArray(TABLE_HISTORY),
|
||||||
@@ -105,6 +109,19 @@ class SyncHelper(
|
|||||||
gcHistory()
|
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> {
|
private fun upsertHistory(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
|
||||||
val uri = uri(authorityHistory, TABLE_HISTORY)
|
val uri = uri(authorityHistory, TABLE_HISTORY)
|
||||||
val operations = ArrayList<ContentProviderOperation>()
|
val operations = ArrayList<ContentProviderOperation>()
|
||||||
@@ -298,4 +315,10 @@ class SyncHelper(
|
|||||||
private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject
|
private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject
|
||||||
|
|
||||||
private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray
|
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.groupLogin.isVisible = false
|
||||||
binding.groupPassword.isVisible = true
|
binding.groupPassword.isVisible = true
|
||||||
pageBackCallback.update()
|
pageBackCallback.update()
|
||||||
|
binding.editPassword.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.button_back -> {
|
R.id.button_back -> {
|
||||||
binding.groupPassword.isVisible = false
|
binding.groupPassword.isVisible = false
|
||||||
binding.groupLogin.isVisible = true
|
binding.groupLogin.isVisible = true
|
||||||
pageBackCallback.update()
|
pageBackCallback.update()
|
||||||
|
binding.editEmail.requestFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.button_done -> {
|
R.id.button_done -> {
|
||||||
@@ -200,6 +202,7 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
|
|||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
binding.groupLogin.isVisible = true
|
binding.groupLogin.isVisible = true
|
||||||
binding.groupPassword.isVisible = false
|
binding.groupPassword.isVisible = false
|
||||||
|
binding.editEmail.requestFocus()
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import dagger.hilt.InstallIn
|
|||||||
import dagger.hilt.android.EntryPointAccessors
|
import dagger.hilt.android.EntryPointAccessors
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import org.koitharu.kotatsu.core.db.*
|
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
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
abstract class SyncProvider : ContentProvider() {
|
abstract class SyncProvider : ContentProvider() {
|
||||||
@@ -24,7 +22,6 @@ abstract class SyncProvider : ContentProvider() {
|
|||||||
EntryPointAccessors.fromApplication(checkNotNull(context), SyncProviderEntryPoint::class.java)
|
EntryPointAccessors.fromApplication(checkNotNull(context), SyncProviderEntryPoint::class.java)
|
||||||
}
|
}
|
||||||
private val database by lazy { entryPoint.database }
|
private val database by lazy { entryPoint.database }
|
||||||
private val logger by lazy { entryPoint.logger }
|
|
||||||
|
|
||||||
private val supportedTables = setOf(
|
private val supportedTables = setOf(
|
||||||
TABLE_FAVOURITES,
|
TABLE_FAVOURITES,
|
||||||
@@ -52,7 +49,6 @@ abstract class SyncProvider : ContentProvider() {
|
|||||||
.selection(selection, selectionArgs)
|
.selection(selection, selectionArgs)
|
||||||
.orderBy(sortOrder)
|
.orderBy(sortOrder)
|
||||||
.create()
|
.create()
|
||||||
logger.log("query: ${sqlQuery.sql} (${selectionArgs.contentToString()})")
|
|
||||||
return database.openHelper.readableDatabase.query(sqlQuery)
|
return database.openHelper.readableDatabase.query(sqlQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +61,6 @@ abstract class SyncProvider : ContentProvider() {
|
|||||||
if (values == null || table == null) {
|
if (values == null || table == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
logger.log { "insert: $table [$values]" }
|
|
||||||
val db = database.openHelper.writableDatabase
|
val db = database.openHelper.writableDatabase
|
||||||
if (db.insert(table, SQLiteDatabase.CONFLICT_IGNORE, values) < 0) {
|
if (db.insert(table, SQLiteDatabase.CONFLICT_IGNORE, values) < 0) {
|
||||||
db.update(table, values)
|
db.update(table, values)
|
||||||
@@ -75,7 +70,6 @@ abstract class SyncProvider : ContentProvider() {
|
|||||||
|
|
||||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
|
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||||
val table = getTableName(uri) ?: return 0
|
val table = getTableName(uri) ?: return 0
|
||||||
logger.log { "delete: $table ($selection) : (${selectionArgs.contentToString()})" }
|
|
||||||
return database.openHelper.writableDatabase.delete(table, selection, selectionArgs)
|
return database.openHelper.writableDatabase.delete(table, selection, selectionArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +78,6 @@ abstract class SyncProvider : ContentProvider() {
|
|||||||
if (values == null || table == null) {
|
if (values == null || table == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
logger.log { "update: $table ($selection) : (${selectionArgs.contentToString()}) [$values]" }
|
|
||||||
return database.openHelper.writableDatabase
|
return database.openHelper.writableDatabase
|
||||||
.update(table, SQLiteDatabase.CONFLICT_IGNORE, values, selection, selectionArgs)
|
.update(table, SQLiteDatabase.CONFLICT_IGNORE, values, selection, selectionArgs)
|
||||||
}
|
}
|
||||||
@@ -127,8 +120,5 @@ abstract class SyncProvider : ContentProvider() {
|
|||||||
interface SyncProviderEntryPoint {
|
interface SyncProviderEntryPoint {
|
||||||
|
|
||||||
val database: MangaDatabase
|
val database: MangaDatabase
|
||||||
|
|
||||||
@get:SyncLogger
|
|
||||||
val logger: FileLogger
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
|
|||||||
runCatchingCancellable {
|
runCatchingCancellable {
|
||||||
syncHelper.syncFavourites(syncResult)
|
syncHelper.syncFavourites(syncResult)
|
||||||
SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
|
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 {
|
runCatchingCancellable {
|
||||||
syncHelper.syncHistory(syncResult)
|
syncHelper.syncHistory(syncResult)
|
||||||
SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
|
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.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
@@ -84,6 +85,7 @@ class ShareHelper(private val context: Context) {
|
|||||||
fun shareLogs(loggers: Collection<FileLogger>) {
|
fun shareLogs(loggers: Collection<FileLogger>) {
|
||||||
val intentBuilder = ShareCompat.IntentBuilder(context)
|
val intentBuilder = ShareCompat.IntentBuilder(context)
|
||||||
.setType(TYPE_TEXT)
|
.setType(TYPE_TEXT)
|
||||||
|
var hasLogs = false
|
||||||
for (logger in loggers) {
|
for (logger in loggers) {
|
||||||
val logFile = logger.file
|
val logFile = logger.file
|
||||||
if (!logFile.exists()) {
|
if (!logFile.exists()) {
|
||||||
@@ -91,8 +93,13 @@ class ShareHelper(private val context: Context) {
|
|||||||
}
|
}
|
||||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", logFile)
|
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", logFile)
|
||||||
intentBuilder.addStream(uri)
|
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.utils.ext
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
|
import android.app.ActivityManager.MemoryInfo
|
||||||
import android.app.ActivityOptions
|
import android.app.ActivityOptions
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.ACTIVITY_SERVICE
|
import android.content.Context.ACTIVITY_SERVICE
|
||||||
@@ -19,6 +20,7 @@ import android.provider.Settings
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewPropertyAnimator
|
import android.view.ViewPropertyAnimator
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.IntegerRes
|
import androidx.annotation.IntegerRes
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
@@ -137,6 +139,13 @@ fun isLowRamDevice(context: Context): Boolean {
|
|||||||
return context.activityManager?.isLowRamDevice ?: false
|
return context.activityManager?.isLowRamDevice ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Context.ramAvailable: Long
|
||||||
|
get() {
|
||||||
|
val result = MemoryInfo()
|
||||||
|
activityManager?.getMemoryInfo(result)
|
||||||
|
return result.availMem
|
||||||
|
}
|
||||||
|
|
||||||
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
|
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
|
||||||
view,
|
view,
|
||||||
0,
|
0,
|
||||||
@@ -170,3 +179,18 @@ fun Context.findActivity(): Activity? = when (this) {
|
|||||||
is ContextWrapper -> baseContext.findActivity()
|
is ContextWrapper -> baseContext.findActivity()
|
||||||
else -> null
|
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.ActivityNotFoundException
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.util.AndroidRuntimeException
|
||||||
import androidx.collection.arraySetOf
|
import androidx.collection.arraySetOf
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import okio.FileNotFoundException
|
import okio.FileNotFoundException
|
||||||
@@ -94,3 +95,8 @@ inline fun <R> runCatchingCancellable(block: () -> R): Result<R> {
|
|||||||
Result.failure(e)
|
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 {
|
fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
|
||||||
if (visibility != View.VISIBLE) {
|
if (visibility != View.VISIBLE) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.koitharu.kotatsu.favourites.domain.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class Cover(
|
||||||
|
val url: String,
|
||||||
|
val source: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val mangaSource: MangaSource?
|
||||||
|
get() = if (source.isEmpty()) null else MangaSource.values().find { it.name == source }
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Cover
|
||||||
|
|
||||||
|
if (url != other.url) return false
|
||||||
|
return source == other.source
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = url.hashCode()
|
||||||
|
result = 31 * result + source.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Cover(url='$url', source=$source)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -410,4 +410,6 @@
|
|||||||
<string name="downloads_paused">Спампоўкі прыпыненыя</string>
|
<string name="downloads_paused">Спампоўкі прыпыненыя</string>
|
||||||
<string name="downloads_removed">Спампоўкі выдалены</string>
|
<string name="downloads_removed">Спампоўкі выдалены</string>
|
||||||
<string name="downloads_cancelled">Спампоўкі былі адменены</string>
|
<string name="downloads_cancelled">Спампоўкі былі адменены</string>
|
||||||
|
<string name="translations">Пераклады</string>
|
||||||
|
<string name="web_view_unavailable">WebView недаступны: праверце, ці ўсталяваны пастаўшчык WebView</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
<string name="zoom_mode_keep_start">Am Anfang ausrichten</string>
|
<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_width">An Breite anpassen</string>
|
||||||
<string name="zoom_mode_fit_height">An Höhe 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="black_dark_theme">Reines Schwarz</string>
|
||||||
<string name="right_to_left">Von rechts nach links</string>
|
<string name="right_to_left">Von rechts nach links</string>
|
||||||
<string name="create_category">Neue Kategorie</string>
|
<string name="create_category">Neue Kategorie</string>
|
||||||
@@ -91,17 +91,17 @@
|
|||||||
<string name="confirm">Bestätigen</string>
|
<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="protect_application_subtitle">Gib ein Passwort ein, mit dem die App gestartet werden soll</string>
|
||||||
<string name="next">Weiter</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="read_more">Mehr erfahren</string>
|
||||||
<string name="tracker_warning">Einige Geräte haben ein anderes Systemverhalten, welches womöglich Hintergrundprozesse unterbricht.</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="welcome">Willkommen</string>
|
||||||
<string name="remove_category">Entfernen</string>
|
<string name="remove_category">Entfernen</string>
|
||||||
<string name="rotate_screen">Bildschirm drehen</string>
|
<string name="rotate_screen">Bildschirm drehen</string>
|
||||||
<string name="size_s">Größe: %s</string>
|
<string name="size_s">Größe: %s</string>
|
||||||
<string name="new_version_s">Neue Version: %s</string>
|
<string name="new_version_s">Neue Version: %s</string>
|
||||||
<string name="search_results">Suchergebnisse</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="read_later">Später lesen</string>
|
||||||
<string name="favourites_category_empty">Diese Kategorie ist leer</string>
|
<string name="favourites_category_empty">Diese Kategorie ist leer</string>
|
||||||
<string name="all_favourites">Alle Favoriten</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_secondary">Speicher Manga aus Online-Quellen oder importiere Dateien.</string>
|
||||||
<string name="text_local_holder_primary">Speichere erst etwas</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_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="favourites_categories">Favoriten-Kategorien</string>
|
||||||
<string name="vibration">Vibration</string>
|
<string name="vibration">Vibration</string>
|
||||||
<string name="light_indicator">LED-Anzeige</string>
|
<string name="light_indicator">LED-Anzeige</string>
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
<string name="open_in_browser">Im Browser öffnen</string>
|
<string name="open_in_browser">Im Browser öffnen</string>
|
||||||
<string name="external_storage">Externer Speicher</string>
|
<string name="external_storage">Externer Speicher</string>
|
||||||
<string name="internal_storage">Interner 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_search_history">Suchverlauf löschen</string>
|
||||||
<string name="clear_thumbs_cache">Miniaturansichten Cache löschen</string>
|
<string name="clear_thumbs_cache">Miniaturansichten Cache löschen</string>
|
||||||
<string name="error">Fehler</string>
|
<string name="error">Fehler</string>
|
||||||
@@ -158,14 +158,14 @@
|
|||||||
<string name="clear">Löschen</string>
|
<string name="clear">Löschen</string>
|
||||||
<string name="taps_on_edges">Rand antippen</string>
|
<string name="taps_on_edges">Rand antippen</string>
|
||||||
<string name="captcha_solve">Lösen</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="silent">Stumm</string>
|
||||||
<string name="reader_mode_hint">Die gewählte Konfiguration wird für diesen Manga gespeichert</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="tap_to_try_again">Tippe, um es erneut zu versuchen</string>
|
||||||
<string name="today">Heute</string>
|
<string name="today">Heute</string>
|
||||||
<string name="long_ago">Vor langer Zeit</string>
|
<string name="long_ago">Vor langer Zeit</string>
|
||||||
<string name="yesterday">Gestern</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_with_errors">Die Daten wurden wiederhergestellt, aber es gibt Fehler</string>
|
||||||
<string name="data_restored_success">Alle Daten wiederhergestellt</string>
|
<string name="data_restored_success">Alle Daten wiederhergestellt</string>
|
||||||
<string name="zoom_mode_fit_center">An Zentrum anpassen</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="dont_check">Nicht prüfen</string>
|
||||||
<string name="domain">Domäne</string>
|
<string name="domain">Domäne</string>
|
||||||
<string name="gestures_only">Nur Gesten</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="chapter_is_missing_text">Lade dieses fehlende Kapitel herunter oder lese es online.</string>
|
||||||
<string name="queued">In Warteschlange</string>
|
<string name="queued">In Warteschlange</string>
|
||||||
<string name="about_app_translation">Übersetzung</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="text_clear_cookies_prompt">Du wirst von allen Quellen abgemeldet</string>
|
||||||
<string name="genres">Genres</string>
|
<string name="genres">Genres</string>
|
||||||
<string name="auth_not_supported_by">Anmeldung bei %s wird nicht unterstützt</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="reorder">Neu anordnen</string>
|
||||||
<string name="empty">Leer</string>
|
<string name="empty">Leer</string>
|
||||||
<string name="explore">Erkunden</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="exit_confirmation_summary">Drücke zweimal Zurück, um die App zu beenden</string>
|
||||||
<string name="feed">Feed</string>
|
<string name="feed">Feed</string>
|
||||||
<string name="incognito_mode">Inkognito-Modus</string>
|
<string name="incognito_mode">Inkognito-Modus</string>
|
||||||
@@ -398,16 +398,18 @@
|
|||||||
<string name="remove_completed">Entfernung abgeschlossen</string>
|
<string name="remove_completed">Entfernung abgeschlossen</string>
|
||||||
<string name="cancel_all">Alle abbrechen</string>
|
<string name="cancel_all">Alle abbrechen</string>
|
||||||
<string name="downloads_wifi_only">Nur über Wi-Fi herunterladen</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="suggestions_notifications_summary">Zeige ab und zu Benachrichtigungen mit Manga-Vorschlägen</string>
|
||||||
<string name="downloads_removed">Downloads wurden entfernt</string>
|
<string name="downloads_removed">Downloads wurden entfernt</string>
|
||||||
<string name="suggestion_manga">Vorschlag: %s</string>
|
<string name="suggestion_manga">Vorschlag: %s</string>
|
||||||
<string name="more">Mehr</string>
|
<string name="more">Mehr</string>
|
||||||
<string name="cancel_all_downloads_confirm">Alle Downloads werden abgebrochen, teilweise heruntergeladene Dateien gehen verloren</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="text_downloads_list_holder">Du hast keine Downloads</string>
|
||||||
<string name="downloads_resumed">Downloads wurden fortgesetzt</string>
|
<string name="downloads_resumed">Downloads wurden fortgesetzt</string>
|
||||||
<string name="downloads_paused">Downloads wurden pausiert</string>
|
<string name="downloads_paused">Downloads wurden pausiert</string>
|
||||||
<string name="downloads_cancelled">Downloads wurden abgebrochen</string>
|
<string name="downloads_cancelled">Downloads wurden abgebrochen</string>
|
||||||
<string name="suggestions_enable_prompt">Willst du personalisierte Manga-Vorschläge erhalten\?</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>
|
</resources>
|
||||||
@@ -410,4 +410,6 @@
|
|||||||
<string name="downloads_removed">Las descargas se han eliminado</string>
|
<string name="downloads_removed">Las descargas se han eliminado</string>
|
||||||
<string name="downloads_cancelled">Las descargas se han cancelado</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="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>
|
</resources>
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
<string name="tracking">Pelacakan</string>
|
<string name="tracking">Pelacakan</string>
|
||||||
<string name="logout">Keluar</string>
|
<string name="logout">Keluar</string>
|
||||||
<string name="sync">Sinkronisasi</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_reading">Dibaca</string>
|
||||||
<string name="status_on_hold">Ditunda</string>
|
<string name="status_on_hold">Ditunda</string>
|
||||||
<string name="invalid_domain_message">Domain tidak valid</string>
|
<string name="invalid_domain_message">Domain tidak valid</string>
|
||||||
|
|||||||
@@ -4,4 +4,11 @@
|
|||||||
<string name="details">ଵିଵରଣୀ</string>
|
<string name="details">ଵିଵରଣୀ</string>
|
||||||
<string name="settings">ସେଟିଂ</string>
|
<string name="settings">ସେଟିଂ</string>
|
||||||
<string name="suggestion_manga">ପରାମର୍ଶ: %s</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>
|
</resources>
|
||||||
@@ -410,4 +410,6 @@
|
|||||||
<string name="suggestions_notifications_summary">Иногда показывать уведомления с рекомендуемой мангой</string>
|
<string name="suggestions_notifications_summary">Иногда показывать уведомления с рекомендуемой мангой</string>
|
||||||
<string name="remove_completed_downloads_confirm">Ваша история загрузок будет удалена</string>
|
<string name="remove_completed_downloads_confirm">Ваша история загрузок будет удалена</string>
|
||||||
<string name="suggestions_enable_prompt">Хотите ли Вы получать персонализированные рекомендации манги\?</string>
|
<string name="suggestions_enable_prompt">Хотите ли Вы получать персонализированные рекомендации манги\?</string>
|
||||||
|
<string name="translations">Переводы</string>
|
||||||
|
<string name="web_view_unavailable">WebView недоступен: проверьте, установлен ли провайдер WebView</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -410,4 +410,6 @@
|
|||||||
<string name="downloads_removed">Завантаження видалено</string>
|
<string name="downloads_removed">Завантаження видалено</string>
|
||||||
<string name="downloads_cancelled">Завантаження скасовано</string>
|
<string name="downloads_cancelled">Завантаження скасовано</string>
|
||||||
<string name="suggestions_enable_prompt">Хочете отримувати персоналізовані пропозиції щодо манги\?</string>
|
<string name="suggestions_enable_prompt">Хочете отримувати персоналізовані пропозиції щодо манги\?</string>
|
||||||
|
<string name="web_view_unavailable">WebView недоступний: перевірте, чи встановлено провайдер WebView</string>
|
||||||
|
<string name="translations">Переклади</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -415,4 +415,7 @@
|
|||||||
<string name="downloads_cancelled">Downloads have been cancelled</string>
|
<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="suggestions_enable_prompt">Do you want to receive personalized manga suggestions?</string>
|
||||||
<string name="translations">Translations</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>
|
</resources>
|
||||||
|
|||||||
@@ -39,6 +39,12 @@
|
|||||||
android:summary="@string/computing_"
|
android:summary="@string/computing_"
|
||||||
android:title="@string/clear_pages_cache" />
|
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
|
<Preference
|
||||||
android:key="cookies_clear"
|
android:key="cookies_clear"
|
||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import java.util.EnumSet
|
|||||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain("localhost", null)
|
get() = ConfigKey.Domain("localhost")
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
override val sortOrders: Set<SortOrder>
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
get() = EnumSet.allOf(SortOrder::class.java)
|
||||||
|
|||||||
Reference in New Issue
Block a user