Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4c2197aa5d
|
|||
|
a679b6775d
|
|||
|
d3e4e97c6f
|
|||
|
d1b0af85c4
|
|||
| ce95e0657b | |||
|
|
6bb159a6d9 | ||
|
|
a75583f750 | ||
|
fff9df9609
|
|||
|
f9609edea5
|
|||
|
f1245742c0
|
|||
|
42d933ba83
|
|||
|
4df644e21f
|
|||
|
|
e4ba738c00 | ||
|
|
b7f09243aa | ||
|
|
50d4c41855 | ||
|
67adc8b681
|
|||
|
34fb4af9fe
|
|||
|
05241f73d9
|
|||
|
d666e4b967
|
|||
| b4bf607d3a | |||
|
|
a417d5aaa9 | ||
|
|
ad3b5dde91 | ||
|
|
ded7cdb71e |
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
versionCode = 574
|
||||
versionName = '6.0.1'
|
||||
versionCode = 576
|
||||
versionName = '6.0.3'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
||||
ksp {
|
||||
@@ -81,7 +81,7 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:3a76504380') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:2f7e704e21') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name="org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity"
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.browser.cloudflare
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.webkit.CookieManager
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
@@ -13,6 +14,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
|
||||
@@ -38,7 +40,13 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
|
||||
if (!catchingWebViewUnavailability {
|
||||
setContentView(
|
||||
ActivityBrowserBinding.inflate(
|
||||
layoutInflater
|
||||
)
|
||||
)
|
||||
}) {
|
||||
return
|
||||
}
|
||||
supportActionBar?.run {
|
||||
@@ -86,6 +94,11 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
viewBinding.webView.restoreState(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.opt_captcha, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.appbar.updatePadding(
|
||||
top = insets.top,
|
||||
@@ -104,6 +117,16 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_retry -> {
|
||||
viewBinding.webView.stopLoading()
|
||||
val targetUrl = intent?.dataString?.toHttpUrlOrNull()
|
||||
if (targetUrl != null) {
|
||||
clearCfCookies(targetUrl)
|
||||
viewBinding.webView.loadUrl(targetUrl.toString())
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
@@ -141,7 +164,15 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
|
||||
|
||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||
setTitle(title)
|
||||
supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
|
||||
supportActionBar?.subtitle =
|
||||
subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
|
||||
}
|
||||
|
||||
private fun clearCfCookies(url: HttpUrl) {
|
||||
cookieJar.removeCookies(url) { cookie ->
|
||||
val name = cookie.name
|
||||
name.startsWith("cf_") || name.startsWith("_cf") || name.startsWith("__cf")
|
||||
}
|
||||
}
|
||||
|
||||
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() {
|
||||
|
||||
@@ -20,25 +20,8 @@ interface ContentCache {
|
||||
|
||||
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
|
||||
|
||||
class Key(
|
||||
data class Key(
|
||||
val source: MangaSource,
|
||||
val url: String,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Key
|
||||
|
||||
if (source != other.source) return false
|
||||
return url == other.url
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = source.hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.room.Embedded
|
||||
import androidx.room.Junction
|
||||
import androidx.room.Relation
|
||||
|
||||
class MangaWithTags(
|
||||
data class MangaWithTags(
|
||||
@Embedded val manga: MangaEntity,
|
||||
@Relation(
|
||||
parentColumn = "manga_id",
|
||||
@@ -12,21 +12,4 @@ class MangaWithTags(
|
||||
associateBy = Junction(MangaTagsEntity::class)
|
||||
)
|
||||
val tags: List<TagEntity>,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaWithTags
|
||||
|
||||
if (manga != other.manga) return false
|
||||
return tags == other.tags
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = manga.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.github
|
||||
|
||||
import java.util.*
|
||||
|
||||
class VersionId(
|
||||
data class VersionId(
|
||||
val major: Int,
|
||||
val minor: Int,
|
||||
val build: Int,
|
||||
@@ -30,28 +30,6 @@ class VersionId(
|
||||
return variantNumber.compareTo(other.variantNumber)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as VersionId
|
||||
|
||||
if (major != other.major) return false
|
||||
if (minor != other.minor) return false
|
||||
if (build != other.build) return false
|
||||
if (variantType != other.variantType) return false
|
||||
return variantNumber == other.variantNumber
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = major
|
||||
result = 31 * result + minor
|
||||
result = 31 * result + build
|
||||
result = 31 * result + variantType.hashCode()
|
||||
result = 31 * result + variantNumber
|
||||
return result
|
||||
}
|
||||
|
||||
private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) {
|
||||
"a", "alpha" -> 1
|
||||
"b", "beta" -> 2
|
||||
|
||||
@@ -48,10 +48,10 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
||||
if (groups.size == 1) {
|
||||
return groups.keys.first()
|
||||
}
|
||||
val candidates = HashMap<String?, List<MangaChapter>>(groups.size)
|
||||
for (locale in LocaleListCompat.getAdjustedDefault()) {
|
||||
val displayLanguage = locale.getDisplayLanguage(locale)
|
||||
val displayName = locale.getDisplayName(locale)
|
||||
val candidates = HashMap<String?, List<MangaChapter>>(3)
|
||||
for (branch in groups.keys) {
|
||||
if (branch != null && (
|
||||
branch.contains(displayLanguage, ignoreCase = true) ||
|
||||
@@ -61,8 +61,11 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
|
||||
candidates[branch] = groups[branch] ?: continue
|
||||
}
|
||||
}
|
||||
if (candidates.isNotEmpty()) {
|
||||
return candidates.maxBy { it.value.size }.key
|
||||
}
|
||||
}
|
||||
return candidates.ifEmpty { groups }.maxByOrNull { it.value.size }?.key
|
||||
return groups.maxByOrNull { it.value.size }?.key
|
||||
}
|
||||
|
||||
val Manga.isLocal: Boolean
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.network.cookies
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.util.Predicate
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import org.koitharu.kotatsu.core.util.ext.newBuilder
|
||||
@@ -31,13 +32,16 @@ class AndroidCookieJar : MutableCookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeCookies(url: HttpUrl) {
|
||||
override fun removeCookies(url: HttpUrl, predicate: Predicate<Cookie>?) {
|
||||
val cookies = loadForRequest(url)
|
||||
if (cookies.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val urlString = url.toString()
|
||||
for (c in cookies) {
|
||||
if (predicate != null && !predicate.test(c)) {
|
||||
continue
|
||||
}
|
||||
val nc = c.newBuilder()
|
||||
.expiresAt(System.currentTimeMillis() - 100000)
|
||||
.build()
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
|
||||
|
||||
class CookieWrapper(
|
||||
data class CookieWrapper(
|
||||
val cookie: Cookie,
|
||||
) {
|
||||
|
||||
@@ -66,17 +66,4 @@ class CookieWrapper(
|
||||
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
|
||||
|
||||
return cookie == other.cookie
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return cookie.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.network.cookies
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.util.Predicate
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.HttpUrl
|
||||
@@ -14,7 +15,7 @@ interface MutableCookieJar : CookieJar {
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
|
||||
|
||||
@WorkerThread
|
||||
fun removeCookies(url: HttpUrl)
|
||||
fun removeCookies(url: HttpUrl, predicate: Predicate<Cookie>?)
|
||||
|
||||
suspend fun clear(): Boolean
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.util.Predicate
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Cookie
|
||||
@@ -57,12 +58,14 @@ class PreferencesCookieJar(
|
||||
|
||||
@Synchronized
|
||||
@WorkerThread
|
||||
override fun removeCookies(url: HttpUrl) {
|
||||
override fun removeCookies(url: HttpUrl, predicate: Predicate<Cookie>?) {
|
||||
loadPersistent()
|
||||
val toRemove = HashSet<String>()
|
||||
for ((key, cookie) in cache) {
|
||||
if (cookie.isExpired() || cookie.cookie.matches(url)) {
|
||||
toRemove += key
|
||||
if (predicate == null || predicate.test(cookie.cookie)) {
|
||||
toRemove += key
|
||||
}
|
||||
}
|
||||
}
|
||||
if (toRemove.isNotEmpty()) {
|
||||
|
||||
@@ -5,20 +5,19 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.util.SystemUiController
|
||||
|
||||
abstract class BaseFullscreenActivity<B : ViewBinding> :
|
||||
BaseActivity<B>() {
|
||||
|
||||
private lateinit var insetsControllerCompat: WindowInsetsControllerCompat
|
||||
protected lateinit var systemUiController: SystemUiController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
with(window) {
|
||||
insetsControllerCompat = WindowInsetsControllerCompat(this, decorView)
|
||||
systemUiController = SystemUiController(this)
|
||||
statusBarColor = Color.TRANSPARENT
|
||||
navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim)
|
||||
@@ -30,15 +29,7 @@ abstract class BaseFullscreenActivity<B : ViewBinding> :
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
}
|
||||
insetsControllerCompat.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
showSystemUI()
|
||||
}
|
||||
|
||||
protected fun hideSystemUI() {
|
||||
insetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
|
||||
protected fun showSystemUI() {
|
||||
insetsControllerCompat.show(WindowInsetsCompat.Type.systemBars())
|
||||
// insetsControllerCompat.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
systemUiController.setSystemUiVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +106,7 @@ class TrimTransformation(
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TrimTransformation
|
||||
|
||||
return tolerance == other.tolerance
|
||||
return this === other || (other is TrimTransformation && other.tolerance == tolerance)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
||||
@@ -20,38 +20,19 @@ sealed class DateTimeAgo {
|
||||
override fun equals(other: Any?): Boolean = other === JustNow
|
||||
}
|
||||
|
||||
class MinutesAgo(val minutes: Int) : DateTimeAgo() {
|
||||
|
||||
data class MinutesAgo(val minutes: Int) : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getQuantityString(R.plurals.minutes_ago, minutes, minutes)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as MinutesAgo
|
||||
return minutes == other.minutes
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = minutes
|
||||
|
||||
override fun toString() = "minutes_ago_$minutes"
|
||||
}
|
||||
|
||||
class HoursAgo(val hours: Int) : DateTimeAgo() {
|
||||
data class HoursAgo(val hours: Int) : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getQuantityString(R.plurals.hours_ago, hours, hours)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as HoursAgo
|
||||
return hours == other.hours
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = hours
|
||||
|
||||
override fun toString() = "hours_ago_$hours"
|
||||
}
|
||||
|
||||
@@ -75,26 +56,15 @@ sealed class DateTimeAgo {
|
||||
override fun equals(other: Any?): Boolean = other === Yesterday
|
||||
}
|
||||
|
||||
class DaysAgo(val days: Int) : DateTimeAgo() {
|
||||
|
||||
data class DaysAgo(val days: Int) : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return resources.getQuantityString(R.plurals.days_ago, days, days)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as DaysAgo
|
||||
return days == other.days
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = days
|
||||
|
||||
override fun toString() = "days_ago_$days"
|
||||
}
|
||||
|
||||
class MonthsAgo(val months: Int) : DateTimeAgo() {
|
||||
|
||||
data class MonthsAgo(val months: Int) : DateTimeAgo() {
|
||||
override fun format(resources: Resources): String {
|
||||
return if (months == 0) {
|
||||
resources.getString(R.string.this_month)
|
||||
@@ -102,19 +72,6 @@ sealed class DateTimeAgo {
|
||||
resources.getQuantityString(R.plurals.months_ago, months, months)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MonthsAgo
|
||||
|
||||
return months == other.months
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return months
|
||||
}
|
||||
}
|
||||
|
||||
class Absolute(private val date: Date) : DateTimeAgo() {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.koitharu.kotatsu.core.ui.util
|
||||
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
sealed class SystemUiController(
|
||||
protected val window: Window,
|
||||
) {
|
||||
|
||||
abstract fun setSystemUiVisible(value: Boolean)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
private class Api30Impl(window: Window) : SystemUiController(window) {
|
||||
|
||||
private val insetsController = checkNotNull(window.decorView.windowInsetsController)
|
||||
|
||||
override fun setSystemUiVisible(value: Boolean) {
|
||||
if (value) {
|
||||
insetsController.show(WindowInsets.Type.systemBars())
|
||||
insetsController.systemBarsBehavior = WindowInsetsController.BEHAVIOR_DEFAULT
|
||||
} else {
|
||||
insetsController.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
insetsController.hide(WindowInsets.Type.systemBars())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private class LegacyImpl(window: Window) : SystemUiController(window) {
|
||||
|
||||
override fun setSystemUiVisible(value: Boolean) {
|
||||
window.decorView.systemUiVisibility = if (value) {
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
} else {
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
operator fun invoke(window: Window): SystemUiController =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
Api30Impl(window)
|
||||
} else {
|
||||
LegacyImpl(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,39 +139,14 @@ class ChipsView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
class ChipModel(
|
||||
data class ChipModel(
|
||||
@ColorRes val tint: Int,
|
||||
val title: CharSequence,
|
||||
@DrawableRes val icon: Int,
|
||||
val isCheckable: Boolean,
|
||||
val isChecked: Boolean,
|
||||
val data: Any? = null,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ChipModel
|
||||
|
||||
if (tint != other.tint) return false
|
||||
if (title != other.title) return false
|
||||
if (icon != other.icon) return false
|
||||
if (isCheckable != other.isCheckable) return false
|
||||
if (isChecked != other.isChecked) return false
|
||||
return data == other.data
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = tint.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + icon.hashCode()
|
||||
result = 31 * result + isCheckable.hashCode()
|
||||
result = 31 * result + isChecked.hashCode()
|
||||
result = 31 * result + (data?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fun interface OnChipClickListener {
|
||||
|
||||
|
||||
@@ -118,27 +118,10 @@ class SegmentedBarView @JvmOverloads constructor(
|
||||
segmentsSizes.add(w)
|
||||
}
|
||||
|
||||
class Segment(
|
||||
data class Segment(
|
||||
@FloatRange(from = 0.0, to = 1.0) val percent: Float,
|
||||
@ColorInt val color: Int,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Segment
|
||||
|
||||
if (percent != other.percent) return false
|
||||
return color == other.color
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = percent.hashCode()
|
||||
result = 31 * result + color
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private class OutlineProvider : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
|
||||
@@ -16,8 +16,10 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.fs.FileSequence
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.io.path.readAttributes
|
||||
|
||||
fun File.subdir(name: String) = File(this, name).also {
|
||||
if (!it.exists()) it.mkdirs()
|
||||
@@ -99,3 +101,10 @@ private suspend fun SequenceScope<File>.listFilesRecursiveImpl(root: File, filte
|
||||
fun File.children() = FileSequence(this)
|
||||
|
||||
fun Sequence<File>.filterWith(filter: FileFilter): Sequence<File> = filter { f -> filter.accept(f) }
|
||||
|
||||
val File.creationTime
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
toPath().readAttributes<BasicFileAttributes>().creationTime().toMillis()
|
||||
} else {
|
||||
lastModified()
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
private const val MSG_NO_SPACE_LEFT = "No space left on device"
|
||||
private const val IMAGE_FORMAT_NO_SUPPORTED = "Image format not supported"
|
||||
|
||||
fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
|
||||
is AuthRequiredException -> resources.getString(R.string.auth_required)
|
||||
@@ -81,6 +82,7 @@ private fun getHttpDisplayMessage(statusCode: Int, resources: Resources): String
|
||||
private fun getDisplayMessage(msg: String?, resources: Resources): String? = when {
|
||||
msg.isNullOrEmpty() -> null
|
||||
msg.contains(MSG_NO_SPACE_LEFT) -> resources.getString(R.string.error_no_space_left)
|
||||
msg.contains(IMAGE_FORMAT_NO_SUPPORTED) -> resources.getString(R.string.error_corrupted_file)
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.text.format.DateUtils
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||
|
||||
class ChapterListItem(
|
||||
data class ChapterListItem(
|
||||
val chapter: MangaChapter,
|
||||
val flags: Int,
|
||||
private val uploadDateMs: Long,
|
||||
@@ -66,24 +66,6 @@ class ChapterListItem(
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ChapterListItem
|
||||
|
||||
if (chapter != other.chapter) return false
|
||||
if (flags != other.flags) return false
|
||||
return uploadDateMs == other.uploadDateMs
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = chapter.hashCode()
|
||||
result = 31 * result + flags
|
||||
result = 31 * result + uploadDateMs.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val FLAG_UNREAD = 2
|
||||
|
||||
@@ -3,35 +3,14 @@ package org.koitharu.kotatsu.details.ui.model
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
class HistoryInfo(
|
||||
data 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
|
||||
|
||||
other as HistoryInfo
|
||||
|
||||
if (totalChapters != other.totalChapters) return false
|
||||
if (currentChapter != other.currentChapter) return false
|
||||
if (history != other.history) return false
|
||||
return isIncognitoMode == other.isIncognitoMode
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = totalChapters
|
||||
result = 31 * result + currentChapter
|
||||
result = 31 * result + (history?.hashCode() ?: 0)
|
||||
result = 31 * result + isIncognitoMode.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun HistoryInfo(
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.details.ui.model
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class MangaBranch(
|
||||
data class MangaBranch(
|
||||
val name: String?,
|
||||
val count: Int,
|
||||
val isSelected: Boolean,
|
||||
@@ -21,24 +21,6 @@ class MangaBranch(
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaBranch
|
||||
|
||||
if (name != other.name) return false
|
||||
if (count != other.count) return false
|
||||
return isSelected == other.isSelected
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + count
|
||||
result = 31 * result + isSelected.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "$name: $count"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
class DownloadItemModel(
|
||||
data class DownloadItemModel(
|
||||
val id: UUID,
|
||||
val workState: WorkInfo.State,
|
||||
val isIndeterminate: Boolean,
|
||||
@@ -64,38 +64,4 @@ class DownloadItemModel(
|
||||
else -> super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as DownloadItemModel
|
||||
|
||||
if (id != other.id) return false
|
||||
if (workState != other.workState) return false
|
||||
if (isIndeterminate != other.isIndeterminate) return false
|
||||
if (isPaused != other.isPaused) return false
|
||||
if (manga != other.manga) return false
|
||||
if (error != other.error) return false
|
||||
if (max != other.max) return false
|
||||
if (totalChapters != other.totalChapters) return false
|
||||
if (progress != other.progress) return false
|
||||
if (eta != other.eta) return false
|
||||
return timestamp == other.timestamp
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + workState.hashCode()
|
||||
result = 31 * result + isIndeterminate.hashCode()
|
||||
result = 31 * result + isPaused.hashCode()
|
||||
result = 31 * result + manga.hashCode()
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
result = 31 * result + max
|
||||
result = 31 * result + totalChapters
|
||||
result = 31 * result + progress
|
||||
result = 31 * result + eta.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,11 @@ package org.koitharu.kotatsu.explore.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ExploreButtons(
|
||||
data class ExploreButtons(
|
||||
val isRandomLoading: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is ExploreButtons
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ExploreButtons
|
||||
|
||||
return isRandomLoading == other.isRandomLoading
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return isRandomLoading.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.explore.ui.model
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class MangaSourceItem(
|
||||
data class MangaSourceItem(
|
||||
val source: MangaSource,
|
||||
val isGrid: Boolean,
|
||||
) : ListModel {
|
||||
@@ -11,20 +11,4 @@ class MangaSourceItem(
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is MangaSourceItem && other.source == source
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaSourceItem
|
||||
|
||||
if (source != other.source) return false
|
||||
return isGrid == other.isGrid
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = source.hashCode()
|
||||
result = 31 * result + isGrid.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
data class RecommendationsItem(
|
||||
val manga: Manga
|
||||
) : ListModel {
|
||||
|
||||
val summary: String = manga.tags.joinToString { it.title }
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
|
||||
@@ -3,31 +3,10 @@ package org.koitharu.kotatsu.favourites.domain.model
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.find
|
||||
|
||||
class Cover(
|
||||
data class Cover(
|
||||
val url: String,
|
||||
val source: String,
|
||||
) {
|
||||
|
||||
val mangaSource: MangaSource?
|
||||
get() = if (source.isEmpty()) null else MangaSource.entries.find(source)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Cover
|
||||
|
||||
if (url != other.url) return false
|
||||
return source == other.source
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = url.hashCode()
|
||||
result = 31 * result + source.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Cover(url='$url', source=$source)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoriesAdapter
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
@@ -66,7 +67,7 @@ class FavouriteCategoriesActivity :
|
||||
attachToRecyclerView(viewBinding.recyclerView)
|
||||
}
|
||||
|
||||
viewModel.categories.observe(this, ::onCategoriesChanged)
|
||||
viewModel.content.observe(this, ::onCategoriesChanged)
|
||||
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
|
||||
}
|
||||
|
||||
@@ -77,6 +78,14 @@ class FavouriteCategoriesActivity :
|
||||
}
|
||||
|
||||
override fun onItemClick(item: FavouriteCategory, view: View) {
|
||||
if (selectionController.onItemClick(item.id)) {
|
||||
return
|
||||
}
|
||||
val intent = FavouritesActivity.newIntent(view.context, item)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onEditClick(item: FavouriteCategory, view: View) {
|
||||
if (selectionController.onItemClick(item.id)) {
|
||||
return
|
||||
}
|
||||
@@ -112,8 +121,8 @@ class FavouriteCategoriesActivity :
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCategoriesChanged(categories: List<ListModel>) {
|
||||
adapter.items = categories
|
||||
private suspend fun onCategoriesChanged(categories: List<ListModel>) {
|
||||
adapter.emit(categories)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
@@ -128,7 +137,14 @@ class FavouriteCategoriesActivity :
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder,
|
||||
): Boolean = viewHolder.itemViewType == target.itemViewType
|
||||
): Boolean {
|
||||
if (viewHolder.itemViewType != target.itemViewType) {
|
||||
return false
|
||||
}
|
||||
val fromPos = viewHolder.bindingAdapterPosition
|
||||
val toPos = target.bindingAdapterPosition
|
||||
return fromPos != toPos && fromPos != RecyclerView.NO_POSITION && toPos != RecyclerView.NO_POSITION
|
||||
}
|
||||
|
||||
override fun canDropOver(
|
||||
recyclerView: RecyclerView,
|
||||
@@ -153,7 +169,8 @@ class FavouriteCategoriesActivity :
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
viewBinding.recyclerView.isNestedScrollingEnabled = actionState == ItemTouchHelper.ACTION_STATE_IDLE
|
||||
viewBinding.recyclerView.isNestedScrollingEnabled =
|
||||
actionState == ItemTouchHelper.ACTION_STATE_IDLE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
@@ -7,4 +8,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
interface FavouriteCategoriesListListener : OnListItemClickListener<FavouriteCategory> {
|
||||
|
||||
fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean
|
||||
|
||||
fun onEditClick(item: FavouriteCategory, view: View)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.requireValue
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
|
||||
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 java.util.Collections
|
||||
import org.koitharu.kotatsu.parsers.util.move
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -28,26 +27,17 @@ class FavouritesCategoriesViewModel @Inject constructor(
|
||||
|
||||
private var reorderJob: Job? = null
|
||||
|
||||
val categories = repository.observeCategoriesWithCovers()
|
||||
.map { list ->
|
||||
list.map { (category, covers) ->
|
||||
CategoryListModel(
|
||||
mangaCount = covers.size,
|
||||
covers = covers.take(3),
|
||||
category = category,
|
||||
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources,
|
||||
)
|
||||
}.ifEmpty {
|
||||
listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_favourites,
|
||||
textPrimary = R.string.text_empty_holder_primary,
|
||||
textSecondary = R.string.empty_favourite_categories,
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||
val content = MutableStateFlow<List<ListModel>>(listOf(LoadingState))
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
repository.observeCategoriesWithCovers()
|
||||
.collectLatest {
|
||||
reorderJob?.join()
|
||||
updateContent(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCategories(ids: Set<Long>) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
@@ -59,18 +49,18 @@ class FavouritesCategoriesViewModel @Inject constructor(
|
||||
settings.isAllFavouritesVisible = isVisible
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean = categories.value.none { it is CategoryListModel }
|
||||
fun isEmpty(): Boolean = content.value.none { it is CategoryListModel }
|
||||
|
||||
fun reorderCategories(oldPos: Int, newPos: Int) {
|
||||
val prevJob = reorderJob
|
||||
reorderJob = launchJob(Dispatchers.Default) {
|
||||
prevJob?.join()
|
||||
val items = categories.requireValue()
|
||||
val ids = items.mapNotNullTo(ArrayList(items.size)) {
|
||||
val snapshot = content.requireValue().toMutableList()
|
||||
snapshot.move(oldPos, newPos)
|
||||
content.value = snapshot
|
||||
val ids = snapshot.mapNotNullTo(ArrayList(snapshot.size)) {
|
||||
(it as? CategoryListModel)?.category?.id
|
||||
}
|
||||
Collections.swap(ids, oldPos, newPos)
|
||||
ids.remove(0L)
|
||||
repository.reorderCategories(ids)
|
||||
}
|
||||
}
|
||||
@@ -84,9 +74,29 @@ class FavouritesCategoriesViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun getCategories(ids: Set<Long>): ArrayList<FavouriteCategory> {
|
||||
val items = categories.requireValue()
|
||||
val items = content.requireValue()
|
||||
return items.mapNotNullTo(ArrayList(ids.size)) { item ->
|
||||
(item as? CategoryListModel)?.category?.takeIf { it.id in ids }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContent(categories: Map<FavouriteCategory, List<Cover>>) {
|
||||
content.value = categories.map { (category, covers) ->
|
||||
CategoryListModel(
|
||||
mangaCount = covers.size,
|
||||
covers = covers.take(3),
|
||||
category = category,
|
||||
isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources,
|
||||
)
|
||||
}.ifEmpty {
|
||||
listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_favourites,
|
||||
textPrimary = R.string.text_empty_holder_primary,
|
||||
textSecondary = R.string.empty_favourite_categories,
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ class CategoriesAdapter(
|
||||
) : BaseListAdapter<ListModel>() {
|
||||
|
||||
init {
|
||||
addDelegate(ListItemType.CATEGORY_LARGE ,categoryAD(coil, lifecycleOwner, onItemClickListener))
|
||||
addDelegate(ListItemType.STATE_EMPTY ,emptyStateListAD(coil, lifecycleOwner, listListener))
|
||||
addDelegate(ListItemType.STATE_LOADING ,loadingStateAD())
|
||||
addDelegate(ListItemType.CATEGORY_LARGE, categoryAD(coil, lifecycleOwner, onItemClickListener))
|
||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listListener))
|
||||
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,13 @@ fun categoryAD(
|
||||
{ inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
val eventListener = object : OnClickListener, OnLongClickListener, OnTouchListener {
|
||||
override fun onClick(v: View) = clickListener.onItemClick(item.category, itemView)
|
||||
override fun onLongClick(v: View) = clickListener.onItemLongClick(item.category, itemView)
|
||||
override fun onClick(v: View) = if (v.id == R.id.imageView_edit) {
|
||||
clickListener.onEditClick(item.category, v)
|
||||
} else {
|
||||
clickListener.onItemClick(item.category, v)
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View) = clickListener.onItemLongClick(item.category, v)
|
||||
override fun onTouch(v: View?, event: MotionEvent): Boolean = event.actionMasked == MotionEvent.ACTION_DOWN &&
|
||||
clickListener.onDragHandleTouch(this@adapterDelegateViewBinding)
|
||||
}
|
||||
@@ -57,6 +62,7 @@ fun categoryAD(
|
||||
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
|
||||
itemView.setOnClickListener(eventListener)
|
||||
itemView.setOnLongClickListener(eventListener)
|
||||
binding.imageViewEdit.setOnClickListener(eventListener)
|
||||
binding.imageViewHandle.setOnTouchListener(eventListener)
|
||||
|
||||
bind { payloads ->
|
||||
|
||||
@@ -46,4 +46,8 @@ class CategoryListModel(
|
||||
result = 31 * result + category.isVisibleInLibrary.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "CategoryListModel(categoryId=${category.id})"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.favourites.ui.container
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class FavouriteTabModel(
|
||||
data class FavouriteTabModel(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
) : ListModel {
|
||||
@@ -10,20 +10,4 @@ class FavouriteTabModel(
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is FavouriteTabModel && other.id == id
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FavouriteTabModel
|
||||
|
||||
if (id != other.id) return false
|
||||
return title == other.title
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,30 +263,11 @@ class FilterCoordinator @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
private class TagsWrapper(
|
||||
private data class TagsWrapper(
|
||||
val tags: Set<MangaTag>,
|
||||
val isLoading: Boolean,
|
||||
val isError: Boolean,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TagsWrapper
|
||||
|
||||
if (tags != other.tags) return false
|
||||
if (isLoading != other.isLoading) return false
|
||||
return isError == other.isError
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = tags.hashCode()
|
||||
result = 31 * result + isLoading.hashCode()
|
||||
result = 31 * result + isError.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private class TagTitleComparator(lc: String?) : Comparator<MangaTag> {
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
sealed interface FilterItem : ListModel {
|
||||
|
||||
class Sort(
|
||||
data class Sort(
|
||||
val order: SortOrder,
|
||||
val isSelected: Boolean,
|
||||
) : FilterItem {
|
||||
@@ -24,25 +24,9 @@ sealed interface FilterItem : ListModel {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Sort
|
||||
|
||||
if (order != other.order) return false
|
||||
return isSelected == other.isSelected
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = order.hashCode()
|
||||
result = 31 * result + isSelected.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class Tag(
|
||||
data class Tag(
|
||||
val tag: MangaTag,
|
||||
val isChecked: Boolean,
|
||||
) : FilterItem {
|
||||
@@ -58,43 +42,14 @@ sealed interface FilterItem : ListModel {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Tag
|
||||
|
||||
if (tag != other.tag) return false
|
||||
return isChecked == other.isChecked
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = tag.hashCode()
|
||||
result = 31 * result + isChecked.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class Error(
|
||||
data class Error(
|
||||
@StringRes val textResId: Int,
|
||||
) : FilterItem {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is Error && textResId == other.textResId
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Error
|
||||
|
||||
return textResId == other.textResId
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return textResId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,7 @@ package org.koitharu.kotatsu.filter.ui.model
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
class FilterState(
|
||||
data class FilterState(
|
||||
val sortOrder: SortOrder?,
|
||||
val tags: Set<MangaTag>,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FilterState
|
||||
|
||||
if (sortOrder != other.sortOrder) return false
|
||||
return tags == other.tags
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = sortOrder?.hashCode() ?: 0
|
||||
result = 31 * result + tags.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.creationTime
|
||||
import org.koitharu.kotatsu.core.util.ext.listFilesRecursive
|
||||
import org.koitharu.kotatsu.core.util.ext.longHashCode
|
||||
import org.koitharu.kotatsu.core.util.ext.toListSorted
|
||||
@@ -62,7 +63,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
name = f.nameWithoutExtension.toHumanReadable(),
|
||||
number = i + 1,
|
||||
source = MangaSource.LOCAL,
|
||||
uploadDate = f.lastModified(),
|
||||
uploadDate = f.creationTime,
|
||||
url = f.toUri().toString(),
|
||||
scanlator = null,
|
||||
branch = null,
|
||||
@@ -77,7 +78,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
largeCoverUrl = null,
|
||||
description = null,
|
||||
)
|
||||
LocalManga(root, manga)
|
||||
LocalManga(manga, root)
|
||||
}
|
||||
|
||||
override suspend fun getMangaInfo(): Manga? = runInterruptible(Dispatchers.IO) {
|
||||
|
||||
@@ -94,7 +94,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
|
||||
)
|
||||
}
|
||||
}
|
||||
return LocalManga(root, manga)
|
||||
return LocalManga(manga, root)
|
||||
}
|
||||
|
||||
override suspend fun getMangaInfo(): Manga? = runInterruptible(Dispatchers.IO) {
|
||||
|
||||
@@ -2,22 +2,21 @@ package org.koitharu.kotatsu.local.domain.model
|
||||
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import org.koitharu.kotatsu.core.util.ext.creationTime
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import java.io.File
|
||||
|
||||
class LocalManga(
|
||||
val file: File,
|
||||
data class LocalManga(
|
||||
val manga: Manga,
|
||||
val file: File = manga.url.toUri().toFile(),
|
||||
) {
|
||||
|
||||
constructor(manga: Manga) : this(manga.url.toUri().toFile(), manga)
|
||||
|
||||
var createdAt: Long = -1L
|
||||
private set
|
||||
get() {
|
||||
if (field == -1L) {
|
||||
field = file.lastModified()
|
||||
field = file.creationTime
|
||||
}
|
||||
return field
|
||||
}
|
||||
@@ -31,22 +30,6 @@ class LocalManga(
|
||||
return manga.tags.containsAll(tags)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as LocalManga
|
||||
|
||||
if (manga != other.manga) return false
|
||||
return file == other.file
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = manga.hashCode()
|
||||
result = 31 * result + file.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "LocalManga(${file.path}: ${manga.title})"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.domain
|
||||
import android.graphics.ColorMatrix
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
|
||||
class ReaderColorFilter(
|
||||
data class ReaderColorFilter(
|
||||
val brightness: Float,
|
||||
val contrast: Float,
|
||||
val isInverted: Boolean,
|
||||
@@ -51,22 +51,4 @@ class ReaderColorFilter(
|
||||
)
|
||||
set(matrix)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ReaderColorFilter
|
||||
|
||||
if (brightness != other.brightness) return false
|
||||
if (contrast != other.contrast) return false
|
||||
return isInverted == other.isInverted
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = brightness.hashCode()
|
||||
result = 31 * result + contrast.hashCode()
|
||||
result = 31 * result + isInverted.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.koitharu.kotatsu.core.util.GridTouchHelper
|
||||
import org.koitharu.kotatsu.core.util.IdlingDetector
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.isRtl
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
@@ -309,22 +310,20 @@ class ReaderActivity :
|
||||
|
||||
private fun setUiIsVisible(isUiVisible: Boolean) {
|
||||
if (viewBinding.appbarTop.isVisible != isUiVisible) {
|
||||
val transition = TransitionSet()
|
||||
.setOrdering(TransitionSet.ORDERING_TOGETHER)
|
||||
.addTransition(Slide(Gravity.TOP).addTarget(viewBinding.appbarTop))
|
||||
.addTransition(Fade().addTarget(viewBinding.infoBar))
|
||||
viewBinding.appbarBottom?.let { bottomBar ->
|
||||
transition.addTransition(Slide(Gravity.BOTTOM).addTarget(bottomBar))
|
||||
if (isAnimationsEnabled) {
|
||||
val transition = TransitionSet()
|
||||
.setOrdering(TransitionSet.ORDERING_TOGETHER)
|
||||
.addTransition(Slide(Gravity.TOP).addTarget(viewBinding.appbarTop))
|
||||
.addTransition(Fade().addTarget(viewBinding.infoBar))
|
||||
viewBinding.appbarBottom?.let { bottomBar ->
|
||||
transition.addTransition(Slide(Gravity.BOTTOM).addTarget(bottomBar))
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
|
||||
viewBinding.appbarTop.isVisible = isUiVisible
|
||||
viewBinding.appbarBottom?.isVisible = isUiVisible
|
||||
viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value)
|
||||
if (isUiVisible) {
|
||||
showSystemUI()
|
||||
} else {
|
||||
hideSystemUI()
|
||||
}
|
||||
systemUiController.setSystemUiVisible(isUiVisible)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class WebtoonFrameLayout @JvmOverloads constructor(
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val target by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val target by lazy(LazyThreadSafetyMode.NONE) {
|
||||
findViewById<WebtoonImageView>(R.id.ssiv)
|
||||
}
|
||||
|
||||
@@ -24,4 +24,4 @@ class WebtoonFrameLayout @JvmOverloads constructor(
|
||||
target.scrollBy(dy)
|
||||
return target.getScroll() - oldScroll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,10 +84,22 @@ class WebtoonImageView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
width = width.coerceAtLeast(suggestedMinimumWidth)
|
||||
height = height.coerceIn(suggestedMinimumHeight, parentHeight())
|
||||
height = height.coerceAtLeast(suggestedMinimumHeight).coerceAtMost(parentHeight())
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
if (oldh == h || oldw == 0 || oldh == 0 || scrollRange == SCROLL_UNKNOWN) return
|
||||
|
||||
computeScrollRange()
|
||||
val container = parents.firstNotNullOfOrNull { it as? WebtoonFrameLayout } ?: return
|
||||
val parentHeight = parentHeight()
|
||||
if (scrollPos != 0 && container.bottom < parentHeight) {
|
||||
scrollTo(scrollRange)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToInternal(pos: Int) {
|
||||
scrollPos = pos
|
||||
ct.set(sWidth / 2f, (height / 2f + pos.toFloat()) / minScale)
|
||||
|
||||
@@ -2,16 +2,30 @@ package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat.TYPE_TOUCH
|
||||
import androidx.core.view.forEach
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
|
||||
import java.util.LinkedList
|
||||
import java.util.WeakHashMap
|
||||
|
||||
class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : RecyclerView(context, attrs, defStyleAttr) {
|
||||
|
||||
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null
|
||||
private val detachedViews = WeakHashMap<View, Unit>()
|
||||
|
||||
override fun onChildDetachedFromWindow(child: View) {
|
||||
super.onChildDetachedFromWindow(child)
|
||||
detachedViews[child] = Unit
|
||||
}
|
||||
|
||||
override fun onChildAttachedToWindow(child: View) {
|
||||
super.onChildAttachedToWindow(child)
|
||||
detachedViews.remove(child)
|
||||
}
|
||||
|
||||
override fun startNestedScroll(axes: Int) = startNestedScroll(axes, TYPE_TOUCH)
|
||||
|
||||
@@ -98,6 +112,15 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) }
|
||||
}
|
||||
|
||||
fun relayoutChildren() {
|
||||
forEach { child ->
|
||||
(child as WebtoonFrameLayout).target.requestLayout()
|
||||
}
|
||||
detachedViews.keys.forEach { child ->
|
||||
(child as WebtoonFrameLayout).target.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OnPageScrollListener {
|
||||
|
||||
private var lastPosition = NO_POSITION
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.koitharu.kotatsu.reader.ui.pager.webtoon
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
@@ -15,7 +15,7 @@ import android.widget.OverScroller
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
|
||||
private const val MAX_SCALE = 2.5f
|
||||
private const val MIN_SCALE = 1f // under-scaling disabled due to buggy nested scroll
|
||||
private const val MIN_SCALE = 0.5f
|
||||
|
||||
class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -23,7 +23,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
defStyles: Int = 0,
|
||||
) : FrameLayout(context, attrs, defStyles), ScaleGestureDetector.OnScaleGestureListener {
|
||||
|
||||
private val targetChild by lazy(LazyThreadSafetyMode.NONE) { getChildAt(0) }
|
||||
private val targetChild by lazy(LazyThreadSafetyMode.NONE) { getChildAt(0) as WebtoonRecyclerView }
|
||||
|
||||
private val scaleDetector = ScaleGestureDetector(context, this)
|
||||
private val gestureDetector = GestureDetectorCompat(context, GestureListener())
|
||||
@@ -40,7 +40,6 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
private var halfHeight = 0f
|
||||
private val translateBounds = RectF()
|
||||
private val targetHitRect = Rect()
|
||||
private var pendingScroll = 0
|
||||
|
||||
var isZoomEnable = true
|
||||
set(value) {
|
||||
@@ -97,12 +96,11 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
if (newHeight != targetChild.height) {
|
||||
targetChild.layoutParams.height = newHeight
|
||||
targetChild.requestLayout()
|
||||
targetChild.relayoutChildren()
|
||||
}
|
||||
|
||||
if (scale < 1) {
|
||||
targetChild.getHitRect(targetHitRect)
|
||||
targetChild.scrollBy(0, pendingScroll)
|
||||
pendingScroll = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +122,6 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
pendingScroll = dy.toInt()
|
||||
transformMatrix.postTranslate(dx, dy)
|
||||
syncMatrixValues()
|
||||
}
|
||||
@@ -159,9 +156,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
|
||||
|
||||
override fun onScaleEnd(p0: ScaleGestureDetector) {
|
||||
pendingScroll = 0
|
||||
}
|
||||
override fun onScaleEnd(p0: ScaleGestureDetector) = Unit
|
||||
|
||||
|
||||
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable {
|
||||
@@ -175,7 +170,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
val newScale = if (scale != 1f) 1f else MAX_SCALE * 0.8f
|
||||
ObjectAnimator.ofFloat(scale, newScale).run {
|
||||
ValueAnimator.ofFloat(scale, newScale).run {
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
duration = 300
|
||||
addUpdateListener {
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||
|
||||
class PageThumbnail(
|
||||
data class PageThumbnail(
|
||||
val isCurrent: Boolean,
|
||||
val repository: MangaRepository,
|
||||
val page: ReaderPage,
|
||||
@@ -16,23 +16,4 @@ class PageThumbnail(
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is PageThumbnail && page == other.page
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as PageThumbnail
|
||||
|
||||
if (isCurrent != other.isCurrent) return false
|
||||
if (repository != other.repository) return false
|
||||
return page == other.page
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = isCurrent.hashCode()
|
||||
result = 31 * result + repository.hashCode()
|
||||
result = 31 * result + page.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,40 +2,17 @@ package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ScrobblerManga(
|
||||
data class ScrobblerManga(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val altName: String?,
|
||||
val cover: String,
|
||||
val url: String,
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is ScrobblerManga && other.id == id
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ScrobblerManga
|
||||
|
||||
if (id != other.id) return false
|
||||
if (name != other.name) return false
|
||||
if (altName != other.altName) return false
|
||||
if (cover != other.cover) return false
|
||||
return url == other.url
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + altName.hashCode()
|
||||
result = 31 * result + cover.hashCode()
|
||||
result = 31 * result + url.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ScrobblerManga #$id \"$name\" $url"
|
||||
}
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
class ScrobblerUser(
|
||||
data class ScrobblerUser(
|
||||
val id: Long,
|
||||
val nickname: String,
|
||||
val avatar: String,
|
||||
val service: ScrobblerService,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ScrobblerUser
|
||||
|
||||
if (id != other.id) return false
|
||||
if (nickname != other.nickname) return false
|
||||
if (avatar != other.avatar) return false
|
||||
return service == other.service
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + nickname.hashCode()
|
||||
result = 31 * result + avatar.hashCode()
|
||||
result = 31 * result + service.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.scrobbling.common.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ScrobblingInfo(
|
||||
data class ScrobblingInfo(
|
||||
val scrobbler: ScrobblerService,
|
||||
val mangaId: Long,
|
||||
val targetId: Long,
|
||||
@@ -19,38 +19,4 @@ class ScrobblingInfo(
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is ScrobblingInfo && other.scrobbler == scrobbler
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ScrobblingInfo
|
||||
|
||||
if (scrobbler != other.scrobbler) return false
|
||||
if (mangaId != other.mangaId) return false
|
||||
if (targetId != other.targetId) return false
|
||||
if (status != other.status) return false
|
||||
if (chapter != other.chapter) return false
|
||||
if (comment != other.comment) return false
|
||||
if (rating != other.rating) return false
|
||||
if (title != other.title) return false
|
||||
if (coverUrl != other.coverUrl) return false
|
||||
if (description != other.description) return false
|
||||
return externalUrl == other.externalUrl
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = scrobbler.hashCode()
|
||||
result = 31 * result + mangaId.hashCode()
|
||||
result = 31 * result + targetId.hashCode()
|
||||
result = 31 * result + (status?.hashCode() ?: 0)
|
||||
result = 31 * result + chapter
|
||||
result = 31 * result + (comment?.hashCode() ?: 0)
|
||||
result = 31 * result + rating.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + coverUrl.hashCode()
|
||||
result = 31 * result + (description?.hashCode() ?: 0)
|
||||
result = 31 * result + externalUrl.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ScrobblerHint(
|
||||
data class ScrobblerHint(
|
||||
@DrawableRes val icon: Int,
|
||||
@StringRes val textPrimary: Int,
|
||||
@StringRes val textSecondary: Int,
|
||||
@@ -15,26 +15,4 @@ class ScrobblerHint(
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is ScrobblerHint && other.textPrimary == textPrimary
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ScrobblerHint
|
||||
|
||||
if (icon != other.icon) return false
|
||||
if (textPrimary != other.textPrimary) return false
|
||||
if (textSecondary != other.textSecondary) return false
|
||||
if (error != other.error) return false
|
||||
return actionStringRes == other.actionStringRes
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = icon
|
||||
result = 31 * result + textPrimary
|
||||
result = 31 * result + textSecondary
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
result = 31 * result + actionStringRes
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class MultiSearchListModel(
|
||||
data class MultiSearchListModel(
|
||||
val source: MangaSource,
|
||||
val hasMore: Boolean,
|
||||
val list: List<MangaItemModel>,
|
||||
@@ -23,24 +23,4 @@ class MultiSearchListModel(
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MultiSearchListModel
|
||||
|
||||
if (source != other.source) return false
|
||||
if (hasMore != other.hasMore) return false
|
||||
if (list != other.list) return false
|
||||
return error == other.error
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = source.hashCode()
|
||||
result = 31 * result + hasMore.hashCode()
|
||||
result = 31 * result + list.hashCode()
|
||||
result = 31 * result + (error?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class SourceSettingsViewModel @Inject constructor(
|
||||
.scheme("https")
|
||||
.host(repository.domain)
|
||||
.build()
|
||||
cookieJar.removeCookies(url)
|
||||
cookieJar.removeCookies(url, null)
|
||||
onActionDone.call(ReversibleAction(R.string.cookies_cleared, null))
|
||||
loadUsername()
|
||||
}
|
||||
|
||||
@@ -2,17 +2,12 @@ package org.koitharu.kotatsu.settings.sources
|
||||
|
||||
import androidx.annotation.CheckResult
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.getLocaleTitle
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -46,32 +41,39 @@ class SourcesManageViewModel @Inject constructor(
|
||||
private val expandedGroups = MutableStateFlow(emptySet<String?>())
|
||||
private var searchQuery = MutableStateFlow<String?>(null)
|
||||
private var reorderJob: Job? = null
|
||||
|
||||
val content = combine(
|
||||
repository.observeEnabledSources(),
|
||||
expandedGroups,
|
||||
searchQuery,
|
||||
observeTip(),
|
||||
settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) { isNsfwContentDisabled },
|
||||
) { sources, groups, query, tip, noNsfw ->
|
||||
buildList(sources, groups, query, tip, noNsfw)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val content = MutableStateFlow<List<SourceConfigItem>>(emptyList())
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
combine(
|
||||
repository.observeEnabledSources(),
|
||||
expandedGroups,
|
||||
searchQuery,
|
||||
observeTip(),
|
||||
settings.observeAsFlow(AppSettings.KEY_DISABLE_NSFW) { isNsfwContentDisabled },
|
||||
) { sources, groups, query, tip, noNsfw ->
|
||||
buildList(sources, groups, query, tip, noNsfw)
|
||||
}.collectLatest {
|
||||
reorderJob?.join()
|
||||
content.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reorderSources(oldPos: Int, newPos: Int) {
|
||||
val snapshot = content.value.toMutableList()
|
||||
val prevJob = reorderJob
|
||||
reorderJob = launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
if ((snapshot[oldPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
|
||||
return@launchJob
|
||||
}
|
||||
if ((snapshot[newPos] as? SourceConfigItem.SourceItem)?.isDraggable != true) {
|
||||
return@launchJob
|
||||
}
|
||||
delay(100)
|
||||
snapshot.move(oldPos, newPos)
|
||||
content.value = snapshot
|
||||
prevJob?.join()
|
||||
val newSourcesList = snapshot.mapNotNull { x ->
|
||||
if (x is SourceConfigItem.SourceItem && x.isDraggable) {
|
||||
x.source
|
||||
|
||||
@@ -5,14 +5,13 @@ import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import java.io.File
|
||||
|
||||
class DirectoryModel(
|
||||
data class DirectoryModel(
|
||||
val title: String?,
|
||||
@StringRes val titleRes: Int,
|
||||
val file: File?,
|
||||
val isChecked: Boolean,
|
||||
val isAvailable: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is DirectoryModel && other.file == file && other.title == title && other.titleRes == titleRes
|
||||
}
|
||||
@@ -24,26 +23,4 @@ class DirectoryModel(
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as DirectoryModel
|
||||
|
||||
if (title != other.title) return false
|
||||
if (titleRes != other.titleRes) return false
|
||||
if (file != other.file) return false
|
||||
if (isChecked != other.isChecked) return false
|
||||
return isAvailable == other.isAvailable
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = title?.hashCode() ?: 0
|
||||
result = 31 * result + titleRes
|
||||
result = 31 * result + (file?.hashCode() ?: 0)
|
||||
result = 31 * result + isChecked.hashCode()
|
||||
result = 31 * result + isAvailable.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,13 @@
|
||||
package org.koitharu.kotatsu.settings.userdata
|
||||
|
||||
class StorageUsage(
|
||||
data class StorageUsage(
|
||||
val savedManga: Item,
|
||||
val pagesCache: Item,
|
||||
val otherCache: Item,
|
||||
val available: Item,
|
||||
) {
|
||||
|
||||
class Item(
|
||||
data class Item(
|
||||
val bytes: Long,
|
||||
val percent: Float,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Item
|
||||
|
||||
if (bytes != other.bytes) return false
|
||||
return percent == other.percent
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = bytes.hashCode()
|
||||
result = 31 * result + percent.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as StorageUsage
|
||||
|
||||
if (savedManga != other.savedManga) return false
|
||||
if (pagesCache != other.pagesCache) return false
|
||||
if (otherCache != other.otherCache) return false
|
||||
return available == other.available
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = savedManga.hashCode()
|
||||
result = 31 * result + pagesCache.hashCode()
|
||||
result = 31 * result + otherCache.hashCode()
|
||||
result = 31 * result + available.hashCode()
|
||||
return result
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.view.ViewTreeObserver
|
||||
import android.widget.HorizontalScrollView
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
import androidx.customview.view.AbsSavedState
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
@@ -52,7 +53,11 @@ class ThemeChooserPreference @JvmOverloads constructor(
|
||||
binding.linear.removeAllViews()
|
||||
for (theme in entries) {
|
||||
val context = ContextThemeWrapper(context, theme.styleResId)
|
||||
val item = ItemColorSchemeBinding.inflate(LayoutInflater.from(context), binding.linear, false)
|
||||
val item =
|
||||
ItemColorSchemeBinding.inflate(LayoutInflater.from(context), binding.linear, false)
|
||||
if (binding.linear.childCount == 0) {
|
||||
item.root.updatePaddingRelative(start = 0)
|
||||
}
|
||||
val isSelected = theme == currentValue
|
||||
item.card.isChecked = isSelected
|
||||
item.card.strokeWidth = if (isSelected) context.resources.getDimensionPixelSize(
|
||||
@@ -76,7 +81,8 @@ class ThemeChooserPreference @JvmOverloads constructor(
|
||||
}
|
||||
binding.scrollView.viewTreeObserver.run {
|
||||
scrollPersistListener?.let { removeOnScrollChangedListener(it) }
|
||||
scrollPersistListener = ScrollPersistListener(WeakReference(binding.scrollView), lastScrollPosition)
|
||||
scrollPersistListener =
|
||||
ScrollPersistListener(WeakReference(binding.scrollView), lastScrollPosition)
|
||||
addOnScrollChangedListener(scrollPersistListener)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
@@ -133,7 +139,7 @@ class ThemeChooserPreference @JvmOverloads constructor(
|
||||
|
||||
constructor(
|
||||
superState: Parcelable,
|
||||
scrollPosition: Int
|
||||
scrollPosition: Int,
|
||||
) : super(superState) {
|
||||
this.scrollPosition = scrollPosition
|
||||
}
|
||||
@@ -151,7 +157,8 @@ class ThemeChooserPreference @JvmOverloads constructor(
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> {
|
||||
override fun createFromParcel(`in`: Parcel) = SavedState(`in`, SavedState::class.java.classLoader)
|
||||
override fun createFromParcel(`in`: Parcel) =
|
||||
SavedState(`in`, SavedState::class.java.classLoader)
|
||||
|
||||
override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
package org.koitharu.kotatsu.sync.domain
|
||||
|
||||
class SyncAuthResult(
|
||||
data class SyncAuthResult(
|
||||
val host: String,
|
||||
val email: String,
|
||||
val password: String,
|
||||
val token: String,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SyncAuthResult
|
||||
|
||||
if (host != other.host) return false
|
||||
if (email != other.email) return false
|
||||
if (password != other.password) return false
|
||||
return token == other.token
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = host.hashCode()
|
||||
result = 31 * result + email.hashCode()
|
||||
result = 31 * result + password.hashCode()
|
||||
result = 31 * result + token.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -110,21 +110,26 @@ class Tracker @Inject constructor(
|
||||
private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates.Success {
|
||||
if (track.isEmpty()) {
|
||||
// first check or manga was empty on last check
|
||||
return MangaUpdates.Success(manga, emptyList(), isValid = false)
|
||||
return MangaUpdates.Success(manga, emptyList(), isValid = false, channelId = null)
|
||||
}
|
||||
val chapters = requireNotNull(manga.getChapters(branch))
|
||||
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
|
||||
return when {
|
||||
newChapters.isEmpty() -> {
|
||||
MangaUpdates.Success(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId)
|
||||
MangaUpdates.Success(
|
||||
manga = manga,
|
||||
newChapters = emptyList(),
|
||||
isValid = chapters.lastOrNull()?.id == track.lastChapterId,
|
||||
channelId = null
|
||||
)
|
||||
}
|
||||
|
||||
newChapters.size == chapters.size -> {
|
||||
MangaUpdates.Success(manga, emptyList(), isValid = false)
|
||||
MangaUpdates.Success(manga, emptyList(), isValid = false, channelId = null)
|
||||
}
|
||||
|
||||
else -> {
|
||||
MangaUpdates.Success(manga, newChapters, isValid = true)
|
||||
MangaUpdates.Success(manga, newChapters, isValid = true, channelId = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,12 @@ package org.koitharu.kotatsu.tracker.domain.model
|
||||
import java.util.*
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
class MangaTracking(
|
||||
data class MangaTracking(
|
||||
val manga: Manga,
|
||||
val lastChapterId: Long,
|
||||
val lastCheck: Date?,
|
||||
) {
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return lastChapterId == 0L
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaTracking
|
||||
|
||||
if (manga != other.manga) return false
|
||||
if (lastChapterId != other.lastChapterId) return false
|
||||
return lastCheck == other.lastCheck
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = manga.hashCode()
|
||||
result = 31 * result + lastChapterId.hashCode()
|
||||
result = 31 * result + (lastCheck?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,17 @@ sealed interface MangaUpdates {
|
||||
|
||||
val manga: Manga
|
||||
|
||||
class Success(
|
||||
data class Success(
|
||||
override val manga: Manga,
|
||||
val newChapters: List<MangaChapter>,
|
||||
val isValid: Boolean,
|
||||
val channelId: String?,
|
||||
) : MangaUpdates {
|
||||
|
||||
fun isNotEmpty() = newChapters.isNotEmpty()
|
||||
}
|
||||
|
||||
class Failure(
|
||||
data class Failure(
|
||||
override val manga: Manga,
|
||||
val error: Throwable?,
|
||||
) : MangaUpdates {
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.tracker.ui.feed.model
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
class FeedItem(
|
||||
data class FeedItem(
|
||||
val id: Long,
|
||||
val imageUrl: String,
|
||||
val title: String,
|
||||
@@ -11,32 +11,7 @@ class FeedItem(
|
||||
val count: Int,
|
||||
val isNew: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is FeedItem && other.id == id
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FeedItem
|
||||
|
||||
if (id != other.id) return false
|
||||
if (imageUrl != other.imageUrl) return false
|
||||
if (title != other.title) return false
|
||||
if (manga != other.manga) return false
|
||||
if (count != other.count) return false
|
||||
return isNew == other.isNew
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + imageUrl.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + manga.hashCode()
|
||||
result = 31 * result + count
|
||||
result = 31 * result + isNew.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
@@ -148,19 +149,9 @@ class TrackWorker @AssistedInject constructor(
|
||||
send(
|
||||
runCatchingCancellable {
|
||||
tracker.fetchUpdates(track, commit = true)
|
||||
.copy(channelId = channelId)
|
||||
}.onFailure { e ->
|
||||
if (e is CloudFlareProtectedException) {
|
||||
CaptchaNotifier(applicationContext).notify(e)
|
||||
}
|
||||
logger.log("checkUpdatesAsync", e)
|
||||
}.onSuccess { updates ->
|
||||
if (updates.isValid && updates.isNotEmpty()) {
|
||||
showNotification(
|
||||
manga = updates.manga,
|
||||
channelId = channelId,
|
||||
newChapters = updates.newChapters,
|
||||
)
|
||||
}
|
||||
}.getOrElse { error ->
|
||||
MangaUpdates.Failure(
|
||||
manga = track.manga,
|
||||
@@ -171,10 +162,33 @@ class TrackWorker @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onEach {
|
||||
when (it) {
|
||||
is MangaUpdates.Failure -> {
|
||||
val e = it.error
|
||||
if (e is CloudFlareProtectedException) {
|
||||
CaptchaNotifier(applicationContext).notify(e)
|
||||
}
|
||||
}
|
||||
|
||||
is MangaUpdates.Success -> {
|
||||
if (it.isValid && it.isNotEmpty()) {
|
||||
showNotification(
|
||||
manga = it.manga,
|
||||
channelId = it.channelId,
|
||||
newChapters = it.newChapters,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toList(ArrayList(tracks.size))
|
||||
}
|
||||
|
||||
private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List<MangaChapter>) {
|
||||
private suspend fun showNotification(
|
||||
manga: Manga,
|
||||
channelId: String?,
|
||||
newChapters: List<MangaChapter>,
|
||||
) {
|
||||
if (newChapters.isEmpty() || channelId == null || !applicationContext.checkNotificationPermission()) {
|
||||
return
|
||||
}
|
||||
@@ -239,7 +253,10 @@ class TrackWorker @AssistedInject constructor(
|
||||
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
val title = applicationContext.getString(R.string.check_for_new_chapters)
|
||||
val channel = NotificationChannelCompat.Builder(WORKER_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
val channel = NotificationChannelCompat.Builder(
|
||||
WORKER_CHANNEL_ID,
|
||||
NotificationManagerCompat.IMPORTANCE_LOW
|
||||
)
|
||||
.setName(title)
|
||||
.setShowBadge(false)
|
||||
.setVibrationEnabled(false)
|
||||
@@ -260,7 +277,11 @@ class TrackWorker @AssistedInject constructor(
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
|
||||
.build()
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ForegroundInfo(WORKER_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
ForegroundInfo(
|
||||
WORKER_NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
)
|
||||
} else {
|
||||
ForegroundInfo(WORKER_NOTIFICATION_ID, notification)
|
||||
}
|
||||
@@ -320,7 +341,8 @@ class TrackWorker @AssistedInject constructor(
|
||||
}
|
||||
|
||||
fun startNow() {
|
||||
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
||||
val constraints =
|
||||
Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
|
||||
val request = OneTimeWorkRequestBuilder<TrackWorker>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG_ONESHOT)
|
||||
|
||||
@@ -2,28 +2,7 @@ package org.koitharu.kotatsu.tracker.work
|
||||
|
||||
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
|
||||
|
||||
class TrackingItem(
|
||||
data class TrackingItem(
|
||||
val tracking: MangaTracking,
|
||||
val channelId: String?,
|
||||
) {
|
||||
|
||||
operator fun component1() = tracking
|
||||
|
||||
operator fun component2() = channelId
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TrackingItem
|
||||
|
||||
if (tracking != other.tracking) return false
|
||||
return channelId == other.channelId
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = tracking.hashCode()
|
||||
result = 31 * result + channelId.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="@dimen/appwidget_corner_radius_inner" />
|
||||
<solid android:color="@android:color/system_accent2_100" />
|
||||
</shape>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="@dimen/appwidget_corner_radius_background" />
|
||||
<solid android:color="@android:color/system_accent2_50" />
|
||||
</shape>
|
||||
@@ -3,5 +3,5 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="@dimen/appwidget_corner_radius_inner" />
|
||||
<solid android:color="@color/kotatsu_primaryContainer" />
|
||||
<solid android:color="?android:panelColorBackground" />
|
||||
</shape>
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="@dimen/appwidget_corner_radius_background" />
|
||||
<solid android:color="@color/kotatsu_background" />
|
||||
<solid android:color="?android:colorBackground" />
|
||||
</shape>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
app:layout_constraintBottom_toTopOf="@id/textView_subtitle"
|
||||
app:layout_constraintEnd_toStartOf="@id/imageView_handle"
|
||||
app:layout_constraintEnd_toStartOf="@id/imageView_edit"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
@@ -120,10 +120,23 @@
|
||||
app:layout_constraintTop_toTopOf="@id/textView_subtitle"
|
||||
app:srcCompat="@drawable/ic_eye" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/edit"
|
||||
android:padding="@dimen/margin_normal"
|
||||
android:src="@drawable/ic_edit"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/imageView_handle"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView_handle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/reorder"
|
||||
android:padding="@dimen/margin_normal"
|
||||
android:src="@drawable/ic_reorder_handle"
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
android:id="@+id/ssiv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="1dp"
|
||||
app:panEnabled="false"
|
||||
app:quickScaleEnabled="false"
|
||||
app:zoomEnabled="false" />
|
||||
|
||||
<include layout="@layout/layout_page_info" />
|
||||
|
||||
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonFrameLayout>
|
||||
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonFrameLayout>
|
||||
|
||||
@@ -8,70 +8,53 @@
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="8dp"
|
||||
tools:ignore="PrivateResource">
|
||||
|
||||
<include layout="@layout/image_frame" />
|
||||
|
||||
<LinearLayout
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:clipChildren="false"
|
||||
android:ellipsize="marquee"
|
||||
android:labelFor="@id/scrollView"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
tools:ignore="LabelFor"
|
||||
tools:text="@string/color_theme" />
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/linear"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:baselineAligned="true"
|
||||
android:baselineAlignedChildIndex="0"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="marquee"
|
||||
android:labelFor="@id/seekbar"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
tools:ignore="LabelFor" />
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/summary"
|
||||
style="@style/PreferenceSummaryTextStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
<TextView
|
||||
android:id="@android:id/summary"
|
||||
style="@style/PreferenceSummaryTextStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:scrollbars="none"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linear"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
android:numColumns="auto_fit"
|
||||
android:orientation="horizontal"
|
||||
android:padding="4dp"
|
||||
android:scrollbarStyle="insideOverlay"
|
||||
tools:listitem="@layout/item_shelf" />
|
||||
|
||||
<TextView
|
||||
|
||||
11
app/src/main/res/menu/opt_captcha.xml
Normal file
11
app/src/main/res/menu/opt_captcha.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_retry"
|
||||
android:title="@string/try_again"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
@@ -471,4 +471,5 @@
|
||||
<string name="related_manga_summary">Паказаць спіс звязанай мангі. У некаторых выпадках ён можа быць недакладным або адсутнічаць</string>
|
||||
<string name="advanced">Пашыраныя</string>
|
||||
<string name="default_section">Раздзел па змаўчанні</string>
|
||||
<string name="manga_list">Спіс мангі</string>
|
||||
</resources>
|
||||
@@ -7,29 +7,46 @@
|
||||
<item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item>
|
||||
<item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_dark_primary_container</item>
|
||||
<item name="colorOnPrimary">@color/m3_sys_color_dynamic_dark_on_primary</item>
|
||||
<item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_dark_on_primary_container</item>
|
||||
<item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_dark_on_primary_container
|
||||
</item>
|
||||
<item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_dark_inverse_primary</item>
|
||||
<item name="colorSecondary">@color/m3_sys_color_dynamic_dark_secondary</item>
|
||||
<item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_dark_secondary_container</item>
|
||||
<item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_dark_secondary_container
|
||||
</item>
|
||||
<item name="colorOnSecondary">@color/m3_sys_color_dynamic_dark_on_secondary</item>
|
||||
<item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_dark_on_secondary_container</item>
|
||||
<item name="colorOnSecondaryContainer">
|
||||
@color/m3_sys_color_dynamic_dark_on_secondary_container
|
||||
</item>
|
||||
<item name="colorTertiary">@color/m3_sys_color_dynamic_dark_tertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_dark_tertiary_container</item>
|
||||
<item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_dark_tertiary_container
|
||||
</item>
|
||||
<item name="colorOnTertiary">@color/m3_sys_color_dynamic_dark_on_tertiary</item>
|
||||
<item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_dark_on_tertiary_container</item>
|
||||
<item name="colorOnTertiaryContainer">
|
||||
@color/m3_sys_color_dynamic_dark_on_tertiary_container
|
||||
</item>
|
||||
<item name="colorSurface">@color/m3_sys_color_dynamic_dark_surface</item>
|
||||
<item name="colorSurfaceDim">@color/m3_sys_color_dynamic_dark_surface_dim</item>
|
||||
<item name="colorSurfaceBright">@color/m3_sys_color_dynamic_dark_surface_bright</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/m3_sys_color_dynamic_dark_surface_container_lowest</item>
|
||||
<item name="colorSurfaceContainerLow">@color/m3_sys_color_dynamic_dark_surface_container_low</item>
|
||||
<item name="colorSurfaceContainerLowest">
|
||||
@color/m3_sys_color_dynamic_dark_surface_container_lowest
|
||||
</item>
|
||||
<item name="colorSurfaceContainerLow">
|
||||
@color/m3_sys_color_dynamic_dark_surface_container_low
|
||||
</item>
|
||||
<item name="colorSurfaceContainer">@color/m3_sys_color_dynamic_dark_surface_container</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/m3_sys_color_dynamic_dark_surface_container_high</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/m3_sys_color_dynamic_dark_surface_container_highest</item>
|
||||
<item name="colorSurfaceContainerHigh">
|
||||
@color/m3_sys_color_dynamic_dark_surface_container_high
|
||||
</item>
|
||||
<item name="colorSurfaceContainerHighest">
|
||||
@color/m3_sys_color_dynamic_dark_surface_container_highest
|
||||
</item>
|
||||
<item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_dark_surface_variant</item>
|
||||
<item name="colorOnSurface">@color/m3_sys_color_dynamic_dark_on_surface</item>
|
||||
<item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_dark_on_surface_variant
|
||||
</item>
|
||||
<item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_dark_inverse_surface</item>
|
||||
<item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_dark_inverse_on_surface</item>
|
||||
<item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_dark_inverse_on_surface
|
||||
</item>
|
||||
<item name="colorOnBackground">@color/m3_sys_color_dynamic_dark_on_background</item>
|
||||
<item name="colorError">@color/m3_sys_color_dark_error</item>
|
||||
<item name="colorErrorContainer">@color/m3_sys_color_dark_error_container</item>
|
||||
@@ -42,26 +59,42 @@
|
||||
<item name="m3ColorExploreButton">@android:color/system_neutral2_800</item>
|
||||
<item name="m3ColorBottomMenuBackground">@android:color/system_neutral2_800</item>
|
||||
<!-- Default Framework Text Colors. -->
|
||||
<item name="android:textColorPrimary">@color/m3_dynamic_dark_default_color_primary_text</item>
|
||||
<item name="android:textColorPrimaryInverse">@color/m3_dynamic_default_color_primary_text</item>
|
||||
<item name="android:textColorSecondary">@color/m3_dynamic_dark_default_color_secondary_text</item>
|
||||
<item name="android:textColorSecondaryInverse">@color/m3_dynamic_default_color_secondary_text</item>
|
||||
<item name="android:textColorTertiary">@color/m3_dynamic_dark_default_color_secondary_text</item>
|
||||
<item name="android:textColorTertiaryInverse">@color/m3_dynamic_default_color_secondary_text</item>
|
||||
<item name="android:textColorPrimaryDisableOnly">@color/m3_dynamic_dark_primary_text_disable_only</item>
|
||||
<item name="android:textColorPrimaryInverseDisableOnly">@color/m3_dynamic_primary_text_disable_only</item>
|
||||
<item name="android:textColorPrimary">@color/m3_dynamic_dark_default_color_primary_text
|
||||
</item>
|
||||
<item name="android:textColorPrimaryInverse">@color/m3_dynamic_default_color_primary_text
|
||||
</item>
|
||||
<item name="android:textColorSecondary">
|
||||
@color/m3_dynamic_dark_default_color_secondary_text
|
||||
</item>
|
||||
<item name="android:textColorSecondaryInverse">
|
||||
@color/m3_dynamic_default_color_secondary_text
|
||||
</item>
|
||||
<item name="android:textColorTertiary">@color/m3_dynamic_dark_default_color_secondary_text
|
||||
</item>
|
||||
<item name="android:textColorTertiaryInverse">
|
||||
@color/m3_dynamic_default_color_secondary_text
|
||||
</item>
|
||||
<item name="android:textColorPrimaryDisableOnly">
|
||||
@color/m3_dynamic_dark_primary_text_disable_only
|
||||
</item>
|
||||
<item name="android:textColorPrimaryInverseDisableOnly">
|
||||
@color/m3_dynamic_primary_text_disable_only
|
||||
</item>
|
||||
<item name="android:textColorHint">@color/m3_dynamic_dark_hint_foreground</item>
|
||||
<item name="android:textColorHintInverse">@color/m3_dynamic_hint_foreground</item>
|
||||
<item name="android:textColorHighlight">@color/m3_dynamic_dark_highlighted_text</item>
|
||||
<item name="android:textColorHighlightInverse">@color/m3_dynamic_highlighted_text</item>
|
||||
<item name="android:textColorAlertDialogListItem">@color/m3_dynamic_dark_default_color_primary_text</item>
|
||||
<item name="android:textColorAlertDialogListItem">
|
||||
@color/m3_dynamic_dark_default_color_primary_text
|
||||
</item>
|
||||
<!-- Fixes -->
|
||||
<item name="bottomNavigationStyle">@style/Widget.Kotatsu.BottomNavigationView</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Kotatsu.AppWidgetContainer" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||
<item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_secondary_container</item>
|
||||
<item name="android:panelColorBackground">@color/m3_sys_color_dynamic_dark_inverse_primary</item>
|
||||
<item name="android:colorBackground">@color/m3_ref_palette_dynamic_secondary20</item>
|
||||
<item name="android:panelColorBackground">@color/m3_ref_palette_dynamic_secondary40</item>
|
||||
<item name="colorTertiary">@color/m3_ref_palette_dynamic_secondary40</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -330,7 +330,7 @@
|
||||
<string name="reader_control_ltr_summary">Нажатие на правый край или нажатие правой клавиши всегда переключает на следующую страницу</string>
|
||||
<string name="reader_control_ltr">Эргономичное управление режимом чтения</string>
|
||||
<string name="reset">Сбросить</string>
|
||||
<string name="discard">Отменить</string>
|
||||
<string name="discard">Отклонить</string>
|
||||
<string name="text_unsaved_changes_prompt">Сохранить или отменить несохранённые изменения\?</string>
|
||||
<string name="contrast">Контрастность</string>
|
||||
<string name="brightness">Яркость</string>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<string name="computing_">กำลังคำนวน…</string>
|
||||
<string name="create_shortcut">สร้าง shortcut…</string>
|
||||
<string name="sort_order">เรียวตามลำดับ</string>
|
||||
<string name="enable">เปิดการใช้งาน</string>
|
||||
<string name="enable">เปิดใช้งาน</string>
|
||||
<string name="downloads_removed">การดาวน์โหลดชองคุณได้ถูกลบแล้ว</string>
|
||||
<string name="downloads_cancelled">การดาวน์โหลดของคุณได้ถูกยกเลิกแล้ว</string>
|
||||
<string name="downloaded">ดาวน์โหลดแล้ว</string>
|
||||
@@ -215,7 +215,7 @@
|
||||
<string name="screenshots_block_nsfw">บล็อกบน NSFW</string>
|
||||
<string name="screenshots_block_all">บล็อกตลอด</string>
|
||||
<string name="suggestions">คําแนะนํา</string>
|
||||
<string name="suggestions_enable">เปิดใช้งานคำแนะนำ</string>
|
||||
<string name="suggestions_enable">เปิดใช้คำแนะนำ</string>
|
||||
<string name="manga_shelf">ชั้น</string>
|
||||
<string name="not_available">ไม่สามารถใช้ได้</string>
|
||||
<string name="backup_information">คุณสามารถสํารองข้อมูลประวัติและรายการโปรดและกู้คืน</string>
|
||||
@@ -231,4 +231,42 @@
|
||||
<string name="about_app_translation">การแปล</string>
|
||||
<string name="auth_complete">ได้รับอนุญาต</string>
|
||||
<string name="default_mode">โหมดเริ่มต้น</string>
|
||||
<string name="new_sources_text">มีแหล่งมังงะใหม่ให้เลือก</string>
|
||||
<string name="crash_text">มีบางอย่างผิดพลาด. กรุณารายงานข้อผิดพลาดไปยังนักพัฒนาเพื่อแก้ไข</string>
|
||||
<string name="reader_mode_hint">การตั้งค่าที่ตั้งไว้จะถูกบันทึกไว้สำหรับมังงะเรื่องนี้</string>
|
||||
<string name="suggestions_summary">แนะนำมังงะตามความต้องการของคุณ</string>
|
||||
<string name="text_suggestion_holder">เริ่มอ่านมังงะแล้วคุณจะได้รับการแนะนำมังงะตามความชอบของคุณ</string>
|
||||
<string name="exclude_nsfw_from_suggestions">อย่าแนะนำมังงะ NSFW</string>
|
||||
<string name="reset_filter">รีเซ็ตการกรอง</string>
|
||||
<string name="onboard_text">เลือกภาษาที่คุณต้องการอ่านมังงะ คุณสามารถเปลี่ยนได้ในภายหลังในการตั้งค่า</string>
|
||||
<string name="never">ไม่เคย</string>
|
||||
<string name="only_using_wifi">ใช้ Wi-Fi เท่านั้น</string>
|
||||
<string name="always">ตลอด</string>
|
||||
<string name="preload_pages">โหลดหน้าล่วงหน้า</string>
|
||||
<string name="nsfw">18+</string>
|
||||
<string name="percent_string_pattern">%1$s%%</string>
|
||||
<string name="text_delete_local_manga_batch">ลบรายการที่เลือกออกจากอุปกรณ์อย่างถาวรไหม\?</string>
|
||||
<string name="removal_completed">การลบเสร็จสมบูรณ์</string>
|
||||
<string name="download_slowdown_summary">ช่วยหลีกเลี่ยงการบล็อก IP ของคุณ</string>
|
||||
<string name="canceled">ยกเลิก</string>
|
||||
<string name="account_already_exists">บัญชีนี้มีอยู่แล้ว</string>
|
||||
<string name="back">กลับ</string>
|
||||
<string name="enabled">เปิดใช้งานแล้ว</string>
|
||||
<string name="disabled">ปิดการใช้งานแล้ว</string>
|
||||
<string name="sync_title">ซิงค์ข้อมูลของคุณ</string>
|
||||
<string name="email_enter_hint">กรอกอีเมลของคุณเพื่อดำเนินการต่อ</string>
|
||||
<string name="hide">ซ่อน</string>
|
||||
<string name="notifications_enable">เปิดการแจ้งเตือน</string>
|
||||
<string name="name">ชื่อ</string>
|
||||
<string name="edit">แก้ไข</string>
|
||||
<string name="undo">ย้อนกลับ</string>
|
||||
<string name="disable_battery_optimization">ปิดใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่</string>
|
||||
<string name="disable_battery_optimization_summary">ช่วยในการตรวจสอบการอัปเดตพื้นหลัง</string>
|
||||
<string name="send">ส่ง</string>
|
||||
<string name="status_reading">กำลังอ่าน</string>
|
||||
<string name="status_re_reading">อ่านซ้ำ</string>
|
||||
<string name="status_completed">อ่านเสร็จแล้ว</string>
|
||||
<string name="status_on_hold">พักไว้</string>
|
||||
<string name="disable_all">ปิดการใช้งานทั้งหมด</string>
|
||||
<string name="report">รีพอร์ต</string>
|
||||
</resources>
|
||||
@@ -13,31 +13,51 @@
|
||||
<item name="isMaterial3DynamicColorApplied">true</item>
|
||||
<!-- Color palettes -->
|
||||
<item name="colorPrimary">@color/m3_sys_color_dynamic_light_primary</item>
|
||||
<item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_light_primary_container</item>
|
||||
<item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_light_primary_container
|
||||
</item>
|
||||
<item name="colorOnPrimary">@color/m3_sys_color_dynamic_light_on_primary</item>
|
||||
<item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_light_on_primary_container</item>
|
||||
<item name="colorOnPrimaryContainer">
|
||||
@color/m3_sys_color_dynamic_light_on_primary_container
|
||||
</item>
|
||||
<item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_light_inverse_primary</item>
|
||||
<item name="colorSecondary">@color/m3_sys_color_dynamic_light_secondary</item>
|
||||
<item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_light_secondary_container</item>
|
||||
<item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_light_secondary_container
|
||||
</item>
|
||||
<item name="colorOnSecondary">@color/m3_sys_color_dynamic_light_on_secondary</item>
|
||||
<item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_light_on_secondary_container</item>
|
||||
<item name="colorOnSecondaryContainer">
|
||||
@color/m3_sys_color_dynamic_light_on_secondary_container
|
||||
</item>
|
||||
<item name="colorTertiary">@color/m3_sys_color_dynamic_light_tertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_light_tertiary_container</item>
|
||||
<item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_light_tertiary_container
|
||||
</item>
|
||||
<item name="colorOnTertiary">@color/m3_sys_color_dynamic_light_on_tertiary</item>
|
||||
<item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_light_on_tertiary_container</item>
|
||||
<item name="colorOnTertiaryContainer">
|
||||
@color/m3_sys_color_dynamic_light_on_tertiary_container
|
||||
</item>
|
||||
<item name="colorSurface">@color/m3_sys_color_dynamic_light_surface</item>
|
||||
<item name="colorSurfaceDim">@color/m3_sys_color_dynamic_light_surface_dim</item>
|
||||
<item name="colorSurfaceBright">@color/m3_sys_color_dynamic_light_surface_bright</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/m3_sys_color_dynamic_light_surface_container_lowest</item>
|
||||
<item name="colorSurfaceContainerLow">@color/m3_sys_color_dynamic_light_surface_container_low</item>
|
||||
<item name="colorSurfaceContainer">@color/m3_sys_color_dynamic_light_surface_container</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/m3_sys_color_dynamic_light_surface_container_high</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/m3_sys_color_dynamic_light_surface_container_highest</item>
|
||||
<item name="colorSurfaceContainerLowest">
|
||||
@color/m3_sys_color_dynamic_light_surface_container_lowest
|
||||
</item>
|
||||
<item name="colorSurfaceContainerLow">
|
||||
@color/m3_sys_color_dynamic_light_surface_container_low
|
||||
</item>
|
||||
<item name="colorSurfaceContainer">@color/m3_sys_color_dynamic_light_surface_container
|
||||
</item>
|
||||
<item name="colorSurfaceContainerHigh">
|
||||
@color/m3_sys_color_dynamic_light_surface_container_high
|
||||
</item>
|
||||
<item name="colorSurfaceContainerHighest">
|
||||
@color/m3_sys_color_dynamic_light_surface_container_highest
|
||||
</item>
|
||||
<item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_light_surface_variant</item>
|
||||
<item name="colorOnSurface">@color/m3_sys_color_dynamic_light_on_surface</item>
|
||||
<item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_light_on_surface_variant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_light_on_surface_variant
|
||||
</item>
|
||||
<item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_light_inverse_surface</item>
|
||||
<item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_light_inverse_on_surface</item>
|
||||
<item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_light_inverse_on_surface
|
||||
</item>
|
||||
<item name="colorOnBackground">@color/m3_sys_color_dynamic_light_on_background</item>
|
||||
<item name="colorError">@color/m3_sys_color_light_error</item>
|
||||
<item name="colorErrorContainer">@color/m3_sys_color_light_error_container</item>
|
||||
@@ -51,25 +71,40 @@
|
||||
<item name="m3ColorBottomMenuBackground">@android:color/system_neutral2_100</item>
|
||||
<!-- Default Framework Text Colors. -->
|
||||
<item name="android:textColorPrimary">@color/m3_dynamic_default_color_primary_text</item>
|
||||
<item name="android:textColorPrimaryInverse">@color/m3_dynamic_dark_default_color_primary_text</item>
|
||||
<item name="android:textColorSecondary">@color/m3_dynamic_default_color_secondary_text</item>
|
||||
<item name="android:textColorSecondaryInverse">@color/m3_dynamic_dark_default_color_secondary_text</item>
|
||||
<item name="android:textColorPrimaryInverse">
|
||||
@color/m3_dynamic_dark_default_color_primary_text
|
||||
</item>
|
||||
<item name="android:textColorSecondary">@color/m3_dynamic_default_color_secondary_text
|
||||
</item>
|
||||
<item name="android:textColorSecondaryInverse">
|
||||
@color/m3_dynamic_dark_default_color_secondary_text
|
||||
</item>
|
||||
<item name="android:textColorTertiary">@color/m3_dynamic_default_color_secondary_text</item>
|
||||
<item name="android:textColorTertiaryInverse">@color/m3_dynamic_dark_default_color_secondary_text</item>
|
||||
<item name="android:textColorPrimaryDisableOnly">@color/m3_dynamic_primary_text_disable_only</item>
|
||||
<item name="android:textColorPrimaryInverseDisableOnly">@color/m3_dynamic_dark_primary_text_disable_only</item>
|
||||
<item name="android:textColorTertiaryInverse">
|
||||
@color/m3_dynamic_dark_default_color_secondary_text
|
||||
</item>
|
||||
<item name="android:textColorPrimaryDisableOnly">
|
||||
@color/m3_dynamic_primary_text_disable_only
|
||||
</item>
|
||||
<item name="android:textColorPrimaryInverseDisableOnly">
|
||||
@color/m3_dynamic_dark_primary_text_disable_only
|
||||
</item>
|
||||
<item name="android:textColorHint">@color/m3_dynamic_hint_foreground</item>
|
||||
<item name="android:textColorHintInverse">@color/m3_dynamic_dark_hint_foreground</item>
|
||||
<item name="android:textColorHighlight">@color/m3_dynamic_highlighted_text</item>
|
||||
<item name="android:textColorHighlightInverse">@color/m3_dynamic_dark_highlighted_text</item>
|
||||
<item name="android:textColorAlertDialogListItem">@color/m3_dynamic_default_color_primary_text</item>
|
||||
<item name="android:textColorHighlightInverse">@color/m3_dynamic_dark_highlighted_text
|
||||
</item>
|
||||
<item name="android:textColorAlertDialogListItem">
|
||||
@color/m3_dynamic_default_color_primary_text
|
||||
</item>
|
||||
<!-- Fixes -->
|
||||
<item name="bottomNavigationStyle">@style/Widget.Kotatsu.BottomNavigationView</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Kotatsu.AppWidgetContainer" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||
<item name="android:colorBackground">@color/m3_sys_color_dynamic_light_secondary_container</item>
|
||||
<item name="android:panelColorBackground">@color/m3_sys_color_dynamic_light_inverse_primary</item>
|
||||
<item name="android:colorBackground">@color/m3_ref_palette_dynamic_secondary95</item>
|
||||
<item name="android:panelColorBackground">@color/m3_ref_palette_dynamic_secondary90</item>
|
||||
<item name="colorTertiary">@color/m3_ref_palette_dynamic_secondary70</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -478,4 +478,5 @@
|
||||
<string name="advanced">Advanced</string>
|
||||
<string name="default_section">Default section</string>
|
||||
<string name="manga_list">Manga list</string>
|
||||
<string name="error_corrupted_file">Invalid data is returned or file is corrupted</string>
|
||||
</resources>
|
||||
|
||||
@@ -75,23 +75,31 @@
|
||||
<item name="textInputStyle">@style/Widget.Material3.TextInputLayout.OutlinedBox</item>
|
||||
<item name="toolbarStyle">@style/Widget.Material3.Toolbar</item>
|
||||
<item name="appBarLayoutStyle">@style/Widget.Kotatsu.AppBarLayout</item>
|
||||
<item name="bottomNavigationStyle">@style/Widget.Kotatsu.BottomNavigationView.ColoredIndicators</item>
|
||||
<item name="bottomNavigationStyle">
|
||||
@style/Widget.Kotatsu.BottomNavigationView.ColoredIndicators
|
||||
</item>
|
||||
<item name="tabStyle">@style/Widget.Kotatsu.Tabs</item>
|
||||
<item name="materialCardViewStyle">@style/Widget.Kotatsu.MaterialCardView.Filled</item>
|
||||
<item name="recyclerViewStyle">@style/Widget.Kotatsu.RecyclerView</item>
|
||||
<item name="fastScrollerStyle">@style/Widget.Kotatsu.FastScroller</item>
|
||||
<item name="listItemTextViewStyle">@style/Widget.Kotatsu.ListItemTextView</item>
|
||||
<item name="materialSwitchStyle">@style/Widget.Material3.CompoundButton.MaterialSwitch</item>
|
||||
<item name="materialSwitchStyle">@style/Widget.Material3.CompoundButton.MaterialSwitch
|
||||
</item>
|
||||
<item name="switchPreferenceCompatStyle">@style/Preference.SwitchPreferenceCompat.M3</item>
|
||||
<item name="collapsingToolbarLayoutStyle">@style/Widget.Kotatsu.CollapsingToolbar</item>
|
||||
<item name="circularProgressIndicatorStyle">@style/Widget.Kotatsu.CircularProgressIndicator</item>
|
||||
<item name="linearProgressIndicatorStyle">@style/Widget.Kotatsu.LinearProgressIndicator</item>
|
||||
<item name="circularProgressIndicatorStyle">
|
||||
@style/Widget.Kotatsu.CircularProgressIndicator
|
||||
</item>
|
||||
<item name="linearProgressIndicatorStyle">@style/Widget.Kotatsu.LinearProgressIndicator
|
||||
</item>
|
||||
<item name="bottomSheetDragHandleStyle">@style/Widget.Kotatsu.BottomSheet.DragHandle</item>
|
||||
|
||||
<!-- Text appearance -->
|
||||
<item name="actionMenuTextAppearance">@style/TextAppearance.Kotatsu.Menu</item>
|
||||
<item name="android:textAppearanceListItem">?attr/textAppearanceBodyLarge</item>
|
||||
<item name="android:textAppearanceListItemSecondary">@style/TextAppearance.Kotatsu.Preference.Secondary</item>
|
||||
<item name="android:textAppearanceListItemSecondary">
|
||||
@style/TextAppearance.Kotatsu.Preference.Secondary
|
||||
</item>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -106,7 +114,7 @@
|
||||
<style name="ThemeOverlay.Kotatsu.Amoled" parent="" />
|
||||
|
||||
<style name="Theme.Kotatsu.AppWidgetContainer" parent="@style/Theme.MaterialComponents.Light">
|
||||
<item name="android:colorBackground">@android:color/transparent</item>
|
||||
<item name="android:colorBackground">@color/kotatsu_background</item>
|
||||
<item name="android:panelColorBackground">@color/kotatsu_primaryContainer</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user