Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed56170809 | ||
|
|
a36e5fce29 | ||
|
|
760bfaf4d7 | ||
|
|
24463720b1 | ||
|
|
516470e8ae | ||
|
|
7f530d0476 | ||
|
|
8a2706d70b | ||
|
|
27f09480a0 | ||
|
|
c03dcf6d2e | ||
|
|
bdb2ae9c2f | ||
|
|
3413fe6943 | ||
|
|
e6ae9e8bd6 | ||
|
|
8d9426f257 | ||
|
|
30247e3def | ||
|
|
084dc32d2d | ||
|
|
3393f1397b | ||
|
|
61784bcfc4 | ||
|
|
bd692fc60c | ||
|
|
738299e8d3 | ||
|
|
c8b6dc27b2 | ||
|
|
1493aa39a3 | ||
|
|
f115031846 | ||
|
|
571b85dfd8 | ||
|
|
75cc9e9030 | ||
|
|
656a707b4c | ||
|
|
04afe7a934 | ||
|
|
689670b3ff | ||
|
|
6273a9decb | ||
|
|
72336d4f71 | ||
|
|
731e998eb2 | ||
|
|
9bf53114de | ||
|
|
0e1b8db688 | ||
|
|
3a62e2e6c0 |
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@@ -4,6 +4,6 @@
|
||||
<option name="jvmTarget" value="1.8" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.7.20" />
|
||||
<option name="version" value="1.8.0" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -15,8 +15,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 507
|
||||
versionName '4.1'
|
||||
versionCode 509
|
||||
versionName '4.2'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -83,10 +83,11 @@ afterEvaluate {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:9ee1c21a67') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:cf00732023') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
@@ -113,7 +114,7 @@ dependencies {
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
||||
implementation 'com.squareup.okio:okio:3.2.0'
|
||||
implementation 'com.squareup.okio:okio:3.3.0'
|
||||
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
|
||||
@@ -125,7 +126,7 @@ dependencies {
|
||||
|
||||
implementation 'io.coil-kt:coil-base:2.2.2'
|
||||
implementation 'io.coil-kt:coil-svg:2.2.2'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:95e6c188c6'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
|
||||
implementation 'ch.acra:acra-http:5.9.7'
|
||||
@@ -137,10 +138,10 @@ dependencies {
|
||||
testImplementation 'org.json:json:20220924'
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.5.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.4'
|
||||
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
|
||||
|
||||
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
|
||||
|
||||
|
||||
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@@ -14,3 +14,4 @@
|
||||
|
||||
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
|
||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
||||
|
||||
@@ -188,6 +188,9 @@
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_history" />
|
||||
</service>
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.details.service.MangaPrefetchService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||
@@ -248,6 +251,9 @@
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="com.samsung.android.icon_container.has_icon_container"
|
||||
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
|
||||
|
||||
</application>
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ abstract class CoroutineIntentService : BaseService() {
|
||||
processIntent(startId, intent)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
onError(startId, e)
|
||||
} finally {
|
||||
stopSelf(startId)
|
||||
}
|
||||
|
||||
@@ -516,6 +516,6 @@ class FastScroller @JvmOverloads constructor(
|
||||
|
||||
interface SectionIndexer {
|
||||
|
||||
fun getSectionText(context: Context, position: Int): CharSequence
|
||||
fun getSectionText(context: Context, position: Int): CharSequence?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import android.graphics.Bitmap
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
|
||||
private const val CF_CLEARANCE = "cf_clearance"
|
||||
|
||||
class CloudFlareClient(
|
||||
private val cookieJar: AndroidCookieJar,
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val callback: CloudFlareCallback,
|
||||
private val targetUrl: String,
|
||||
) : WebViewClient() {
|
||||
@@ -42,4 +42,4 @@ class CloudFlareClient(
|
||||
return cookieJar.loadForRequest(targetUrl.toHttpUrl())
|
||||
.find { it.name == CF_CLEARANCE }?.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import androidx.core.view.isInvisible
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
||||
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
|
||||
import org.koitharu.kotatsu.utils.ext.stringArgument
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), CloudFlareCallback {
|
||||
@@ -27,7 +27,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
|
||||
private val pendingResult = Bundle(1)
|
||||
|
||||
@Inject
|
||||
lateinit var cookieJar: AndroidCookieJar
|
||||
lateinit var cookieJar: MutableCookieJar
|
||||
|
||||
override fun onInflateView(
|
||||
inflater: LayoutInflater,
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import android.provider.SearchRecentSuggestions
|
||||
import android.text.Html
|
||||
import android.util.AndroidRuntimeException
|
||||
import androidx.collection.arraySetOf
|
||||
import androidx.room.InvalidationTracker
|
||||
import coil.ComponentRegistry
|
||||
@@ -23,8 +24,14 @@ import okhttp3.CookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||
import org.koitharu.kotatsu.core.cache.StubContentCache
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.network.*
|
||||
import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
|
||||
@@ -40,6 +47,7 @@ import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
|
||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||
import org.koitharu.kotatsu.utils.IncognitoModeIndicator
|
||||
import org.koitharu.kotatsu.utils.ext.activityManager
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.utils.image.CoilImageGetter
|
||||
@@ -52,7 +60,7 @@ import javax.inject.Singleton
|
||||
interface AppModule {
|
||||
|
||||
@Binds
|
||||
fun bindCookieJar(androidCookieJar: AndroidCookieJar): CookieJar
|
||||
fun bindCookieJar(androidCookieJar: MutableCookieJar): CookieJar
|
||||
|
||||
@Binds
|
||||
fun bindMangaLoaderContext(mangaLoaderContextImpl: MangaLoaderContextImpl): MangaLoaderContext
|
||||
@@ -62,6 +70,17 @@ interface AppModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCookieJar(
|
||||
@ApplicationContext context: Context
|
||||
): MutableCookieJar = try {
|
||||
AndroidCookieJar()
|
||||
} catch (e: AndroidRuntimeException) {
|
||||
// WebView is not available
|
||||
PreferencesCookieJar(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(
|
||||
@@ -167,5 +186,17 @@ interface AppModule {
|
||||
activityRecreationHandle,
|
||||
incognitoModeIndicator,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideContentCache(
|
||||
application: Application,
|
||||
): ContentCache {
|
||||
return if (application.activityManager?.isLowRamDevice == true) {
|
||||
StubContentCache()
|
||||
} else {
|
||||
MemoryContentCache(application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
23
app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt
vendored
Normal file
23
app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
interface ContentCache {
|
||||
|
||||
val isCachingEnabled: Boolean
|
||||
|
||||
suspend fun getDetails(source: MangaSource, url: String): Manga?
|
||||
|
||||
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>)
|
||||
|
||||
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
|
||||
|
||||
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
|
||||
|
||||
data class Key(
|
||||
val source: MangaSource,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
16
app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt
vendored
Normal file
16
app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import androidx.collection.LruCache
|
||||
|
||||
class DeferredLruCache<T>(maxSize: Int) : LruCache<ContentCache.Key, SafeDeferred<T>>(maxSize) {
|
||||
|
||||
override fun entryRemoved(
|
||||
evicted: Boolean,
|
||||
key: ContentCache.Key,
|
||||
oldValue: SafeDeferred<T>,
|
||||
newValue: SafeDeferred<T>?,
|
||||
) {
|
||||
super.entryRemoved(evicted, key, oldValue, newValue)
|
||||
oldValue.cancel()
|
||||
}
|
||||
}
|
||||
59
app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt
vendored
Normal file
59
app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
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
|
||||
|
||||
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
||||
|
||||
init {
|
||||
application.registerComponentCallbacks(this)
|
||||
}
|
||||
|
||||
private val detailsCache = DeferredLruCache<Manga>(4)
|
||||
private val pagesCache = DeferredLruCache<List<MangaPage>>(4)
|
||||
|
||||
override val isCachingEnabled: Boolean = true
|
||||
|
||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||
}
|
||||
|
||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||
detailsCache.put(ContentCache.Key(source, url), details)
|
||||
}
|
||||
|
||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
||||
}
|
||||
|
||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||
pagesCache.put(ContentCache.Key(source, url), pages)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||
|
||||
override fun onLowMemory() = Unit
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
trimCache(detailsCache, level)
|
||||
trimCache(pagesCache, level)
|
||||
}
|
||||
|
||||
private fun trimCache(cache: DeferredLruCache<*>, level: Int) {
|
||||
when (level) {
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
|
||||
ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
|
||||
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.evictAll()
|
||||
|
||||
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
|
||||
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1)
|
||||
|
||||
else -> cache.trimToSize(cache.maxSize() / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
20
app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt
vendored
Normal file
20
app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
||||
class SafeDeferred<T>(
|
||||
private val delegate: Deferred<Result<T>>,
|
||||
) {
|
||||
|
||||
suspend fun await(): T {
|
||||
return delegate.await().getOrThrow()
|
||||
}
|
||||
|
||||
suspend fun awaitOrNull(): T? {
|
||||
return delegate.await().getOrNull()
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
delegate.cancel()
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt
vendored
Normal file
18
app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.koitharu.kotatsu.core.cache
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class StubContentCache : ContentCache {
|
||||
|
||||
override val isCachingEnabled: Boolean = false
|
||||
|
||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? = null
|
||||
|
||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) = Unit
|
||||
|
||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
|
||||
|
||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
package org.koitharu.kotatsu.core.network.cookies
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import androidx.annotation.WorkerThread
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
@Singleton
|
||||
class AndroidCookieJar @Inject constructor() : CookieJar {
|
||||
class AndroidCookieJar : MutableCookieJar {
|
||||
|
||||
private val cookieManager = CookieManager.getInstance()
|
||||
|
||||
@WorkerThread
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
val rawCookie = cookieManager.getCookie(url.toString()) ?: return emptyList()
|
||||
return rawCookie.split(';').mapNotNull {
|
||||
@@ -21,6 +19,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
if (cookies.isEmpty()) {
|
||||
return
|
||||
@@ -31,7 +30,7 @@ class AndroidCookieJar @Inject constructor() : CookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
|
||||
override suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
|
||||
cookieManager.removeAllCookies(continuation::resume)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.koitharu.kotatsu.core.network.cookies
|
||||
|
||||
import android.util.Base64
|
||||
import okhttp3.Cookie
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
|
||||
|
||||
class CookieWrapper(
|
||||
val cookie: Cookie,
|
||||
) {
|
||||
|
||||
constructor(encodedString: String) : this(
|
||||
ObjectInputStream(ByteArrayInputStream(Base64.decode(encodedString, Base64.NO_WRAP))).use {
|
||||
val name = it.readUTF()
|
||||
val value = it.readUTF()
|
||||
val expiresAt = it.readLong()
|
||||
val domain = it.readUTF()
|
||||
val path = it.readUTF()
|
||||
val secure = it.readBoolean()
|
||||
val httpOnly = it.readBoolean()
|
||||
val persistent = it.readBoolean()
|
||||
val hostOnly = it.readBoolean()
|
||||
Cookie.Builder().also { c ->
|
||||
c.name(name)
|
||||
c.value(value)
|
||||
if (persistent) {
|
||||
c.expiresAt(expiresAt)
|
||||
}
|
||||
if (hostOnly) {
|
||||
c.hostOnlyDomain(domain)
|
||||
} else {
|
||||
c.domain(domain)
|
||||
}
|
||||
c.path(path)
|
||||
if (secure) {
|
||||
c.secure()
|
||||
}
|
||||
if (httpOnly) {
|
||||
c.httpOnly()
|
||||
}
|
||||
}.build()
|
||||
},
|
||||
)
|
||||
|
||||
fun encode(): String {
|
||||
val output = ByteArrayOutputStream()
|
||||
ObjectOutputStream(output).use {
|
||||
it.writeUTF(cookie.name)
|
||||
it.writeUTF(cookie.value)
|
||||
it.writeLong(cookie.expiresAt)
|
||||
it.writeUTF(cookie.domain)
|
||||
it.writeUTF(cookie.path)
|
||||
it.writeBoolean(cookie.secure)
|
||||
it.writeBoolean(cookie.httpOnly)
|
||||
it.writeBoolean(cookie.persistent)
|
||||
it.writeBoolean(cookie.hostOnly)
|
||||
}
|
||||
return Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun isExpired() = cookie.expiresAt < System.currentTimeMillis()
|
||||
|
||||
fun key(): String {
|
||||
return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CookieWrapper
|
||||
|
||||
if (cookie != other.cookie) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return cookie.hashCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.koitharu.kotatsu.core.network.cookies
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
interface MutableCookieJar : CookieJar {
|
||||
|
||||
@WorkerThread
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie>
|
||||
|
||||
@WorkerThread
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
|
||||
|
||||
suspend fun clear(): Boolean
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.koitharu.kotatsu.core.network.cookies
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.core.content.edit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
|
||||
private const val PREFS_NAME = "cookies"
|
||||
|
||||
class PreferencesCookieJar(
|
||||
context: Context,
|
||||
) : MutableCookieJar {
|
||||
|
||||
private val cache = ArrayMap<String, CookieWrapper>()
|
||||
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private var isLoaded = false
|
||||
|
||||
@WorkerThread
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
loadPersistent()
|
||||
val expired = HashSet<String>()
|
||||
val result = ArrayList<Cookie>()
|
||||
for ((key, cookie) in cache) {
|
||||
if (cookie.isExpired()) {
|
||||
expired += key
|
||||
} else if (cookie.cookie.matches(url)) {
|
||||
result += cookie.cookie
|
||||
}
|
||||
}
|
||||
if (expired.isNotEmpty()) {
|
||||
cache.removeAll(expired)
|
||||
removePersistent(expired)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
val wrapped = cookies.map { CookieWrapper(it) }
|
||||
prefs.edit(commit = true) {
|
||||
for (cookie in wrapped) {
|
||||
val key = cookie.key()
|
||||
cache[key] = cookie
|
||||
if (cookie.cookie.persistent) {
|
||||
putString(key, cookie.encode())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear(): Boolean {
|
||||
cache.clear()
|
||||
withContext(Dispatchers.IO) {
|
||||
prefs.edit(commit = true) { clear() }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun loadPersistent() {
|
||||
if (!isLoaded) {
|
||||
val map = prefs.all
|
||||
cache.ensureCapacity(map.size)
|
||||
for ((k, v) in map) {
|
||||
val cookie = try {
|
||||
CookieWrapper(v as String)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTraceDebug()
|
||||
continue
|
||||
}
|
||||
cache[k] = cookie
|
||||
}
|
||||
isLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun removePersistent(keys: Collection<String>) {
|
||||
prefs.edit(commit = true) {
|
||||
for (key in keys) {
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,25 +6,25 @@ import android.util.Base64
|
||||
import android.webkit.WebView
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.utils.ext.toList
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@Singleton
|
||||
class MangaLoaderContextImpl @Inject constructor(
|
||||
override val httpClient: OkHttpClient,
|
||||
override val cookieJar: AndroidCookieJar,
|
||||
override val cookieJar: MutableCookieJar,
|
||||
@ApplicationContext private val androidContext: Context,
|
||||
) : MangaLoaderContext() {
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.EnumMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.collections.set
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
|
||||
interface MangaRepository {
|
||||
|
||||
@@ -31,6 +37,7 @@ interface MangaRepository {
|
||||
class Factory @Inject constructor(
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val loaderContext: MangaLoaderContext,
|
||||
private val contentCache: ContentCache,
|
||||
) {
|
||||
|
||||
private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java)
|
||||
@@ -42,7 +49,7 @@ interface MangaRepository {
|
||||
cache[source]?.get()?.let { return it }
|
||||
return synchronized(cache) {
|
||||
cache[source]?.get()?.let { return it }
|
||||
val repository = RemoteMangaRepository(MangaParser(source, loaderContext))
|
||||
val repository = RemoteMangaRepository(MangaParser(source, loaderContext), contentCache)
|
||||
cache[source] = WeakReference(repository)
|
||||
repository
|
||||
}
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.parser
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
||||
import org.koitharu.kotatsu.core.prefs.SourceSettings
|
||||
import org.koitharu.kotatsu.parsers.MangaParser
|
||||
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
|
||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
||||
import org.koitharu.kotatsu.parsers.model.*
|
||||
import org.koitharu.kotatsu.parsers.model.Favicons
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
|
||||
class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
||||
class RemoteMangaRepository(
|
||||
private val parser: MangaParser,
|
||||
private val cache: ContentCache,
|
||||
) : MangaRepository {
|
||||
|
||||
override val source: MangaSource
|
||||
get() = parser.source
|
||||
@@ -28,9 +44,23 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
||||
return parser.getList(offset, tags, sortOrder)
|
||||
}
|
||||
|
||||
override suspend fun getDetails(manga: Manga): Manga = parser.getDetails(manga)
|
||||
override suspend fun getDetails(manga: Manga): Manga {
|
||||
cache.getDetails(source, manga.url)?.let { return it }
|
||||
val details = asyncSafe {
|
||||
parser.getDetails(manga)
|
||||
}
|
||||
cache.putDetails(source, manga.url, details)
|
||||
return details.await()
|
||||
}
|
||||
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = parser.getPages(chapter)
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
cache.getPages(source, chapter.url)?.let { return it }
|
||||
val pages = asyncSafe {
|
||||
parser.getPages(chapter)
|
||||
}
|
||||
cache.putPages(source, chapter.url, pages)
|
||||
return pages.await()
|
||||
}
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
|
||||
|
||||
@@ -45,4 +75,10 @@ class RemoteMangaRepository(private val parser: MangaParser) : MangaRepository {
|
||||
}
|
||||
|
||||
private fun getConfig() = parser.config as SourceSettings
|
||||
}
|
||||
|
||||
private fun <T> asyncSafe(block: suspend CoroutineScope.() -> T) = SafeDeferred(
|
||||
processLifecycleScope.async(Dispatchers.Default) {
|
||||
runCatchingCancellable { block() }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
@@ -17,6 +16,7 @@ import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.shelf.domain.ShelfSection
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.getEnumValue
|
||||
import org.koitharu.kotatsu.utils.ext.observe
|
||||
import org.koitharu.kotatsu.utils.ext.putEnumValue
|
||||
@@ -34,6 +34,7 @@ import javax.inject.Singleton
|
||||
class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val connectivityManager = context.connectivityManager
|
||||
|
||||
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
||||
remove(MangaSource.LOCAL)
|
||||
@@ -156,6 +157,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isDynamicShortcutsEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_SHORTCUTS, true)
|
||||
|
||||
fun isContentPrefetchEnabled(): Boolean {
|
||||
val policy = NetworkPolicy.from(prefs.getString(KEY_PREFETCH_CONTENT, null), NetworkPolicy.NEVER)
|
||||
return policy.isNetworkAllowed(connectivityManager)
|
||||
}
|
||||
|
||||
var sourcesOrder: List<String>
|
||||
get() = prefs.getString(KEY_SOURCES_ORDER, null)
|
||||
?.split('|')
|
||||
@@ -234,12 +240,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isWebtoonZoomEnable: Boolean
|
||||
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
|
||||
|
||||
fun isPagesPreloadAllowed(cm: ConnectivityManager): Boolean {
|
||||
return when (prefs.getString(KEY_PAGES_PRELOAD, null)?.toIntOrNull()) {
|
||||
NETWORK_ALWAYS -> true
|
||||
NETWORK_NEVER -> false
|
||||
else -> cm.isActiveNetworkMetered
|
||||
}
|
||||
fun isPagesPreloadEnabled(): Boolean {
|
||||
val policy = NetworkPolicy.from(prefs.getString(KEY_PAGES_PRELOAD, null), NetworkPolicy.NON_METERED)
|
||||
return policy.isNetworkAllowed(connectivityManager)
|
||||
}
|
||||
|
||||
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
|
||||
@@ -293,7 +296,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val TRACK_FAVOURITES = "favourites"
|
||||
|
||||
const val KEY_LIST_MODE = "list_mode_2"
|
||||
const val KEY_APP_SECTION = "app_section_2"
|
||||
const val KEY_THEME = "theme"
|
||||
const val KEY_DYNAMIC_THEME = "dynamic_theme"
|
||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||
@@ -355,13 +357,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
||||
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||
const val KEY_SHELF_SECTIONS = "shelf_sections_2"
|
||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||
|
||||
// About
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
const val KEY_APP_TRANSLATION = "about_app_translation"
|
||||
|
||||
private const val NETWORK_NEVER = 0
|
||||
private const val NETWORK_ALWAYS = 1
|
||||
private const val NETWORK_NON_METERED = 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
|
||||
enum class NetworkPolicy(
|
||||
private val key: Int,
|
||||
) {
|
||||
|
||||
NEVER(0),
|
||||
ALWAYS(1),
|
||||
NON_METERED(2);
|
||||
|
||||
fun isNetworkAllowed(cm: ConnectivityManager) = when (this) {
|
||||
NEVER -> false
|
||||
ALWAYS -> true
|
||||
NON_METERED -> !cm.isActiveNetworkMetered
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(key: String?, default: NetworkPolicy): NetworkPolicy {
|
||||
val intKey = key?.toIntOrNull() ?: return default
|
||||
return enumValues<NetworkPolicy>().find { it.key == intKey } ?: default
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,10 +52,13 @@ class ZipOutput(
|
||||
return if (entryNames.add(entry.name)) {
|
||||
val zipEntry = ZipEntry(entry.name)
|
||||
output.putNextEntry(zipEntry)
|
||||
other.getInputStream(entry).use { input ->
|
||||
input.copyTo(output)
|
||||
try {
|
||||
other.getInputStream(entry).use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
} finally {
|
||||
output.closeEntry()
|
||||
}
|
||||
output.closeEntry()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.koitharu.kotatsu.details.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import org.koitharu.kotatsu.base.ui.CoroutineIntentService
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaChapters
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MangaPrefetchService : CoroutineIntentService() {
|
||||
|
||||
@Inject
|
||||
lateinit var mangaRepositoryFactory: MangaRepository.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var cache: ContentCache
|
||||
|
||||
@Inject
|
||||
lateinit var historyRepository: HistoryRepository
|
||||
|
||||
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_PREFETCH_DETAILS -> prefetchDetails(
|
||||
manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return,
|
||||
)
|
||||
|
||||
ACTION_PREFETCH_PAGES -> prefetchPages(
|
||||
chapter = intent.getParcelableExtraCompat<ParcelableMangaChapters>(EXTRA_CHAPTER)
|
||||
?.chapters?.singleOrNull() ?: return,
|
||||
)
|
||||
|
||||
ACTION_PREFETCH_LAST -> prefetchLast()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(startId: Int, error: Throwable) = Unit
|
||||
|
||||
private suspend fun prefetchDetails(manga: Manga) = coroutineScope {
|
||||
val source = mangaRepositoryFactory.create(manga.source)
|
||||
runCatchingCancellable { source.getDetails(manga) }
|
||||
}
|
||||
|
||||
private suspend fun prefetchPages(chapter: MangaChapter) {
|
||||
val source = mangaRepositoryFactory.create(chapter.source)
|
||||
runCatchingCancellable { source.getPages(chapter) }
|
||||
}
|
||||
|
||||
private suspend fun prefetchLast() {
|
||||
val last = historyRepository.getLastOrNull() ?: return
|
||||
if (last.source == MangaSource.LOCAL) return
|
||||
val repo = mangaRepositoryFactory.create(last.source)
|
||||
val details = runCatchingCancellable { repo.getDetails(last) }.getOrNull() ?: return
|
||||
val chapters = details.chapters
|
||||
if (chapters.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val history = historyRepository.getOne(last)
|
||||
val chapter = if (history == null) {
|
||||
chapters.firstOrNull()
|
||||
} else {
|
||||
chapters.find { x -> x.id == history.chapterId } ?: chapters.firstOrNull()
|
||||
} ?: return
|
||||
runCatchingCancellable { repo.getPages(chapter) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val EXTRA_MANGA = "manga"
|
||||
private const val EXTRA_CHAPTER = "manga"
|
||||
private const val ACTION_PREFETCH_DETAILS = "details"
|
||||
private const val ACTION_PREFETCH_PAGES = "pages"
|
||||
private const val ACTION_PREFETCH_LAST = "last"
|
||||
|
||||
fun prefetchDetails(context: Context, manga: Manga) {
|
||||
if (!isPrefetchAvailable(context, manga.source)) return
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_DETAILS
|
||||
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
fun prefetchPages(context: Context, chapter: MangaChapter) {
|
||||
if (!isPrefetchAvailable(context, chapter.source)) return
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_PAGES
|
||||
intent.putExtra(EXTRA_CHAPTER, ParcelableMangaChapters(listOf(chapter)))
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
fun prefetchLast(context: Context) {
|
||||
if (!isPrefetchAvailable(context, null)) return
|
||||
val intent = Intent(context, MangaPrefetchService::class.java)
|
||||
intent.action = ACTION_PREFETCH_LAST
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
private fun isPrefetchAvailable(context: Context, source: MangaSource?): Boolean {
|
||||
if (source == MangaSource.LOCAL) {
|
||||
return false
|
||||
}
|
||||
val entryPoint = EntryPointAccessors.fromApplication(context, PrefetchCompanionEntryPoint::class.java)
|
||||
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.details.service
|
||||
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface PrefetchCompanionEntryPoint {
|
||||
val settings: AppSettings
|
||||
val contentCache: ContentCache
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
@@ -33,6 +34,8 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.ui.MangaErrorDialog
|
||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
|
||||
@@ -120,6 +123,7 @@ class DetailsActivity :
|
||||
viewModel.branches.observe(this) {
|
||||
binding.buttonDropdown.isVisible = it.size > 1
|
||||
}
|
||||
viewModel.chapters.observe(this, PrefetchObserver(this))
|
||||
|
||||
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
|
||||
addMenuProvider(
|
||||
@@ -228,18 +232,18 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(info: HistoryInfo?) {
|
||||
private fun onHistoryChanged(info: HistoryInfo) {
|
||||
with(binding.buttonRead) {
|
||||
if (info?.history != null) {
|
||||
if (info.history != null) {
|
||||
setText(R.string._continue)
|
||||
setIconResource(R.drawable.ic_play)
|
||||
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
|
||||
} else {
|
||||
setText(R.string.read)
|
||||
setIconResource(R.drawable.ic_read)
|
||||
setIconResource(if (info.isIncognitoMode) R.drawable.ic_incognito else R.drawable.ic_play)
|
||||
}
|
||||
}
|
||||
val text = when {
|
||||
info == null -> getString(R.string.loading_)
|
||||
!info.isValid -> getString(R.string.loading_)
|
||||
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
|
||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
||||
@@ -325,6 +329,24 @@ class DetailsActivity :
|
||||
return sb
|
||||
}
|
||||
|
||||
private class PrefetchObserver(
|
||||
private val context: Context,
|
||||
) : Observer<List<ChapterListItem>> {
|
||||
|
||||
private var isCalled = false
|
||||
|
||||
override fun onChanged(t: List<ChapterListItem>?) {
|
||||
if (t.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
if (!isCalled) {
|
||||
isCalled = true
|
||||
val item = t.find { it.hasFlag(ChapterListItem.FLAG_CURRENT) } ?: t.first()
|
||||
MangaPrefetchService.prefetchPages(context, item.chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context, manga: Manga): Intent {
|
||||
|
||||
@@ -18,7 +18,6 @@ import coil.request.ImageRequest
|
||||
import coil.util.CoilUtils
|
||||
import com.google.android.material.chip.Chip
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
@@ -27,9 +26,9 @@ import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
|
||||
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
@@ -45,8 +44,20 @@ import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import org.koitharu.kotatsu.utils.ext.computeSize
|
||||
import org.koitharu.kotatsu.utils.ext.crossfade
|
||||
import org.koitharu.kotatsu.utils.ext.drawableTop
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.utils.ext.toFileOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DetailsFragment :
|
||||
@@ -75,7 +86,7 @@ class DetailsFragment :
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
|
||||
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
|
||||
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
|
||||
@@ -123,12 +134,14 @@ class DetailsFragment :
|
||||
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_finished)
|
||||
}
|
||||
}
|
||||
|
||||
MangaState.ONGOING -> {
|
||||
infoLayout.textViewState.apply {
|
||||
textAndVisible = resources.getString(R.string.state_ongoing)
|
||||
drawableTop = ContextCompat.getDrawable(context, R.drawable.ic_state_ongoing)
|
||||
}
|
||||
}
|
||||
|
||||
else -> infoLayout.textViewState.isVisible = false
|
||||
}
|
||||
if (manga.source == MangaSource.LOCAL) {
|
||||
@@ -178,8 +191,8 @@ class DetailsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(history: MangaHistory?) {
|
||||
binding.progressView.setPercent(history?.percent ?: PROGRESS_NONE, animate = true)
|
||||
private fun onHistoryChanged(history: HistoryInfo) {
|
||||
binding.progressView.setPercent(history.history?.percent ?: PROGRESS_NONE, animate = true)
|
||||
}
|
||||
|
||||
private fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
@@ -229,6 +242,7 @@ class DetailsFragment :
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.textView_source -> {
|
||||
startActivity(
|
||||
MangaListActivity.newIntent(
|
||||
@@ -237,6 +251,7 @@ class DetailsFragment :
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
R.id.imageView_cover -> {
|
||||
startActivity(
|
||||
ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }),
|
||||
@@ -249,7 +264,7 @@ class DetailsFragment :
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
when (v.id) {
|
||||
R.id.button_read -> {
|
||||
if (viewModel.readingHistory.value == null) {
|
||||
if (viewModel.historyInfo.value?.history == null) {
|
||||
return false
|
||||
}
|
||||
val menu = PopupMenu(v.context, v)
|
||||
@@ -271,12 +286,14 @@ class DetailsFragment :
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
menu.show()
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,18 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import java.io.IOException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -46,6 +54,7 @@ import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
import java.io.IOException
|
||||
|
||||
class DetailsViewModel @AssistedInject constructor(
|
||||
@Assisted intent: MangaIntent,
|
||||
@@ -80,8 +89,14 @@ class DetailsViewModel @AssistedInject constructor(
|
||||
private val favourite = favouritesRepository.observeCategoriesIds(delegate.mangaId).map { it.isNotEmpty() }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
||||
|
||||
private val newChapters = trackingRepository.observeNewChaptersCount(delegate.mangaId)
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
private val newChapters = settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
|
||||
.flatMapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
trackingRepository.observeNewChaptersCount(delegate.mangaId)
|
||||
} else {
|
||||
flowOf(0)
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
||||
|
||||
private val chaptersQuery = MutableStateFlow("")
|
||||
|
||||
@@ -91,17 +106,18 @@ class DetailsViewModel @AssistedInject constructor(
|
||||
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
|
||||
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
|
||||
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
@Deprecated("")
|
||||
val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
|
||||
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
|
||||
|
||||
val historyInfo = combine(
|
||||
val historyInfo: LiveData<HistoryInfo> = combine(
|
||||
delegate.manga,
|
||||
history,
|
||||
) { m, h ->
|
||||
HistoryInfo(m, h)
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
|
||||
historyRepository.observeShouldSkip(delegate.manga),
|
||||
) { m, h, im ->
|
||||
HistoryInfo(m, h, im)
|
||||
}.asFlowLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
defaultValue = HistoryInfo(null, null, false),
|
||||
)
|
||||
|
||||
val bookmarks = delegate.manga.flatMapLatest {
|
||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
||||
|
||||
@@ -42,7 +42,8 @@ class ChaptersAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence {
|
||||
return items[position].chapter.number.toString()
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val item = items.getOrNull(position) ?: return null
|
||||
return item.chapter.number.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ class HistoryInfo(
|
||||
val totalChapters: Int,
|
||||
val currentChapter: Int,
|
||||
val history: MangaHistory?,
|
||||
val isIncognitoMode: Boolean,
|
||||
) {
|
||||
|
||||
val isValid: Boolean
|
||||
get() = totalChapters >= 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
@@ -18,6 +22,7 @@ class HistoryInfo(
|
||||
if (totalChapters != other.totalChapters) return false
|
||||
if (currentChapter != other.currentChapter) return false
|
||||
if (history != other.history) return false
|
||||
if (isIncognitoMode != other.isIncognitoMode) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -26,20 +31,21 @@ class HistoryInfo(
|
||||
var result = totalChapters
|
||||
result = 31 * result + currentChapter
|
||||
result = 31 * result + (history?.hashCode() ?: 0)
|
||||
result = 31 * result + isIncognitoMode.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
fun HistoryInfo(manga: Manga?, history: MangaHistory?): HistoryInfo? {
|
||||
val chapters = manga?.chapters ?: return null
|
||||
fun HistoryInfo(manga: Manga?, history: MangaHistory?, isIncognitoMode: Boolean): HistoryInfo {
|
||||
val chapters = manga?.chapters
|
||||
return HistoryInfo(
|
||||
totalChapters = chapters.size,
|
||||
currentChapter = if (history != null) {
|
||||
totalChapters = chapters?.size ?: -1,
|
||||
currentChapter = if (history != null && !chapters.isNullOrEmpty()) {
|
||||
chapters.indexOfFirst { it.id == history.chapterId }
|
||||
} else {
|
||||
-1
|
||||
},
|
||||
history = history,
|
||||
isIncognitoMode = isIncognitoMode,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,15 +7,16 @@ import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.titleRes
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener {
|
||||
@@ -23,9 +24,7 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
|
||||
@Inject
|
||||
lateinit var viewModelFactory: FavouritesListViewModel.Factory
|
||||
|
||||
override val viewModel by assistedViewModels<FavouritesListViewModel> {
|
||||
viewModelFactory.create(categoryId)
|
||||
}
|
||||
override val viewModel by assistedViewModels { viewModelFactory.create(categoryId) }
|
||||
|
||||
private val categoryId: Long
|
||||
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: NO_ID
|
||||
@@ -34,6 +33,9 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if (viewModel.categoryId != NO_ID) {
|
||||
addMenuProvider(FavouritesListMenuProvider(view.context, viewModel))
|
||||
}
|
||||
viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() }
|
||||
}
|
||||
|
||||
@@ -73,6 +75,7 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onActionItemClicked(controller, mode, item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.list
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.forEach
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.titleRes
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
class FavouritesListMenuProvider(
|
||||
private val context: Context,
|
||||
private val viewModel: FavouritesListViewModel,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_favourites, menu)
|
||||
val subMenu = menu.findItem(R.id.action_order)?.subMenu ?: return
|
||||
for (order in FavouriteCategoriesActivity.SORT_ORDERS) {
|
||||
subMenu.add(R.id.group_order, Menu.NONE, order.ordinal, order.titleRes)
|
||||
}
|
||||
subMenu.setGroupCheckable(R.id.group_order, true, true)
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
super.onPrepareMenu(menu)
|
||||
val order = viewModel.sortOrder.value ?: return
|
||||
menu.findItem(R.id.action_order)?.subMenu?.forEach { item ->
|
||||
if (item.order == order.ordinal) {
|
||||
item.isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
if (menuItem.groupId == R.id.group_order) {
|
||||
val order = enumValues<SortOrder>()[menuItem.order]
|
||||
viewModel.setSortOrder(order)
|
||||
return true
|
||||
}
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_edit -> {
|
||||
context.startActivity(
|
||||
FavouritesCategoryEditActivity.newIntent(context, viewModel.categoryId),
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
|
||||
class FavouritesListViewModel @AssistedInject constructor(
|
||||
@Assisted private val categoryId: Long,
|
||||
@Assisted val categoryId: Long,
|
||||
private val repository: FavouritesRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
@@ -55,7 +55,7 @@ class FavouritesListViewModel @AssistedInject constructor(
|
||||
} else {
|
||||
repository.observeAll(categoryId)
|
||||
},
|
||||
createListModeFlow(),
|
||||
listModeFlow,
|
||||
) { list, mode ->
|
||||
when {
|
||||
list.isEmpty() -> listOf(
|
||||
@@ -117,7 +117,11 @@ class FavouritesListViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override suspend fun getCounter(mangaId: Long): Int {
|
||||
return trackingRepository.getNewChaptersCount(mangaId)
|
||||
return if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getProgress(mangaId: Long): Float {
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package org.koitharu.kotatsu.history.domain
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.*
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTag
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
import org.koitharu.kotatsu.history.data.toMangaHistory
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -18,6 +26,7 @@ import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||
import org.koitharu.kotatsu.scrobbling.domain.tryScrobble
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
import javax.inject.Inject
|
||||
|
||||
const val PROGRESS_NONE = -1f
|
||||
|
||||
@@ -66,7 +75,7 @@ class HistoryRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int, percent: Float) {
|
||||
if (manga.isNsfw && settings.isHistoryExcludeNsfw || settings.isIncognitoModeEnabled) {
|
||||
if (shouldSkip(manga)) {
|
||||
return
|
||||
}
|
||||
val tags = manga.tags.toEntities()
|
||||
@@ -138,6 +147,30 @@ class HistoryRepository @Inject constructor(
|
||||
return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() }
|
||||
}
|
||||
|
||||
fun shouldSkip(manga: Manga): Boolean {
|
||||
return manga.isNsfw && settings.isHistoryExcludeNsfw || settings.isIncognitoModeEnabled
|
||||
}
|
||||
|
||||
fun observeShouldSkip(manga: Manga): Flow<Boolean> {
|
||||
return settings.observe()
|
||||
.filter { key -> key == AppSettings.KEY_INCOGNITO_MODE || key == AppSettings.KEY_HISTORY_EXCLUDE_NSFW }
|
||||
.onStart { emit("") }
|
||||
.map { shouldSkip(manga) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeShouldSkip(mangaFlow: Flow<Manga?>): Flow<Boolean> {
|
||||
return mangaFlow
|
||||
.distinctUntilChangedBy { it?.isNsfw }
|
||||
.flatMapLatest { m ->
|
||||
if (m != null) {
|
||||
observeShouldSkip(m)
|
||||
} else {
|
||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun recover(ids: Collection<Long>) {
|
||||
db.withTransaction {
|
||||
for (id in ids) {
|
||||
|
||||
@@ -14,14 +14,14 @@ class HistoryListAdapter(
|
||||
listener: MangaListListener
|
||||
) : MangaListAdapter(coil, lifecycleOwner, listener), FastScroller.SectionIndexer {
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence {
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val list = items
|
||||
for (i in (0..position).reversed()) {
|
||||
val item = list[i]
|
||||
val item = list.getOrNull(i) ?: continue
|
||||
if (item is DateTimeAgo) {
|
||||
return item.format(context.resources)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ package org.koitharu.kotatsu.history.ui
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -21,11 +18,20 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toListModel
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HistoryListViewModel @Inject constructor(
|
||||
@@ -42,7 +48,7 @@ class HistoryListViewModel @Inject constructor(
|
||||
override val content = combine(
|
||||
repository.observeAllWithHistory(),
|
||||
historyGrouping,
|
||||
createListModeFlow(),
|
||||
listModeFlow,
|
||||
) { list, grouped, mode ->
|
||||
when {
|
||||
list.isEmpty() -> listOf(
|
||||
@@ -53,6 +59,7 @@ class HistoryListViewModel @Inject constructor(
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
|
||||
else -> mapList(list, grouped, mode)
|
||||
}
|
||||
}.onStart {
|
||||
@@ -103,7 +110,11 @@ class HistoryListViewModel @Inject constructor(
|
||||
}
|
||||
prevDate = date
|
||||
}
|
||||
val counter = trackingRepository.getNewChaptersCount(manga.id)
|
||||
val counter = if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(manga.id)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val percent = if (showPercent) history.percent else PROGRESS_NONE
|
||||
result += when (mode) {
|
||||
ListMode.LIST -> manga.toListModel(counter, percent)
|
||||
|
||||
@@ -49,6 +49,9 @@ class ListModeBottomSheet :
|
||||
}
|
||||
|
||||
override fun onButtonChecked(group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean) {
|
||||
if (!isChecked) {
|
||||
return
|
||||
}
|
||||
val mode = when (checkedId) {
|
||||
R.id.button_list -> ListMode.LIST
|
||||
R.id.button_list_detailed -> ListMode.DETAILED_LIST
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.view.ActionMode
|
||||
@@ -15,7 +19,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.reverseAsync
|
||||
@@ -42,13 +45,23 @@ import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import org.koitharu.kotatsu.utils.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class MangaListFragment :
|
||||
@@ -138,6 +151,20 @@ abstract class MangaListFragment :
|
||||
return selectionController?.onItemLongClick(item.id) ?: false
|
||||
}
|
||||
|
||||
override fun onReadClick(manga: Manga, view: View) {
|
||||
if (selectionController?.onItemClick(manga.id) != true) {
|
||||
val intent = ReaderActivity.newIntent(context ?: return, manga)
|
||||
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
|
||||
if (selectionController?.onItemClick(manga.id) != true) {
|
||||
val intent = MangaListActivity.newIntent(context ?: return, setOf(tag))
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onRefresh() {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
@@ -251,12 +278,14 @@ abstract class MangaListFragment :
|
||||
)
|
||||
addItemDecoration(decoration)
|
||||
}
|
||||
|
||||
ListMode.DETAILED_LIST -> {
|
||||
layoutManager = FitHeightLinearLayoutManager(context)
|
||||
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
|
||||
updatePadding(left = spacing, right = spacing)
|
||||
addItemDecoration(SpacingItemDecoration(spacing))
|
||||
}
|
||||
|
||||
ListMode.GRID -> {
|
||||
layoutManager = FitHeightGridLayoutManager(context, checkNotNull(spanResolver).spanCount).also {
|
||||
it.spanSizeLookup = spanSizeLookup
|
||||
@@ -284,21 +313,25 @@ abstract class MangaListFragment :
|
||||
selectionController?.addAll(ids)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_share -> {
|
||||
ShareHelper(requireContext()).shareMangaLinks(selectedItems)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_favourite -> {
|
||||
FavouriteCategoriesBottomSheet.show(childFragmentManager, selectedItems)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_save -> {
|
||||
DownloadService.confirmAndStart(requireContext(), selectedItems)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
|
||||
abstract class MangaListViewModel(
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
abstract val content: LiveData<List<ListModel>>
|
||||
val listMode = MutableLiveData<ListMode>()
|
||||
protected val listModeFlow = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, settings.listMode)
|
||||
val listMode = listModeFlow.asFlowLiveData(viewModelScope.coroutineContext)
|
||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||
val gridScale = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
@@ -30,13 +33,6 @@ abstract class MangaListViewModel(
|
||||
|
||||
open fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
||||
|
||||
protected fun createListModeFlow() = settings.observeAsFlow(AppSettings.KEY_LIST_MODE) { listMode }
|
||||
.onEach {
|
||||
if (listMode.value != it) {
|
||||
listMode.postValue(it)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onRefresh()
|
||||
|
||||
abstract fun onRetry()
|
||||
|
||||
@@ -22,11 +22,12 @@ open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDec
|
||||
|
||||
protected val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
protected val checkIcon = ContextCompat.getDrawable(context, materialR.drawable.ic_mtrl_checked_circle)
|
||||
protected val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing_outer)
|
||||
protected val iconOffset = context.resources.getDimensionPixelOffset(R.dimen.card_indicator_offset)
|
||||
protected val iconSize = context.resources.getDimensionPixelOffset(R.dimen.card_indicator_size)
|
||||
protected val strokeColor = context.getThemeColor(materialR.attr.colorPrimary, Color.RED)
|
||||
protected val fillColor = ColorUtils.setAlphaComponent(
|
||||
ColorUtils.blendARGB(strokeColor, context.getThemeColor(materialR.attr.colorSurface), 0.8f),
|
||||
0x74
|
||||
0x74,
|
||||
)
|
||||
protected val defaultRadius = context.resources.getDimension(R.dimen.list_selector_corner)
|
||||
|
||||
@@ -65,11 +66,11 @@ open class MangaSelectionDecoration(context: Context) : AbstractSelectionItemDec
|
||||
setBounds(
|
||||
(bounds.left + iconOffset).toInt(),
|
||||
(bounds.top + iconOffset).toInt(),
|
||||
(bounds.left + iconOffset + intrinsicWidth).toInt(),
|
||||
(bounds.top + iconOffset + intrinsicHeight).toInt(),
|
||||
(bounds.left + iconOffset + iconSize).toInt(),
|
||||
(bounds.top + iconOffset + iconSize).toInt(),
|
||||
)
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
|
||||
interface MangaDetailsClickListener : OnListItemClickListener<Manga> {
|
||||
|
||||
fun onReadClick(manga: Manga, view: View)
|
||||
|
||||
fun onTagClick(manga: Manga, tag: MangaTag, view: View)
|
||||
}
|
||||
@@ -1,34 +1,52 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.google.android.material.badge.BadgeDrawable
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.utils.ext.referer
|
||||
import org.koitharu.kotatsu.utils.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.utils.image.CoverSizeResolver
|
||||
|
||||
fun mangaListDetailedItemAD(
|
||||
coil: ImageLoader,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
clickListener: OnListItemClickListener<Manga>,
|
||||
clickListener: MangaDetailsClickListener,
|
||||
) = adapterDelegateViewBinding<MangaListDetailedModel, ListModel, ItemMangaListDetailsBinding>(
|
||||
{ inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
var badge: BadgeDrawable? = null
|
||||
|
||||
itemView.setOnClickListener {
|
||||
clickListener.onItemClick(item.manga, it)
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
clickListener.onItemLongClick(item.manga, it)
|
||||
val listenerAdapter = object : View.OnClickListener, View.OnLongClickListener, ChipsView.OnChipClickListener {
|
||||
override fun onClick(v: View) = when (v.id) {
|
||||
R.id.button_read -> clickListener.onReadClick(item.manga, v)
|
||||
else -> clickListener.onItemClick(item.manga, v)
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean = clickListener.onItemLongClick(item.manga, v)
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val tag = data as? MangaTag ?: return
|
||||
clickListener.onTagClick(item.manga, tag, chip)
|
||||
}
|
||||
}
|
||||
itemView.setOnClickListener(listenerAdapter)
|
||||
itemView.setOnLongClickListener(listenerAdapter)
|
||||
binding.buttonRead.setOnClickListener(listenerAdapter)
|
||||
binding.chipsTags.onChipClickListener = listenerAdapter
|
||||
|
||||
bind { payloads ->
|
||||
binding.textViewTitle.text = item.title
|
||||
@@ -44,8 +62,9 @@ fun mangaListDetailedItemAD(
|
||||
lifecycle(lifecycleOwner)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
binding.textViewRating.textAndVisible = item.rating
|
||||
binding.textViewTags.text = item.tags
|
||||
binding.chipsTags.setChips(item.tags)
|
||||
binding.ratingBar.isVisible = item.manga.hasRating
|
||||
binding.ratingBar.rating = binding.ratingBar.numStars * item.manga.rating
|
||||
badge = itemView.bindBadge(badge, item.counter)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
|
||||
interface MangaListListener : OnListItemClickListener<Manga>, ListStateHolderListener, ListHeaderClickListener {
|
||||
interface MangaListListener : MangaDetailsClickListener, ListStateHolderListener, ListHeaderClickListener {
|
||||
|
||||
fun onUpdateFilter(tags: Set<MangaTag>)
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
@@ -11,6 +10,8 @@ import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.utils.ext.ifZero
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
fun Manga.toListModel(counter: Int, progress: Float) = MangaListModel(
|
||||
id = id,
|
||||
@@ -26,12 +27,11 @@ fun Manga.toListDetailedModel(counter: Int, progress: Float) = MangaListDetailed
|
||||
id = id,
|
||||
title = title,
|
||||
subtitle = altTitle,
|
||||
rating = if (hasRating) String.format("%.1f", rating * 5) else null,
|
||||
tags = tags.joinToString(", ") { it.title },
|
||||
coverUrl = coverUrl,
|
||||
manga = this,
|
||||
counter = counter,
|
||||
progress = progress,
|
||||
tags = tags.map { ChipsView.ChipModel(0, it.title, false, false, it) },
|
||||
)
|
||||
|
||||
fun Manga.toGridModel(counter: Int, progress: Float) = MangaGridModel(
|
||||
@@ -69,9 +69,11 @@ suspend fun <C : MutableCollection<in MangaItemModel>> List<Manga>.toUi(
|
||||
ListMode.LIST -> mapTo(destination) {
|
||||
it.toListModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id))
|
||||
}
|
||||
|
||||
ListMode.DETAILED_LIST -> mapTo(destination) {
|
||||
it.toListDetailedModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id))
|
||||
}
|
||||
|
||||
ListMode.GRID -> mapTo(destination) {
|
||||
it.toGridModel(extraProvider.getCounter(it.id), extraProvider.getProgress(it.id))
|
||||
}
|
||||
@@ -95,5 +97,6 @@ private fun getErrorIcon(error: Throwable) = when (error) {
|
||||
is UnknownHostException,
|
||||
is SocketTimeoutException,
|
||||
-> R.drawable.ic_plug_large
|
||||
|
||||
else -> R.drawable.ic_error_large
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
data class MangaListDetailedModel(
|
||||
override val id: Long,
|
||||
override val title: String,
|
||||
val subtitle: String?,
|
||||
val tags: String,
|
||||
override val coverUrl: String,
|
||||
val rating: String?,
|
||||
override val manga: Manga,
|
||||
override val counter: Int,
|
||||
override val progress: Float,
|
||||
) : MangaItemModel
|
||||
val tags: List<ChipsView.ChipModel>,
|
||||
) : MangaItemModel
|
||||
|
||||
@@ -23,10 +23,7 @@ class CbzFetcher(
|
||||
val zip = ZipFile(uri.schemeSpecificPart)
|
||||
val entry = zip.getEntry(uri.fragment)
|
||||
val ext = MimeTypeMap.getFileExtensionFromUrl(entry.name)
|
||||
val bufferedSource = ExtraCloseableBufferedSource(
|
||||
zip.getInputStream(entry).source().buffer(),
|
||||
zip,
|
||||
)
|
||||
val bufferedSource = zip.getInputStream(entry).source().withExtraCloseable(zip).buffer()
|
||||
SourceResult(
|
||||
source = ImageSource(
|
||||
source = bufferedSource,
|
||||
@@ -50,4 +47,4 @@ class CbzFetcher(
|
||||
}
|
||||
|
||||
class CbzMetadata(val uri: Uri) : ImageSource.Metadata()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.BufferedSource
|
||||
import okio.Closeable
|
||||
|
||||
class ExtraCloseableBufferedSource(
|
||||
private val delegate: BufferedSource,
|
||||
vararg closeable: Closeable,
|
||||
) : BufferedSource by delegate {
|
||||
|
||||
private val extraCloseable = closeable
|
||||
|
||||
override fun close() {
|
||||
delegate.close()
|
||||
extraCloseable.forEach { x -> x.closeQuietly() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.koitharu.kotatsu.local.data
|
||||
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.Closeable
|
||||
import okio.Source
|
||||
|
||||
private class ExtraCloseableSource(
|
||||
private val delegate: Source,
|
||||
private val extraCloseable: Closeable,
|
||||
) : Source by delegate {
|
||||
|
||||
override fun close() {
|
||||
try {
|
||||
delegate.close()
|
||||
} finally {
|
||||
extraCloseable.closeQuietly()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Source.withExtraCloseable(closeable: Closeable): Source = ExtraCloseableSource(this, closeable)
|
||||
@@ -4,14 +4,16 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -20,7 +22,11 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.*
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
@@ -30,6 +36,9 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
|
||||
import java.io.IOException
|
||||
import java.util.LinkedList
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LocalListViewModel @Inject constructor(
|
||||
@@ -48,7 +57,7 @@ class LocalListViewModel @Inject constructor(
|
||||
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
listModeFlow,
|
||||
sortOrder.asFlow(),
|
||||
selectedTags,
|
||||
listError,
|
||||
@@ -181,7 +190,11 @@ class LocalListViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun getCounter(mangaId: Long): Int {
|
||||
return trackingRepository.getNewChaptersCount(mangaId)
|
||||
return if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getProgress(mangaId: Long): Float {
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.widgets.SlidingBottomNavigationView
|
||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
|
||||
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
@@ -334,6 +335,7 @@ class MainActivity :
|
||||
TrackWorker.setup(applicationContext)
|
||||
SuggestionsWorker.setup(applicationContext)
|
||||
}
|
||||
MangaPrefetchService.prefetchLast(this@MainActivity)
|
||||
requestNotificationsPermission()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ class MainNavigationDelegate(
|
||||
fun onCreate(savedInstanceState: Bundle?) {
|
||||
primaryFragment?.let {
|
||||
onFragmentChanged(it, fromUser = false)
|
||||
navBar.selectedItemId = getItemId(it)
|
||||
} ?: onNavigationItemSelected(navBar.selectedItemId)
|
||||
}
|
||||
|
||||
@@ -92,6 +93,14 @@ class MainNavigationDelegate(
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getItemId(fragment: Fragment) = when (fragment) {
|
||||
is ShelfFragment -> R.id.nav_shelf
|
||||
is ExploreFragment -> R.id.nav_explore
|
||||
is FeedFragment -> R.id.nav_feed
|
||||
is ToolsFragment -> R.id.nav_tools
|
||||
else -> 0
|
||||
}
|
||||
|
||||
private fun setPrimaryFragment(fragment: Fragment) {
|
||||
fragmentManager.beginTransaction()
|
||||
.setReorderingAllowed(true)
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||
import org.koitharu.kotatsu.core.github.AppUpdateRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -34,9 +35,12 @@ class MainViewModel @Inject constructor(
|
||||
|
||||
val onOpenReader = SingleLiveEvent<Manga>()
|
||||
|
||||
val isResumeEnabled = historyRepository
|
||||
.observeHasItems()
|
||||
.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
val isResumeEnabled = combine(
|
||||
historyRepository.observeHasItems(),
|
||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
||||
) { hasItems, incognito ->
|
||||
hasItems && !incognito
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false)
|
||||
|
||||
val isFeedAvailable = settings.observeAsLiveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.koitharu.kotatsu.reader.domain
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -31,7 +29,6 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.utils.ext.withProgress
|
||||
import org.koitharu.kotatsu.utils.progress.ProgressDeferred
|
||||
@@ -50,13 +47,11 @@ class PageLoader @Inject constructor(
|
||||
private val okHttp: OkHttpClient,
|
||||
private val cache: PagesCache,
|
||||
private val settings: AppSettings,
|
||||
@ApplicationContext context: Context,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
) : Closeable {
|
||||
|
||||
val loaderScope = CoroutineScope(SupervisorJob() + InternalErrorHandler() + Dispatchers.Default)
|
||||
|
||||
private val connectivityManager = context.connectivityManager
|
||||
private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
|
||||
private val convertLock = Mutex()
|
||||
private var repository: MangaRepository? = null
|
||||
@@ -73,7 +68,7 @@ class PageLoader @Inject constructor(
|
||||
}
|
||||
|
||||
fun isPrefetchApplicable(): Boolean {
|
||||
return repository is RemoteMangaRepository && settings.isPagesPreloadAllowed(connectivityManager)
|
||||
return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled()
|
||||
}
|
||||
|
||||
fun prefetch(pages: List<ReaderPage>) {
|
||||
|
||||
@@ -313,7 +313,7 @@ class ReaderViewModel @AssistedInject constructor(
|
||||
} ?: ReaderState(manga, preselectedBranch)
|
||||
}
|
||||
|
||||
val branch = chapters[currentState.value?.chapterId ?: 0L].branch
|
||||
val branch = chapters[currentState.value?.chapterId ?: 0L]?.branch
|
||||
mangaData.value = manga.filterChapters(branch)
|
||||
readerMode.postValue(mode)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
import android.graphics.PointF
|
||||
import android.view.Gravity
|
||||
import android.widget.FrameLayout
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||
@@ -13,12 +14,13 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
|
||||
|
||||
class ReversedPageHolder(
|
||||
owner: LifecycleOwner,
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) : PageHolder(binding, loader, settings, networkState, exceptionResolver) {
|
||||
) : PageHolder(owner, binding, loader, settings, networkState, exceptionResolver) {
|
||||
|
||||
init {
|
||||
(binding.textViewNumber.layoutParams as FrameLayout.LayoutParams)
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.reversed
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
@@ -10,6 +11,7 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class ReversedPagesAdapter(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
@@ -23,6 +25,7 @@ class ReversedPagesAdapter(
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = ReversedPageHolder(
|
||||
owner = lifecycleOwner,
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
|
||||
@@ -39,10 +39,11 @@ class ReversedReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pagerAdapter = ReversedPagesAdapter(
|
||||
viewModel.pageLoader,
|
||||
viewModel.readerSettings,
|
||||
networkState,
|
||||
exceptionResolver,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = viewModel.pageLoader,
|
||||
settings = viewModel.readerSettings,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
with(binding.pager) {
|
||||
adapter = pagerAdapter
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.PointF
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -19,6 +20,7 @@ import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
open class PageHolder(
|
||||
owner: LifecycleOwner,
|
||||
binding: ItemPageBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
@@ -28,6 +30,7 @@ open class PageHolder(
|
||||
View.OnClickListener {
|
||||
|
||||
init {
|
||||
binding.ssiv.bindToLifecycle(owner)
|
||||
binding.ssiv.isEagerLoadingEnabled = !isLowRamDevice(context)
|
||||
binding.ssiv.addOnImageEventListener(delegate)
|
||||
@Suppress("LeakingThis")
|
||||
|
||||
@@ -38,10 +38,11 @@ class PagerReaderFragment : BaseReader<FragmentReaderStandardBinding>() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
pagesAdapter = PagesAdapter(
|
||||
viewModel.pageLoader,
|
||||
viewModel.readerSettings,
|
||||
networkState,
|
||||
exceptionResolver,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = viewModel.pageLoader,
|
||||
settings = viewModel.readerSettings,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
with(binding.pager) {
|
||||
adapter = pagesAdapter
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.standard
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageBinding
|
||||
@@ -10,6 +11,7 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class PagesAdapter(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
@@ -23,6 +25,7 @@ class PagesAdapter(
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = PageHolder(
|
||||
owner = lifecycleOwner,
|
||||
binding = ItemPageBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
loader = loader,
|
||||
settings = settings,
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
|
||||
@@ -10,6 +11,7 @@ import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
|
||||
|
||||
class WebtoonAdapter(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
networkState: NetworkState,
|
||||
@@ -23,6 +25,7 @@ class WebtoonAdapter(
|
||||
networkState: NetworkState,
|
||||
exceptionResolver: ExceptionResolver,
|
||||
) = WebtoonHolder(
|
||||
owner = lifecycleOwner,
|
||||
binding = ItemPageWebtoonBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.davemorrissey.labs.subscaleview.decoder.SkiaPooledImageRegionDecoder
|
||||
@@ -22,6 +23,7 @@ import org.koitharu.kotatsu.utils.ext.setProgressCompat
|
||||
import org.koitharu.kotatsu.utils.ext.showCompat
|
||||
|
||||
class WebtoonHolder(
|
||||
owner: LifecycleOwner,
|
||||
binding: ItemPageWebtoonBinding,
|
||||
loader: PageLoader,
|
||||
settings: ReaderSettings,
|
||||
@@ -34,6 +36,7 @@ class WebtoonHolder(
|
||||
private val goneOnInvisibleListener = GoneOnInvisibleListener(bindingInfo.progressBar)
|
||||
|
||||
init {
|
||||
binding.ssiv.bindToLifecycle(owner)
|
||||
binding.ssiv.regionDecoderFactory = SkiaPooledImageRegionDecoder.Factory()
|
||||
binding.ssiv.addOnImageEventListener(delegate)
|
||||
bindingInfo.buttonRetry.setOnClickListener(this)
|
||||
|
||||
@@ -35,10 +35,11 @@ class WebtoonReaderFragment : BaseReader<FragmentReaderWebtoonBinding>() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
webtoonAdapter = WebtoonAdapter(
|
||||
viewModel.pageLoader,
|
||||
viewModel.readerSettings,
|
||||
networkState,
|
||||
exceptionResolver,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
loader = viewModel.pageLoader,
|
||||
settings = viewModel.readerSettings,
|
||||
networkState = networkState,
|
||||
exceptionResolver = exceptionResolver,
|
||||
)
|
||||
with(binding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
|
||||
@@ -63,7 +63,7 @@ class RemoteListViewModel @AssistedInject constructor(
|
||||
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
listModeFlow,
|
||||
createHeaderFlow(),
|
||||
listError,
|
||||
hasNextPage,
|
||||
|
||||
@@ -39,7 +39,7 @@ class SearchViewModel @AssistedInject constructor(
|
||||
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
createListModeFlow(),
|
||||
listModeFlow,
|
||||
listError,
|
||||
hasNextPage,
|
||||
) { list, mode, error, hasNext ->
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
||||
@@ -27,11 +26,15 @@ import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
||||
import org.koitharu.kotatsu.search.ui.multi.adapter.MultiSearchAdapter
|
||||
import org.koitharu.kotatsu.utils.ShareHelper
|
||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
|
||||
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MultiSearchActivity :
|
||||
@@ -110,6 +113,20 @@ class MultiSearchActivity :
|
||||
return selectionController.onItemLongClick(item.id)
|
||||
}
|
||||
|
||||
override fun onReadClick(manga: Manga, view: View) {
|
||||
if (!selectionController.onItemClick(manga.id)) {
|
||||
val intent = ReaderActivity.newIntent(this, manga)
|
||||
startActivity(intent, scaleUpActivityOptionsOf(view).toBundle())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) {
|
||||
if (!selectionController.onItemClick(manga.id)) {
|
||||
val intent = MangaListActivity.newIntent(this, setOf(tag))
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) {
|
||||
viewModel.doSearch(viewModel.query.value.orEmpty())
|
||||
}
|
||||
@@ -139,16 +156,19 @@ class MultiSearchActivity :
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_favourite -> {
|
||||
FavouriteCategoriesBottomSheet.show(supportFragmentManager, collectSelectedItems())
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_save -> {
|
||||
DownloadService.confirmAndStart(this, collectSelectedItems())
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,13 @@ import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.base.ui.dialog.StorageSelectDialog
|
||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
@@ -26,6 +25,8 @@ import org.koitharu.kotatsu.sync.ui.SyncSettingsIntent
|
||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ContentSettingsFragment :
|
||||
@@ -36,9 +37,12 @@ class ContentSettingsFragment :
|
||||
@Inject
|
||||
lateinit var storageManager: LocalStorageManager
|
||||
|
||||
@Inject
|
||||
lateinit var contentCache: ContentCache
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_content)
|
||||
|
||||
findPreference<Preference>(AppSettings.KEY_PREFETCH_CONTENT)?.isVisible = contentCache.isCachingEnabled
|
||||
findPreference<SliderPreference>(AppSettings.KEY_DOWNLOADS_PARALLELISM)?.run {
|
||||
summary = value.toString()
|
||||
setOnPreferenceChangeListener { preference, newValue ->
|
||||
@@ -82,11 +86,13 @@ class ContentSettingsFragment :
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
findPreference<Preference>(key)?.bindStorageName()
|
||||
}
|
||||
|
||||
AppSettings.KEY_SUGGESTIONS -> {
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled,
|
||||
)
|
||||
}
|
||||
|
||||
AppSettings.KEY_SOURCES_HIDDEN -> {
|
||||
bindRemoteSourcesSummary()
|
||||
}
|
||||
@@ -104,6 +110,7 @@ class ContentSettingsFragment :
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_SYNC -> {
|
||||
val am = AccountManager.get(requireContext())
|
||||
val accountType = getString(R.string.account_type_sync)
|
||||
@@ -119,6 +126,7 @@ class ContentSettingsFragment :
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
@@ -41,7 +41,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
||||
lateinit var shikimoriRepository: ShikimoriRepository
|
||||
|
||||
@Inject
|
||||
lateinit var cookieJar: AndroidCookieJar
|
||||
lateinit var cookieJar: MutableCookieJar
|
||||
|
||||
@Inject
|
||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||
|
||||
@@ -16,7 +16,6 @@ import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import com.google.android.material.R as materialR
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
@@ -30,6 +29,7 @@ import org.koitharu.kotatsu.settings.about.AppUpdateDialog
|
||||
import org.koitharu.kotatsu.settings.tools.model.StorageUsage
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ToolsFragment :
|
||||
@@ -48,6 +48,7 @@ class ToolsFragment :
|
||||
binding.buttonSettings.setOnClickListener(this)
|
||||
binding.buttonDownloads.setOnClickListener(this)
|
||||
binding.cardUpdate.root.setOnClickListener(this)
|
||||
binding.cardUpdate.buttonChangelog.setOnClickListener(this)
|
||||
binding.cardUpdate.buttonDownload.setOnClickListener(this)
|
||||
binding.switchIncognito.setOnCheckedChangeListener(this)
|
||||
|
||||
@@ -69,10 +70,13 @@ class ToolsFragment :
|
||||
intent.data = url.toUri()
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.open_in_browser)))
|
||||
}
|
||||
|
||||
R.id.card_update -> {
|
||||
val version = viewModel.appUpdate.value ?: return
|
||||
AppUpdateDialog(v.context).show(version)
|
||||
}
|
||||
|
||||
R.id.button_changelog -> showChangelog()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +145,13 @@ class ToolsFragment :
|
||||
return MaterialColors.harmonize(color, backgroundColor)
|
||||
}
|
||||
|
||||
private fun showChangelog() {
|
||||
TransitionManager.beginDelayedTransition(binding.cardUpdate.root)
|
||||
binding.cardUpdate.buttonChangelog.isVisible = false
|
||||
binding.cardUpdate.textSecondary.isVisible = true
|
||||
binding.cardUpdate.textChangelog.isVisible = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = ToolsFragment()
|
||||
|
||||
@@ -53,17 +53,22 @@ class ShelfViewModel @Inject constructor(
|
||||
|
||||
val content: LiveData<List<ListModel>> = combine(
|
||||
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
|
||||
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
|
||||
networkState,
|
||||
repository.observeShelfContent(),
|
||||
) { sections, isConnected, content ->
|
||||
mapList(content, sections, isConnected)
|
||||
) { sections, isTrackerEnabled, isConnected, content ->
|
||||
mapList(content, isTrackerEnabled, sections, isConnected)
|
||||
}.debounce(500)
|
||||
.catch { e ->
|
||||
emit(listOf(e.toErrorState(canRetry = false)))
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||
|
||||
override suspend fun getCounter(mangaId: Long): Int {
|
||||
return trackingRepository.getNewChaptersCount(mangaId)
|
||||
return if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getProgress(mangaId: Long): Float {
|
||||
@@ -135,6 +140,7 @@ class ShelfViewModel @Inject constructor(
|
||||
|
||||
private suspend fun mapList(
|
||||
content: ShelfContent,
|
||||
isTrackerEnabled: Boolean,
|
||||
sections: List<ShelfSection>,
|
||||
isNetworkAvailable: Boolean,
|
||||
): List<ListModel> {
|
||||
@@ -144,7 +150,10 @@ class ShelfViewModel @Inject constructor(
|
||||
when (section) {
|
||||
ShelfSection.HISTORY -> mapHistory(result, content.history)
|
||||
ShelfSection.LOCAL -> mapLocal(result, content.local)
|
||||
ShelfSection.UPDATED -> mapUpdated(result, content.updated)
|
||||
ShelfSection.UPDATED -> if (isTrackerEnabled) {
|
||||
mapUpdated(result, content.updated)
|
||||
}
|
||||
|
||||
ShelfSection.FAVORITES -> mapFavourites(result, content.favourites)
|
||||
}
|
||||
}
|
||||
@@ -194,7 +203,7 @@ class ShelfViewModel @Inject constructor(
|
||||
val showPercent = settings.isReadingIndicatorsEnabled
|
||||
destination += ShelfSectionModel.History(
|
||||
items = list.map { (manga, history) ->
|
||||
val counter = trackingRepository.getNewChaptersCount(manga.id)
|
||||
val counter = getCounter(manga.id)
|
||||
val percent = if (showPercent) history.percent else PROGRESS_NONE
|
||||
manga.toGridModel(counter, percent)
|
||||
},
|
||||
|
||||
@@ -12,13 +12,13 @@ class ScrollKeepObserver(
|
||||
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
val position = minOf(toPosition, fromPosition) // if items are swapping positions may be swapped too
|
||||
if (position < layoutManager.findFirstVisibleItemPosition()) {
|
||||
if (position == 0 || position < layoutManager.findFirstVisibleItemPosition()) {
|
||||
postScroll(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart < layoutManager.findFirstVisibleItemPosition()) {
|
||||
if (positionStart == 0 || positionStart < layoutManager.findFirstVisibleItemPosition()) {
|
||||
postScroll(positionStart)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ class ShelfAdapter(
|
||||
.addDelegate(errorStateListAD(listener))
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence {
|
||||
val item = items.getOrNull(position) as? ShelfSectionModel
|
||||
return item?.getTitle(context.resources) ?: ""
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val item = items.getOrNull(position) as? ShelfSectionModel ?: return null
|
||||
return item.getTitle(context.resources)
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
|
||||
@@ -23,9 +23,10 @@ class ShelfSettingsViewModel @Inject constructor(
|
||||
|
||||
val content = combine(
|
||||
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
|
||||
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
|
||||
favouritesRepository.observeCategories(),
|
||||
) { sections, categories ->
|
||||
buildList(sections, categories)
|
||||
) { sections, isTrackerEnabled, categories ->
|
||||
buildList(sections, isTrackerEnabled, categories)
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
|
||||
|
||||
private var updateJob: Job? = null
|
||||
@@ -64,13 +65,18 @@ class ShelfSettingsViewModel @Inject constructor(
|
||||
|
||||
private fun buildList(
|
||||
sections: List<ShelfSection>,
|
||||
isTrackerEnabled: Boolean,
|
||||
categories: List<FavouriteCategory>
|
||||
): List<ShelfSettingsItemModel> {
|
||||
val result = ArrayList<ShelfSettingsItemModel>()
|
||||
val sectionsList = ShelfSection.values().toMutableList()
|
||||
if (!isTrackerEnabled) {
|
||||
sectionsList.remove(ShelfSection.UPDATED)
|
||||
}
|
||||
for (section in sections) {
|
||||
sectionsList.remove(section)
|
||||
result.addSection(section, true, categories)
|
||||
if (sectionsList.remove(section)) {
|
||||
result.addSection(section, true, categories)
|
||||
}
|
||||
}
|
||||
for (section in sectionsList) {
|
||||
result.addSection(section, false, categories)
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.suggestions.ui
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -17,6 +16,7 @@ import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SuggestionsViewModel @Inject constructor(
|
||||
@@ -26,7 +26,7 @@ class SuggestionsViewModel @Inject constructor(
|
||||
|
||||
override val content = combine(
|
||||
repository.observeAll(),
|
||||
createListModeFlow(),
|
||||
listModeFlow,
|
||||
) { list, mode ->
|
||||
when {
|
||||
list.isEmpty() -> listOf(
|
||||
@@ -37,6 +37,7 @@ class SuggestionsViewModel @Inject constructor(
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
|
||||
else -> list.toUi(mode)
|
||||
}
|
||||
}.onStart {
|
||||
|
||||
@@ -144,6 +144,10 @@ class FeedFragment :
|
||||
startActivity(DetailsActivity.newIntent(context ?: return, item))
|
||||
}
|
||||
|
||||
override fun onReadClick(manga: Manga, view: View) = Unit
|
||||
|
||||
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) = Unit
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = FeedFragment()
|
||||
|
||||
@@ -34,7 +34,7 @@ class UpdatesViewModel @Inject constructor(
|
||||
|
||||
override val content = combine(
|
||||
repository.observeUpdatedManga(),
|
||||
createListModeFlow(),
|
||||
listModeFlow,
|
||||
) { mangaMap, mode ->
|
||||
when {
|
||||
mangaMap.isEmpty() -> listOf(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="5dp" />
|
||||
android:shape="oval">
|
||||
<solid android:color="?colorAccent" />
|
||||
<padding
|
||||
android:bottom="2dp"
|
||||
android:left="2dp"
|
||||
android:right="2dp"
|
||||
android:top="2dp" />
|
||||
</shape>
|
||||
</shape>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="5dp" />
|
||||
android:shape="oval">
|
||||
<solid android:color="?attr/colorTertiary" />
|
||||
<padding
|
||||
android:bottom="2dp"
|
||||
android:left="2dp"
|
||||
android:right="2dp"
|
||||
android:top="2dp" />
|
||||
</shape>
|
||||
</shape>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="5dp" />
|
||||
android:shape="oval">
|
||||
<solid android:color="?colorBackgroundFloating" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
@@ -12,4 +11,4 @@
|
||||
android:left="2dp"
|
||||
android:right="2dp"
|
||||
android:top="2dp" />
|
||||
</shape>
|
||||
</shape>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="5dp" />
|
||||
android:shape="oval">
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="?android:textColorTertiary" />
|
||||
@@ -11,4 +10,4 @@
|
||||
android:left="2dp"
|
||||
android:right="2dp"
|
||||
android:top="2dp" />
|
||||
</shape>
|
||||
</shape>
|
||||
|
||||
15
app/src/main/res/drawable/bg_circle_button.xml
Normal file
15
app/src/main/res/drawable/bg_circle_button.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="@color/selector_overlay">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?colorSurface" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/selector_overlay" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,7 +1,7 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="14dp"
|
||||
android:height="14dp"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
|
||||
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
|
||||
android:id="@+id/progressView"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_width="@dimen/card_indicator_size"
|
||||
android:layout_height="@dimen/card_indicator_size"
|
||||
android:layout_margin="@dimen/card_indicator_offset"
|
||||
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
|
||||
app:layout_constraintEnd_toEndOf="@id/imageView_cover" />
|
||||
|
||||
|
||||
100
app/src/main/res/layout-w600dp/fragment_tools.xml
Normal file
100
app/src/main/res/layout-w600dp/fragment_tools.xml
Normal file
@@ -0,0 +1,100 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
<include
|
||||
android:id="@+id/card_update"
|
||||
layout="@layout/layout_app_update"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_normal"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<include
|
||||
android:id="@+id/layout_sync"
|
||||
layout="@layout/layout_synchronization"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<include
|
||||
android:id="@+id/layout_storage"
|
||||
layout="@layout/layout_memory_usage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/card_update" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switch_incognito"
|
||||
style="?listItemTextViewStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:layout_marginTop="2dp"
|
||||
android:drawableStart="@drawable/ic_incognito"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:text="@string/incognito_mode"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
android:textColor="?attr/colorControlNormal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_sync"
|
||||
app:layout_goneMarginTop="8dp" />
|
||||
|
||||
<org.koitharu.kotatsu.base.ui.widgets.ListItemTextView
|
||||
android:id="@+id/button_downloads"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:drawableStart="@drawable/ic_download"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:text="@string/downloads"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@id/switch_incognito" />
|
||||
|
||||
<org.koitharu.kotatsu.base.ui.widgets.ListItemTextView
|
||||
android:id="@+id/button_settings"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?android:listPreferredItemHeightSmall"
|
||||
android:drawableStart="@drawable/ic_settings"
|
||||
android:drawablePadding="?android:listPreferredItemPaddingStart"
|
||||
android:paddingStart="?android:listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:text="@string/settings"
|
||||
android:textAppearance="?attr/textAppearanceButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_downloads" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -34,9 +34,9 @@
|
||||
|
||||
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
|
||||
android:id="@+id/progressView"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_width="@dimen/card_indicator_size"
|
||||
android:layout_height="@dimen/card_indicator_size"
|
||||
android:layout_margin="@dimen/card_indicator_offset"
|
||||
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
|
||||
app:layout_constraintEnd_toEndOf="@id/imageView_cover" />
|
||||
|
||||
|
||||
@@ -13,13 +13,15 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="?android:listPreferredItemPaddingStart"
|
||||
android:background="@drawable/bg_badge_default"
|
||||
android:ellipsize="none"
|
||||
android:gravity="center"
|
||||
android:minWidth="26dp"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="center"
|
||||
android:textSize="14sp"
|
||||
tools:text="13"
|
||||
tools:textColor="?attr/colorOnPrimary" />
|
||||
|
||||
@@ -67,4 +69,4 @@
|
||||
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
|
||||
android:src="@drawable/ic_save_ok" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
app:cardCornerRadius="16dp">
|
||||
app:cardCornerRadius="16dp"
|
||||
tools:layout_width="140dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -31,10 +31,10 @@
|
||||
|
||||
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
|
||||
android:id="@+id/progressView"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_width="@dimen/card_indicator_size"
|
||||
android:layout_height="@dimen/card_indicator_size"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="4dp" />
|
||||
android:layout_margin="@dimen/card_indicator_offset" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
@@ -45,8 +45,7 @@
|
||||
android:elegantTextHeight="false"
|
||||
android:ellipsize="end"
|
||||
android:lines="2"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:padding="8dp"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -4,31 +4,29 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/manga_list_details_item_height"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="16dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/imageView_cover"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="98dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="13:18"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover"
|
||||
tools:src="@sample/covers" />
|
||||
|
||||
|
||||
<org.koitharu.kotatsu.history.ui.util.ReadingProgressView
|
||||
android:id="@+id/progressView"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_width="@dimen/card_indicator_size"
|
||||
android:layout_height="@dimen/card_indicator_size"
|
||||
android:layout_margin="@dimen/card_indicator_offset"
|
||||
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
|
||||
app:layout_constraintEnd_toEndOf="@id/imageView_cover" />
|
||||
|
||||
@@ -36,13 +34,13 @@
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textView_subtitle"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge"
|
||||
android:textSize="20sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
@@ -52,61 +50,68 @@
|
||||
android:id="@+id/textView_subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="none"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="2"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView_title"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/scrollView_tags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:baselineAligned="false"
|
||||
android:clipChildren="false"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:scrollbars="none"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView_subtitle">
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_subtitle"
|
||||
app:layout_goneMarginTop="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_tags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="none"
|
||||
android:gravity="center_vertical"
|
||||
android:requiresFadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
tools:text="@sample/genres" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_rating"
|
||||
<org.koitharu.kotatsu.base.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_tags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="4dp"
|
||||
android:elegantTextHeight="false"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="6dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
app:drawableEndCompat="@drawable/ic_star"
|
||||
tools:ignore="RtlSymmetry"
|
||||
tools:text="9.6" />
|
||||
app:chipSpacingHorizontal="6dp"
|
||||
app:chipSpacingVertical="6dp"
|
||||
app:singleLine="true" />
|
||||
</HorizontalScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
<RatingBar
|
||||
android:id="@+id/ratingBar"
|
||||
style="?android:ratingBarStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:isIndicator="true"
|
||||
android:paddingVertical="8dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintStart_toStartOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/imageView_cover" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/button_read"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/bg_circle_button"
|
||||
android:contentDescription="@string/read"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_read"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/scrollView_tags"
|
||||
app:layout_constraintVertical_bias="1" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.Material3.CardView.Outlined"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/Widget.Material3.CardView.Outlined"
|
||||
app:cardBackgroundColor="?scrimBackground">
|
||||
|
||||
<org.koitharu.kotatsu.base.ui.widgets.CoverImageView
|
||||
@@ -17,17 +17,17 @@
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="@dimen/card_indicator_size"
|
||||
android:layout_height="@dimen/card_indicator_size"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_margin="@dimen/card_indicator_offset"
|
||||
android:ellipsize="none"
|
||||
android:gravity="center"
|
||||
android:minWidth="26dp"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="center"
|
||||
android:textColor="?attr/colorOnTertiary"
|
||||
android:textSize="14sp"
|
||||
tools:background="@drawable/bg_badge_default"
|
||||
tools:text="2" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -13,18 +13,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="86dp"
|
||||
android:layout_height="86dp"
|
||||
android:background="@drawable/bg_circle"
|
||||
android:contentDescription="@null"
|
||||
android:padding="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_totoro" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textPrimary"
|
||||
android:layout_width="0dp"
|
||||
@@ -33,7 +21,7 @@
|
||||
android:text="@string/app_update_available"
|
||||
android:textAppearance="?attr/textAppearanceTitleLarge"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
@@ -44,10 +32,11 @@
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:text="@string/new_version_s"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/textPrimary" />
|
||||
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textPrimary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textChangelog"
|
||||
@@ -55,14 +44,25 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_normal"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:background="@drawable/bg_badge_empty"
|
||||
android:fontFamily="monospace"
|
||||
android:padding="8dp"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textSecondary"
|
||||
tools:text="- Fixes\n- Improvements" />
|
||||
tools:text="- Fixes\n- Improvements"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_changelog"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:text="@string/details"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_download"
|
||||
app:layout_constraintTop_toBottomOf="@id/textChangelog" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_download"
|
||||
@@ -74,7 +74,6 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/textChangelog" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
24
app/src/main/res/menu/opt_favourites.xml
Normal file
24
app/src/main/res/menu/opt_favourites.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_order"
|
||||
android:orderInCategory="50"
|
||||
android:title="@string/sort_order">
|
||||
|
||||
<menu>
|
||||
|
||||
<group android:id="@+id/group_order" />
|
||||
|
||||
</menu>
|
||||
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit"
|
||||
android:orderInCategory="50"
|
||||
android:title="@string/edit_category"
|
||||
android:titleCondensed="@string/edit" />
|
||||
|
||||
</menu>
|
||||
@@ -390,4 +390,5 @@
|
||||
<string name="reader_control_ltr">Ergonomische Leserkontrolle</string>
|
||||
<string name="reader_control_ltr_summary">Tippe auf den rechten Rand oder drücke die rechte Taste, um immer zur nächsten Seite zu wechseln</string>
|
||||
<string name="reader_slider">Seitenwechsel-Schieberegler anzeigen</string>
|
||||
<string name="source_disabled">Quelle deaktiviert</string>
|
||||
</resources>
|
||||
@@ -396,4 +396,6 @@
|
||||
<string name="network_unavailable">La red no está disponible</string>
|
||||
<string name="compact">Compacta</string>
|
||||
<string name="network_unavailable_hint">Enciende la Wi-Fi o la red móvil para leer los mangas en línea</string>
|
||||
<string name="source_disabled">Fuente desactivada</string>
|
||||
<string name="prefetch_content">Precargar el contenido</string>
|
||||
</resources>
|
||||
@@ -395,4 +395,5 @@
|
||||
<string name="compact">Compact</string>
|
||||
<string name="server_error">Erreur côté serveur (%1$d). Veuillez réessayer plus tard</string>
|
||||
<string name="clear_new_chapters_counters">Effacer aussi les informations sur les nouveaux chapitres</string>
|
||||
<string name="source_disabled">Source désactivée</string>
|
||||
</resources>
|
||||
@@ -320,4 +320,6 @@
|
||||
<string name="invalid_domain_message">Dominio non valido</string>
|
||||
<string name="select_range">Seleziona l\'intervallo</string>
|
||||
<string name="not_found_404">Contenuto non trovato o rimosso</string>
|
||||
<string name="compact">Compatto</string>
|
||||
<string name="source_disabled">Fonte disabilitata</string>
|
||||
</resources>
|
||||
43
app/src/main/res/values-pl/plurals.xml
Normal file
43
app/src/main/res/values-pl/plurals.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="new_chapters">
|
||||
<item quantity="one">%1$d nowy rozdział</item>
|
||||
<item quantity="few">%1$d nowe rozdziały</item>
|
||||
<item quantity="many">%1$d nowych rozdziałów</item>
|
||||
</plurals>
|
||||
<plurals name="minutes_ago">
|
||||
<item quantity="one">%1$d minutę temu</item>
|
||||
<item quantity="few">%1$d minuty temu</item>
|
||||
<item quantity="many">%1$d minut temu</item>
|
||||
</plurals>
|
||||
<plurals name="pages">
|
||||
<item quantity="one">Łącznie %1$d strona</item>
|
||||
<item quantity="few">Łącznie %1$d strony</item>
|
||||
<item quantity="many">Łącznie %1$d stron</item>
|
||||
</plurals>
|
||||
<plurals name="hours_ago">
|
||||
<item quantity="one">%1$d godzinę temu</item>
|
||||
<item quantity="few">%1$d godziny temu</item>
|
||||
<item quantity="many">%1$d godzin temu</item>
|
||||
</plurals>
|
||||
<plurals name="days_ago">
|
||||
<item quantity="one">%1$d dzień temu</item>
|
||||
<item quantity="few">%1$d dni temu</item>
|
||||
<item quantity="many">%1$d dni temu</item>
|
||||
</plurals>
|
||||
<plurals name="items">
|
||||
<item quantity="one">%1$d przedmiot</item>
|
||||
<item quantity="few">%1$d przedmioty</item>
|
||||
<item quantity="many">%1$d przedmiotów</item>
|
||||
</plurals>
|
||||
<plurals name="chapters_from_x">
|
||||
<item quantity="one">%1$d rozdział z %2$d</item>
|
||||
<item quantity="few">%1$d rozdziały z %2$d</item>
|
||||
<item quantity="many">%1$d rozdziałów z %2$d</item>
|
||||
</plurals>
|
||||
<plurals name="chapters">
|
||||
<item quantity="one">%1$d rozdział</item>
|
||||
<item quantity="few">%1$d rozdziały</item>
|
||||
<item quantity="many">%1$d rozdziałów</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
397
app/src/main/res/values-pl/strings.xml
Normal file
397
app/src/main/res/values-pl/strings.xml
Normal file
@@ -0,0 +1,397 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="favourites">Ulubione</string>
|
||||
<string name="history">Historia</string>
|
||||
<string name="error_occurred">Napotkano błąd</string>
|
||||
<string name="details">Szczegółowy</string>
|
||||
<string name="chapters">Rozdziały</string>
|
||||
<string name="list">Lista</string>
|
||||
<string name="detailed_list">Lista szczegółowa</string>
|
||||
<string name="grid">Siatka</string>
|
||||
<string name="list_mode">Tryb listy</string>
|
||||
<string name="settings">Ustawienia</string>
|
||||
<string name="loading_">Ładowanie…</string>
|
||||
<string name="chapter_d_of_d">Rozdział %1$d z %2$d</string>
|
||||
<string name="close">Zamknij</string>
|
||||
<string name="clear_history">Wyczyść historię</string>
|
||||
<string name="add">Dodaj</string>
|
||||
<string name="save">Zapisz</string>
|
||||
<string name="share">Udostępnij</string>
|
||||
<string name="search">Szukaj</string>
|
||||
<string name="search_manga">Szukaj mang</string>
|
||||
<string name="manga_downloading_">Pobieranie…</string>
|
||||
<string name="download_complete">Pobrano</string>
|
||||
<string name="downloads">Pobrane</string>
|
||||
<string name="by_name">Nazwa</string>
|
||||
<string name="popular">Popularność</string>
|
||||
<string name="newest">Najnowsze</string>
|
||||
<string name="by_rating">Ocena</string>
|
||||
<string name="filter">Filtry</string>
|
||||
<string name="light">Jasny</string>
|
||||
<string name="dark">Ciemny</string>
|
||||
<string name="pages">Strony</string>
|
||||
<string name="clear">Wyczyść</string>
|
||||
<string name="remove">Usuń</string>
|
||||
<string name="share_image">Udostępnij zdjęcie</string>
|
||||
<string name="delete">Usuń</string>
|
||||
<string name="no_description">Brak opisu</string>
|
||||
<string name="read_mode">Tryb czytania</string>
|
||||
<string name="network_error">Błąd sieci</string>
|
||||
<string name="computing_">Obliczanie…</string>
|
||||
<string name="try_again">Spróbuj ponownie</string>
|
||||
<string name="nothing_found">Nic nie znaleziono</string>
|
||||
<string name="history_is_empty">Brak historii</string>
|
||||
<string name="read">Czytaj</string>
|
||||
<string name="you_have_not_favourites_yet">Brak ulubionych</string>
|
||||
<string name="add_to_favourites">Dodaj do ulubionych</string>
|
||||
<string name="add_new_category">Nowa kategoria</string>
|
||||
<string name="create_shortcut">Stwórz skrót</string>
|
||||
<string name="share_s">Udostępnij %s</string>
|
||||
<string name="processing_">Przetwarzanie…</string>
|
||||
<string name="updated">Zaktualizowane</string>
|
||||
<string name="_s_removed_from_history">„%s” usunięte z historii</string>
|
||||
<string name="save_page">Zapisz stronę</string>
|
||||
<string name="page_saved">Zapisano</string>
|
||||
<string name="vibration">Wibracje</string>
|
||||
<string name="manga_shelf">Biblioteka</string>
|
||||
<string name="recent_manga">Ostatnie</string>
|
||||
<string name="black_dark_theme">Tryb czarny</string>
|
||||
<string name="preparing_">Przygotowywanie…</string>
|
||||
<string name="file_not_found">Plik nieznaleziony</string>
|
||||
<string name="yesterday">Wczoraj</string>
|
||||
<string name="long_ago">Dawno temu</string>
|
||||
<string name="group">Grupa</string>
|
||||
<string name="today">Dzisiaj</string>
|
||||
<string name="sign_in">Zaloguj</string>
|
||||
<string name="next">Dalej</string>
|
||||
<string name="confirm">Potwierdź</string>
|
||||
<string name="welcome">Witaj</string>
|
||||
<string name="state_finished">Skończone</string>
|
||||
<string name="state_ongoing">W trakcie</string>
|
||||
<string name="screenshots_allow">Zezwól</string>
|
||||
<string name="suggestions">Proponowane</string>
|
||||
<string name="suggestions_enable">Włącz propozycje</string>
|
||||
<string name="enabled">Włączone</string>
|
||||
<string name="disabled">Wyłączone</string>
|
||||
<string name="never">Nigdy</string>
|
||||
<string name="always">Zawsze</string>
|
||||
<string name="search_chapters">Znajdź rozdział</string>
|
||||
<string name="percent_string_pattern">%1$s%%</string>
|
||||
<string name="appearance">Wygląd</string>
|
||||
<string name="hide">Schowaj</string>
|
||||
<string name="sync">Synchronizacja</string>
|
||||
<string name="sync_title">Synchronizuj swoje dane</string>
|
||||
<string name="name">Nazwa</string>
|
||||
<string name="edit">Edytuj</string>
|
||||
<string name="logout">Wyloguj</string>
|
||||
<string name="undo">Cofnij</string>
|
||||
<string name="send">Wyślij</string>
|
||||
<string name="status_planned">Planowane</string>
|
||||
<string name="status_reading">Czytane</string>
|
||||
<string name="status_re_reading">Czytane ponownie</string>
|
||||
<string name="status_completed">Skończone</string>
|
||||
<string name="show_all">Pokaż wszystkie</string>
|
||||
<string name="select_range">Wybierz zakres</string>
|
||||
<string name="clear_all_history">Wyczyść całą historię</string>
|
||||
<string name="last_2_hours">Ostatnie 2 godziny</string>
|
||||
<string name="history_cleared">Historia wyczyszczona</string>
|
||||
<string name="manage">Zarządzaj</string>
|
||||
<string name="random">Losowe</string>
|
||||
<string name="empty">Puste</string>
|
||||
<string name="changelog">Lista zmian</string>
|
||||
<string name="explore">Przeglądaj</string>
|
||||
<string name="available">Dostępne</string>
|
||||
<string name="options">Ustawienia</string>
|
||||
<string name="source_disabled">Źródło wyłączone</string>
|
||||
<string name="compact">Kompaktowy</string>
|
||||
<string name="server_error">Błąd po stronie serwera (%1$d). Sprónuj ponownie później</string>
|
||||
<string name="network_unavailable">Sieć niedostępna</string>
|
||||
<string name="different_languages">Inne języki</string>
|
||||
<string name="discard">Odrzuć</string>
|
||||
<string name="brightness">Jasność</string>
|
||||
<string name="contrast">Kontrast</string>
|
||||
<string name="color_correction">Korekcja kolorów</string>
|
||||
<string name="seconds_pattern">%ss</string>
|
||||
<string name="off_short">Wyłącz</string>
|
||||
<string name="automatic_scroll">Automatyczne przewijanie</string>
|
||||
<string name="no_chapters">Brak rozdziałów</string>
|
||||
<string name="incognito_mode">Tryb incognito</string>
|
||||
<string name="downloading_manga">Pobieranie mangi</string>
|
||||
<string name="removed_from_favourites">Usunięto z ulubionych</string>
|
||||
<string name="enter_email_text">Wprowadź swój email aby kontynuować</string>
|
||||
<string name="storage_usage">Wykorzystana pamięć</string>
|
||||
<string name="saved_manga">Zapisane mangi</string>
|
||||
<string name="no_bookmarks_yet">Brak zakładek</string>
|
||||
<string name="no_bookmarks_summary">Możesz tworzyć zakładki w trakcie czytania mangi</string>
|
||||
<string name="bookmarks_removed">Zakładki usunięte</string>
|
||||
<string name="appwidget_recent_description">Twoje ostatnio czytane mangi</string>
|
||||
<string name="disable_all">Wyłącz wszystkie</string>
|
||||
<string name="disable_battery_optimization">Wyłącz optymalizację baterii</string>
|
||||
<string name="detect_reader_mode">Autowykrywanie trybu czytania</string>
|
||||
<string name="removed_from_history">Usunięte z historii</string>
|
||||
<string name="bookmark_added">Dodano zakładkę</string>
|
||||
<string name="bookmark_removed">Usunięto zakładkę</string>
|
||||
<string name="bookmarks">Zakładki</string>
|
||||
<string name="bookmark_remove">Usuń zakładkę</string>
|
||||
<string name="bookmark_add">Dodaj zakładkę</string>
|
||||
<string name="empty_favourite_categories">Brak ulubionych kategorii</string>
|
||||
<string name="edit_category">Edytuj kategorię</string>
|
||||
<string name="notifications_enable">Włącz powiadomienia</string>
|
||||
<string name="back">Wróć</string>
|
||||
<string name="account_already_exists">Konto już istnieje</string>
|
||||
<string name="canceled">Anulowano</string>
|
||||
<string name="download_slowdown">Zwolnienie pobierania</string>
|
||||
<string name="chapters_empty">Brak rozdziałów w tej mandze</string>
|
||||
<string name="various_languages">Różne języki</string>
|
||||
<string name="only_using_wifi">Tylko na Wi-Fi</string>
|
||||
<string name="screenshots_block_all">Zawsze blokuj</string>
|
||||
<string name="date_format">Format daty</string>
|
||||
<string name="genres">Gatunki</string>
|
||||
<string name="find_genre">Znajdź gatunek</string>
|
||||
<string name="read_more">Czytaj więcej</string>
|
||||
<string name="other">Inne</string>
|
||||
<string name="captcha_solve">Rozwiąż</string>
|
||||
<string name="captcha_required">Wymagane CAPTCHA</string>
|
||||
<string name="silent">Cichy</string>
|
||||
<string name="tap_to_try_again">Dotknij aby spróbować ponownie</string>
|
||||
<string name="just_now">Teraz</string>
|
||||
<string name="data_restored">Przywrócone</string>
|
||||
<string name="zoom_mode_fit_width">Dopasuj do szerokości</string>
|
||||
<string name="zoom_mode_fit_height">Dopasuj do wysokości</string>
|
||||
<string name="zoom_mode_fit_center">Dopasuj do środka</string>
|
||||
<string name="create_category">Nowa kategoria</string>
|
||||
<string name="no_update_available">Brak nowych aktualizacji</string>
|
||||
<string name="check_for_updates">Sprawdź dostępność aktualizacji</string>
|
||||
<string name="checking_for_updates">Sprawdzanie aktualizacji…</string>
|
||||
<string name="app_version">Wersja %s</string>
|
||||
<string name="about">O aplikacji</string>
|
||||
<string name="categories_">Kategorie…</string>
|
||||
<string name="rename">Zmień nazwę</string>
|
||||
<string name="remove_category">Usuń</string>
|
||||
<string name="text_empty_holder_primary">Jest tu dosyć pusto…</string>
|
||||
<string name="favourites_categories">Ulubione kategorie</string>
|
||||
<string name="light_indicator">Powiadomienie LED</string>
|
||||
<string name="new_chapters">Nowe rozdziały</string>
|
||||
<string name="close_menu">Zamknij kartę</string>
|
||||
<string name="open_menu">Otwórz kartę</string>
|
||||
<string name="local_storage">Pamięć wewnętrzna</string>
|
||||
<string name="text_shelf_holder_primary">Tutaj będą wyświetlane Twoje mangi</string>
|
||||
<string name="text_shelf_holder_secondary">Znajdź materiały do czytania w zakładce „Przeglądaj”</string>
|
||||
<string name="text_feed_holder">W tym miejscu pojawią się powiadomienia o nowych rozdziałach z mang które czytasz</string>
|
||||
<string name="pages_cache">Strony w pamięci podręcznej</string>
|
||||
<string name="pages_animation">Animacja przewracania strony</string>
|
||||
<string name="other_cache">Inne rzeczy w pamięci podręcznej</string>
|
||||
<string name="open_in_browser">Otwórz w przeglądarce</string>
|
||||
<string name="show_pages_numbers">Numerowane strony</string>
|
||||
<string name="notifications">Powiadomienia</string>
|
||||
<string name="notification_sound">Dźwięk powiadomień</string>
|
||||
<string name="notifications_settings">Ustawienia powiadomień</string>
|
||||
<string name="remote_sources">Zewnętrzne źródła</string>
|
||||
<string name="theme">Motyw</string>
|
||||
<string name="automatic">Systemowy</string>
|
||||
<string name="history_and_cache">Historia i pamięć podręczna</string>
|
||||
<string name="clear_pages_cache">Wyczyść pamięć podręczną stron</string>
|
||||
<string name="cache">Pamięć podręczna</string>
|
||||
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
|
||||
<string name="grid_size">Wielkość siatki</string>
|
||||
<string name="search_on_s">Szukaj na %s</string>
|
||||
<string name="delete_manga">Usuń mangę</string>
|
||||
<string name="_continue">Dalej</string>
|
||||
<string name="dont_ask_again">Nie pytaj ponownie</string>
|
||||
<string name="cancelling_">Anulowanie…</string>
|
||||
<string name="error">Błąd</string>
|
||||
<string name="search_history_cleared">Wyczyszczone</string>
|
||||
<string name="internal_storage">Pamięć wewnętrzna</string>
|
||||
<string name="external_storage">Pamięć zewnętrzna</string>
|
||||
<string name="domain">Domena</string>
|
||||
<string name="application_update">Sprawdź dostępność nowej wersji aplikacji</string>
|
||||
<string name="app_update_available">Nowa wersja aplikacji jest dostępna</string>
|
||||
<string name="show_notification_app_update">Pokaż powiadomienie gdy nowa wersja jest dostępna</string>
|
||||
<string name="large_manga_save_confirm">Ta manga ma %s. Zapisać wszystko?</string>
|
||||
<string name="save_manga">Zapisz</string>
|
||||
<string name="download">Pobierz</string>
|
||||
<string name="read_from_start">Czytaj od początku</string>
|
||||
<string name="category_delete_confirm">Usunąć kategorię „%s” z Twoich ulubionych? Wszystkie mangi w niej będą z niej usunięte.</string>
|
||||
<string name="text_categories_holder">Możesz użyć kategorii do organizowania swoich ulubionych. Kliknij «+» aby stworzyć kategorię</string>
|
||||
<string name="text_local_holder_primary">Najpierw coś zapisz</string>
|
||||
<string name="not_available">Niedostępne</string>
|
||||
<string name="done">Zapisz</string>
|
||||
<string name="all_favourites">Wszystkie ulubione</string>
|
||||
<string name="favourites_category_empty">Pusta kategoria</string>
|
||||
<string name="read_later">Czytaj później</string>
|
||||
<string name="updates">Aktualizacje</string>
|
||||
<string name="new_version_s">Nowa wersja: %s</string>
|
||||
<string name="size_s">Wielkość: %s</string>
|
||||
<string name="waiting_for_network">Czekanie na sieć…</string>
|
||||
<string name="rotate_screen">Obróć ekran</string>
|
||||
<string name="update">Odśwież</string>
|
||||
<string name="track_sources">Szukaj aktualizacji</string>
|
||||
<string name="dont_check">Nie sprawdzaj</string>
|
||||
<string name="enter_password">Wprowadź hasło</string>
|
||||
<string name="wrong_password">Złe hasło</string>
|
||||
<string name="protect_application">Chroń aplikację</string>
|
||||
<string name="protect_application_summary">Pytaj o hasło przy starcie Kotatsu</string>
|
||||
<string name="repeat_password">Wprowadź ponownie hasło</string>
|
||||
<string name="black_dark_theme_summary">Zużywa mniej prądu na ekranach AMOLED</string>
|
||||
<string name="backup_restore">Kopia zapasowa i przywracanie</string>
|
||||
<string name="create_backup">Utwórz kopię zapasową danych</string>
|
||||
<string name="restore_backup">Przywróć z kopii zapasowej</string>
|
||||
<string name="nsfw">18+</string>
|
||||
<string name="enabled_d_of_d">%1$d na %2$d włączone</string>
|
||||
<string name="enter_category_name">Wprowadź nazwę kategorii</string>
|
||||
<string name="standard">Standardowy</string>
|
||||
<string name="webtoon">Webtoon</string>
|
||||
<string name="reader_settings">Ustawienia czytnika</string>
|
||||
<string name="switch_pages">Zmiana strony</string>
|
||||
<string name="volume_buttons">Przyciski głośności</string>
|
||||
<string name="warning">Uwaga</string>
|
||||
<string name="taps_on_edges">Dotknięcie krawędzi</string>
|
||||
<string name="updates_feed_cleared">Wyczyszczone</string>
|
||||
<string name="scale_mode">Tryb skalowania</string>
|
||||
<string name="clear_cookies">Wyczyść ciasteczka</string>
|
||||
<string name="cookies_cleared">Wszystkie ciasteczka wyczyszczone</string>
|
||||
<string name="search_only_on_s">Szukaj tylko na %s</string>
|
||||
<string name="about_app_translation_summary">Przetłumacz tą aplikację</string>
|
||||
<string name="about_app_translation">Tłumaczenie</string>
|
||||
<string name="error_empty_name">Musisz wpisać nazwę</string>
|
||||
<string name="available_sources">Dostępne źródła</string>
|
||||
<string name="dynamic_theme">Motyw dynamiczny</string>
|
||||
<string name="gestures_only">Tylko gesty</string>
|
||||
<string name="cannot_find_available_storage">Brak dostępnej pamięci</string>
|
||||
<string name="other_storage">Inny</string>
|
||||
<string name="search_results">Wyniki wyszukiwania</string>
|
||||
<string name="related">Szukaj podobnych</string>
|
||||
<string name="data_restored_success">Wszystkie dane zostały przywrócone</string>
|
||||
<string name="data_restored_with_errors">Dane zostały przywrócone, ale z błędami</string>
|
||||
<string name="reverse">Od tyłu</string>
|
||||
<string name="text_downloads_holder">Brak aktywnych pobrań</string>
|
||||
<string name="system_default">Domyślny</string>
|
||||
<string name="screenshots_policy">Polityka zrzutów ekranu</string>
|
||||
<string name="suggestions_excluded_genres">Wyklucz gatunki</string>
|
||||
<string name="suggestions_excluded_genres_summary">Określ gatunki, których nie chcesz widzieć w sugestiach</string>
|
||||
<string name="logged_in_as">Zalogowano jako %s</string>
|
||||
<string name="onboard_text">Wybierz języki, w których chcesz czytać mangi. Możesz zmienić to później w ustawieniach.</string>
|
||||
<string name="report">Zgłoś</string>
|
||||
<string name="data_deletion">Usuwanie danych</string>
|
||||
<string name="invalid_domain_message">Nieważna domena</string>
|
||||
<string name="reorder">Zmień kolejność</string>
|
||||
<string name="exit_confirmation">Potwierdzenie wyjścia</string>
|
||||
<string name="memory_usage_pattern">%s - %s</string>
|
||||
<string name="reader_info_pattern">Rozdz. %1$d/%2$d Str. %3$d/%4$d</string>
|
||||
<string name="network_unavailable_hint">Włącz Wi-Fi lub sieć komórkową, aby czytać mangę online</string>
|
||||
<string name="_import">Importuj</string>
|
||||
<string name="text_file_not_supported">Wybierz plik ZIP lub CBZ.</string>
|
||||
<string name="restart">Uruchom ponownie</string>
|
||||
<string name="clear_search_history">Wyczyść historię wyszukiwania</string>
|
||||
<string name="operation_not_supported">Ta operacja nie jest obsługiwana</string>
|
||||
<string name="wait_for_loading_finish">Poczekaj na zakończenie ładowania…</string>
|
||||
<string name="sort_order">Tryb sortowania</string>
|
||||
<string name="content">Treści</string>
|
||||
<string name="filter_load_error">Nie można załadować listy gatunków</string>
|
||||
<string name="status_on_hold">Wstrzymane</string>
|
||||
<string name="status_dropped">Porzucone</string>
|
||||
<string name="use_fingerprint">Użyj odcisku palca, jeśli jest dostępny</string>
|
||||
<string name="appwidget_shelf_description">Mangi z Twoich ulubionych</string>
|
||||
<string name="show_reading_indicators">Pokaż wskaźniki postępu czytania</string>
|
||||
<string name="show_reading_indicators_summary">Pokaż procent przeczytania w historii i ulubionych</string>
|
||||
<string name="exclude_nsfw_from_history_summary">Manga oznaczona jako NSFW nigdy nie zostanie dodana do historii, a Twoje postępy nie zostaną zapisane</string>
|
||||
<string name="dns_over_https">DNS przez HTTPS</string>
|
||||
<string name="default_mode">Tryb domyślny</string>
|
||||
<string name="text_clear_history_prompt">Trwale wyczyścić całą historię czytania?</string>
|
||||
<string name="_s_deleted_from_local_storage">„%s” usunięte z pamięci lokalnej</string>
|
||||
<string name="clear_updates_feed">Wyczyść tablicę aktualizacji</string>
|
||||
<string name="feed">Tablica</string>
|
||||
<string name="text_delete_local_manga">Usunąć trwale „%s” z urządzenia?</string>
|
||||
<string name="network_consumption_warning">Może to spowodować przeniesienie dużej ilości danych</string>
|
||||
<string name="clear_thumbs_cache">Wyczyść pamięć podręczną miniatur</string>
|
||||
<string name="text_search_holder_secondary">Spróbuj przeformułować zapytanie.</string>
|
||||
<string name="text_history_holder_primary">To co czytasz będzie wyświetlane tutaj</string>
|
||||
<string name="text_history_holder_secondary">Znajdź to, co warto przeczytać, w menu bocznym.</string>
|
||||
<string name="text_local_holder_secondary">Zapisz ze źródeł online lub zaimportuj pliki.</string>
|
||||
<string name="manga_save_location">Folder pobranych</string>
|
||||
<string name="feed_will_update_soon">Aktualizacja tablicy rozpocznie się wkrótce</string>
|
||||
<string name="passwords_mismatch">Niezgodne hasła</string>
|
||||
<string name="update_check_failed">Nie można wyszukać aktualizacji</string>
|
||||
<string name="right_to_left">Od prawej do lewej</string>
|
||||
<string name="zoom_mode_keep_start">Trzymaj na starcie</string>
|
||||
<string name="report_github">Utwórz problem na GitHubie</string>
|
||||
<string name="backup_information">Możesz utworzyć kopię zapasową swojej historii i ulubionych oraz przywrócić ją</string>
|
||||
<string name="reader_mode_hint">Wybrana konfiguracja zostanie zapamiętana dla tej mangi</string>
|
||||
<string name="chapters_checking_progress">Sprawdzanie nowych rozdziałów: %1$d z %2$d</string>
|
||||
<string name="clear_feed">Wyczyść tablicę</string>
|
||||
<string name="text_clear_updates_feed_prompt">Wyczyścić trwale całą historię aktualizacji?</string>
|
||||
<string name="check_for_new_chapters">Szukanie nowych rozdziałów</string>
|
||||
<string name="auth_required">Zaloguj się, aby wyświetlić tę zawartość</string>
|
||||
<string name="default_s">Domyślnie: %s</string>
|
||||
<string name="_and_x_more">…i jeszcze %1$d</string>
|
||||
<string name="protect_application_subtitle">Wprowadź hasło, aby uruchomić aplikację</string>
|
||||
<string name="password_length_hint">Hasło musi mieć co najmniej 4 znaki</string>
|
||||
<string name="text_clear_search_history_prompt">Trwale usunąć wszystkie ostatnie zapytania wyszukiwania?</string>
|
||||
<string name="backup_saved">Zapisano kopię zapasową</string>
|
||||
<string name="tracker_warning">Systemy niektórych urządzeń inaczej się zachowują. Może to zakłócać wykonywanie zadań w tle.</string>
|
||||
<string name="queued">W kolejce</string>
|
||||
<string name="chapter_is_missing_text">Pobierz lub przeczytaj ten brakujący rozdział online.</string>
|
||||
<string name="chapter_is_missing">Brak rozdziału</string>
|
||||
<string name="about_feedback">Komentarz</string>
|
||||
<string name="about_feedback_4pda">Temat na 4PDA</string>
|
||||
<string name="auth_complete">Uprawniony</string>
|
||||
<string name="auth_not_supported_by">Logowanie na %s nie jest obsługiwane</string>
|
||||
<string name="text_clear_cookies_prompt">Zostaniesz wylogowany ze wszystkich źródeł</string>
|
||||
<string name="exclude_nsfw_from_history">Wyklucz mangi NSFW z historii</string>
|
||||
<string name="enabled_sources">Wykorzystane źródła</string>
|
||||
<string name="dynamic_theme_summary">Stosuje motyw utworzony na podstawie schematu kolorów Twojej tapety</string>
|
||||
<string name="importing_progress">Importowanie mangi: %1$d z %2$d</string>
|
||||
<string name="screenshots_block_nsfw">Zablokuj na NSFW</string>
|
||||
<string name="suggestions_summary">Proponuj mangi na podstawie Twoich preferencji</string>
|
||||
<string name="suggestions_info">Wszystkie dane są analizowane lokalnie na tym urządzeniu. Twoje dane osobowe nie są przekazywane do żadnych usług</string>
|
||||
<string name="text_suggestion_holder">Zacznij czytać mangę, a otrzymasz spersonalizowane sugestie</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Nie proponuj mang NSFW</string>
|
||||
<string name="reset_filter">Zresetuj filtr</string>
|
||||
<string name="preload_pages">Ładuj wstępnie strony</string>
|
||||
<string name="suggestions_updating">Aktualizowanie sugestii</string>
|
||||
<string name="text_delete_local_manga_batch">Trwale usunąć wybrane elementy z urządzenia?</string>
|
||||
<string name="removal_completed">Usuwanie zakończone</string>
|
||||
<string name="batch_manga_save_confirm">Pobrać wszystkie wybrane mangi i ich rozdziały? Może to zużyć dużo danych i pamięci.</string>
|
||||
<string name="parallel_downloads">Pobieranie równoległe</string>
|
||||
<string name="download_slowdown_summary">Pomaga uniknąć blokowania Twojego adresu IP</string>
|
||||
<string name="local_manga_processing">Przetwarzanie zapisanej mangi</string>
|
||||
<string name="chapters_will_removed_background">Rozdziały zostaną usunięte w tle. Może to zająć trochę czasu</string>
|
||||
<string name="email_enter_hint">Wpisz swój adres e-mail, aby kontynuować</string>
|
||||
<string name="new_sources_text">Dostępne są nowe źródła mang</string>
|
||||
<string name="check_new_chapters_title">Sprawdzaj dostępność nowych rozdziałów i informuj o nich</string>
|
||||
<string name="show_notification_new_chapters_on">Będziesz otrzymywać powiadomienia o aktualizacjach mang, które czytasz</string>
|
||||
<string name="show_notification_new_chapters_off">Nie będziesz otrzymywać powiadomień, ale nowe rozdziały będą podświetlane na listach</string>
|
||||
<string name="tracking">Śledzenie</string>
|
||||
<string name="detect_reader_mode_summary">Automatycznie wykryj, czy manga to webtoon</string>
|
||||
<string name="disable_battery_optimization_summary">Pomaga w sprawdzaniu aktualizacji w tle</string>
|
||||
<string name="crash_text">Coś poszło nie tak. Zgłoś błąd programistom, aby pomóc nam go naprawić.</string>
|
||||
<string name="clear_cookies_summary">Może pomóc w przypadku niektórych problemów. Wszystkie autoryzacje zostaną unieważnione</string>
|
||||
<string name="no_manga_sources">Brak źródeł mang</string>
|
||||
<string name="no_manga_sources_text">Włącz źródła mang do czytania mang online</string>
|
||||
<string name="categories_delete_confirm">Czy na pewno chcesz usunąć wybrane ulubione kategorie? Wszystkie w nich mangi zostaną usunięte i nie będzie można tego cofnąć.</string>
|
||||
<string name="confirm_exit">Naciśnij ponownie Wstecz, aby wyjść</string>
|
||||
<string name="exit_confirmation_summary">Naciśnij dwukrotnie przycisk Wstecz, aby wyjść z aplikacji</string>
|
||||
<string name="removed_from_s">Usunięto z „%s”</string>
|
||||
<string name="not_found_404">Treść nie została znaleziona lub została usunięta</string>
|
||||
<string name="app_update_available_s">Dostępna aktualizacja aplikacji: %s</string>
|
||||
<string name="reader_info_bar">Pokaż pasek informacji w czytniku</string>
|
||||
<string name="comics_archive">Archiwum komiksów</string>
|
||||
<string name="folder_with_images">Folder z obrazami</string>
|
||||
<string name="importing_manga">Importowanie mangi</string>
|
||||
<string name="import_completed">Importowanie zakończone</string>
|
||||
<string name="import_completed_hint">Możesz usunąć oryginalny plik z pamięci, aby zaoszczędzić miejsce</string>
|
||||
<string name="import_will_start_soon">Import rozpocznie się wkrótce</string>
|
||||
<string name="color_correction_hint">Wybrane ustawienia kolorów zostaną zapamiętane dla tej mangi</string>
|
||||
<string name="history_shortcuts">Pokaż ostatnie skróty do mang</string>
|
||||
<string name="history_shortcuts_summary">Pokaż ostatnie mangi po długim naciśnięciu ikony aplikacji</string>
|
||||
<string name="reader_control_ltr_summary">Stuknięcie w prawą krawędź lub naciśnięcie prawego klawisza zawsze powoduje przejście do następnej strony</string>
|
||||
<string name="reader_control_ltr">Ergonomiczne sterowanie czytnikiem</string>
|
||||
<string name="text_unsaved_changes_prompt">Zapisać czy odrzucić niezapisane zmiany?</string>
|
||||
<string name="error_no_space_left">Brak miejsca w urządzeniu</string>
|
||||
<string name="reader_slider">Pokaż suwak przełączania stron</string>
|
||||
<string name="webtoon_zoom">Powiększanie webtoon</string>
|
||||
<string name="webtoon_zoom_summary">Zezwalaj na gest powiększania/pomniejszania w trybie webtoon (beta)</string>
|
||||
<string name="clear_new_chapters_counters">Wyczyść też informacje o nowych rozdziałach</string>
|
||||
<string name="reset">Resetuj</string>
|
||||
<string name="manga_error_description_pattern">Szczegóły błędu:<br><tt>%1$s</tt><br><br>1. Spróbuj <a href=%2$s>otworzyć mangę w przeglądarce internetowej</a> aby upewnić się, że jest dostępna w źródle<br>2. Jeśli jest dostępna, wyślij raport o błędzie do programistów.</string>
|
||||
</resources>
|
||||
@@ -397,4 +397,5 @@
|
||||
<string name="server_error">Внутренняя ошибка сервера (%1$d). Повторите попытку позже</string>
|
||||
<string name="compact">Компактно</string>
|
||||
<string name="source_disabled">Источник отключен</string>
|
||||
<string name="prefetch_content">Предварительная загрузка содержимого</string>
|
||||
</resources>
|
||||
@@ -9,7 +9,7 @@
|
||||
<string name="add">Додати</string>
|
||||
<string name="save">Зберегти</string>
|
||||
<string name="local_storage">Локальне сховище</string>
|
||||
<string name="network_error">Не вдалося підключитися до Інтернету</string>
|
||||
<string name="network_error">Помилка мережі</string>
|
||||
<string name="details">Деталі</string>
|
||||
<string name="try_again">Спробуйте ще раз</string>
|
||||
<string name="open_menu">Відкрити меню</string>
|
||||
@@ -395,4 +395,6 @@
|
||||
<string name="webtoon_zoom_summary">Дозволити жести збільшення/зменшення масштабу в режимі вебтуну (бета)</string>
|
||||
<string name="reader_slider">Відображати повзунок перемикання сторінок</string>
|
||||
<string name="compact">Компактно</string>
|
||||
<string name="prefetch_content">Передвчасне завантаження контенту</string>
|
||||
<string name="source_disabled">Джерело відключено</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user