Compare commits

..

23 Commits

Author SHA1 Message Date
4c2197aa5d Option to retry captcha resolving 2023-09-05 11:26:57 +03:00
a679b6775d Exclude captcha actvity from recent 2023-09-05 10:35:32 +03:00
d3e4e97c6f Fix tracker operations parallelism 2023-09-05 10:30:20 +03:00
d1b0af85c4 Update parsers 2023-09-05 10:30:20 +03:00
ce95e0657b Translated using Weblate (Russian)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-09-05 10:30:08 +03:00
Nayuki
6bb159a6d9 Translated using Weblate (Thai)
Currently translated at 57.5% (274 of 476 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-09-05 10:30:08 +03:00
Макар Разин
a75583f750 Translated using Weblate (Belarusian)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2023-09-05 10:30:08 +03:00
fff9df9609 Fix categories and sources reordering 2023-09-03 17:39:52 +03:00
f9609edea5 Fallback to old systemUiVisibility in reader 2023-09-03 17:06:37 +03:00
f1245742c0 Merge branch 'File_creation_time' of github.com:Isira-Seneviratne/Kotatsu into Isira-Seneviratne-File_creation_time 2023-09-01 13:43:58 +03:00
42d933ba83 Bump version 2023-09-01 13:17:07 +03:00
4df644e21f Fix branch prediction 2023-09-01 12:02:31 +03:00
ViAnh
e4ba738c00 Use WeakHashMap to store views 2023-08-31 19:35:53 +03:00
ViAnh
b7f09243aa Avoid unnecessary child layout in webtoon recycler 2023-08-31 19:35:53 +03:00
ViAnh
50d4c41855 Fix webtoon under scale 2023-08-31 19:35:53 +03:00
67adc8b681 Fix widgets in dark theme 2023-08-31 19:28:25 +03:00
34fb4af9fe Fix color scheme preference 2023-08-31 19:11:29 +03:00
05241f73d9 Improve categories managing 2023-08-31 19:11:29 +03:00
d666e4b967 Fix small webtoon pages 2023-08-31 19:11:29 +03:00
b4bf607d3a Merge pull request #470 from Isira-Seneviratne/Data_classes 2023-08-31 09:17:03 +03:00
Isira Seneviratne
a417d5aaa9 Apply requested changes 2023-08-30 19:27:34 +05:30
Isira Seneviratne
ad3b5dde91 Convert more classes to data classes 2023-08-27 07:10:32 +05:30
Isira Seneviratne
ded7cdb71e Obtain file creation time 2023-08-27 06:19:07 +05:30
80 changed files with 643 additions and 1039 deletions

View File

@@ -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'
}

View File

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

View File

@@ -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>() {

View File

@@ -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
}
}
)
}

View File

@@ -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
}
}
)

View File

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

View File

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

View File

@@ -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()

View File

@@ -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()
}
}

View File

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

View File

@@ -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()) {

View File

@@ -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)
}
}

View File

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

View File

@@ -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() {

View File

@@ -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)
}
}
}

View File

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

View File

@@ -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) {

View File

@@ -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()
}

View File

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

View File

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

View File

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

View File

@@ -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"
}

View File

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

View File

@@ -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()
}
}

View File

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

View File

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

View File

@@ -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)"
}
}

View File

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

View File

@@ -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)
}

View File

@@ -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,
),
)
}
}
}

View File

@@ -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())
}
}

View File

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

View File

@@ -46,4 +46,8 @@ class CategoryListModel(
result = 31 * result + category.isVisibleInLibrary.hashCode()
return result
}
override fun toString(): String {
return "CategoryListModel(categoryId=${category.id})"
}
}

View File

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

View File

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

View File

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

View File

@@ -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
}
}
)

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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})"
}

View File

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

View File

@@ -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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
}

View File

@@ -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
}
}
)

View File

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

View File

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

View File

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

View File

@@ -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()
}

View File

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

View File

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

View File

@@ -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
}
)
}

View File

@@ -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)
}

View File

@@ -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
}
}
)

View File

@@ -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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
android:numColumns="auto_fit"
android:orientation="horizontal"
android:padding="4dp"
android:scrollbarStyle="insideOverlay"
tools:listitem="@layout/item_shelf" />
<TextView

View 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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