Compare commits

..

23 Commits
v6.0 ... v6.0.2

Author SHA1 Message Date
Koitharu
42d933ba83 Bump version 2023-09-01 13:17:07 +03:00
Koitharu
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
Koitharu
67adc8b681 Fix widgets in dark theme 2023-08-31 19:28:25 +03:00
Koitharu
34fb4af9fe Fix color scheme preference 2023-08-31 19:11:29 +03:00
Koitharu
05241f73d9 Improve categories managing 2023-08-31 19:11:29 +03:00
Koitharu
d666e4b967 Fix small webtoon pages 2023-08-31 19:11:29 +03:00
Koitharu
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
Koitharu
4b6b2c3e12 Fix favorites selector 2023-08-30 14:43:57 +03:00
Koitharu
51300e30bd Improve favicon loading 2023-08-30 14:41:44 +03:00
Koitharu
399ac07fb3 Fix storage usage calculation 2023-08-30 14:21:09 +03:00
Eryk Michalak
eeba161235 Translated using Weblate (Polish)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Polish)

Currently translated at 95.5% (455 of 476 strings)

Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-30 12:45:08 +03:00
Nayuki
088a388812 Translated using Weblate (Thai)
Currently translated at 49.5% (236 of 476 strings)

Translated using Weblate (Thai)

Currently translated at 42.8% (204 of 476 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-08-30 12:45:08 +03:00
gallegonovato
943bba3ee8 Translated using Weblate (Spanish)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-30 12:45:08 +03:00
Koitharu
18c3229200 Handle TooManyRequestsException during downloading 2023-08-29 20:23:20 +03:00
Koitharu
9b6f511ac6 Do not discard image requests in onViewRecycled 2023-08-29 17:35:00 +03:00
Isira Seneviratne
ad3b5dde91 Convert more classes to data classes 2023-08-27 07:10:32 +05:30
Koitharu
74ca19a931 Improve widgets ui #457 2023-08-26 18:35:49 +03:00
Koitharu
2684a7384e Restore covers using interceptor 2023-08-26 16:44:09 +03:00
Koitharu
2c561824ef Fix default reader mode option #468 #466 2023-08-25 13:26:48 +03:00
107 changed files with 942 additions and 1216 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 573 versionCode = 575
versionName = '6.0' versionName = '6.0.2'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner" testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp { ksp {

View File

@@ -148,13 +148,21 @@
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity" android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
android:label="@string/manage_categories" /> android:label="@string/manage_categories" />
<activity <activity
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetConfigActivity"
android:exported="true" android:exported="true"
android:label="@string/manga_shelf"> android:label="@string/manga_shelf">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetConfigActivity"
android:exported="true"
android:label="@string/recent_manga">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity <activity
android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity" android:name="org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity"
android:label="@string/search" /> android:label="@string/search" />

View File

@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -40,8 +39,4 @@ fun bookmarkListAD(
enqueueWith(coil) enqueueWith(coil)
} }
} }
onViewRecycled {
binding.imageViewThumb.disposeImageRequest()
}
} }

View File

@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -42,8 +41,4 @@ fun bookmarkLargeAD(
} }
binding.progressView.percent = item.percent binding.progressView.percent = item.percent
} }
onViewRecycled {
binding.imageViewThumb.disposeImageRequest()
}
} }

View File

@@ -1,28 +1,28 @@
package org.koitharu.kotatsu.browser.cloudflare package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import coil.EventListener
import coil.request.ErrorResult import coil.request.ErrorResult
import coil.request.ImageRequest import coil.request.ImageRequest
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
class CaptchaNotifier( class CaptchaNotifier(
private val context: Context, private val context: Context,
) : ImageRequest.Listener { ) : EventListener {
@SuppressLint("MissingPermission")
fun notify(exception: CloudFlareProtectedException) { fun notify(exception: CloudFlareProtectedException) {
val manager = NotificationManagerCompat.from(context) if (!context.checkNotificationPermission()) {
if (!manager.areNotificationsEnabled()) {
return return
} }
val manager = NotificationManagerCompat.from(context)
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT) val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(context.getString(R.string.captcha_required)) .setName(context.getString(R.string.captcha_required))
.setShowBadge(true) .setShowBadge(true)

View File

@@ -13,7 +13,6 @@ import coil.decode.SvgDecoder
import coil.disk.DiskCache import coil.disk.DiskCache
import coil.util.DebugLogger import coil.util.DebugLogger
import dagger.Binds import dagger.Binds
import dagger.Lazy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -26,6 +25,7 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.StubContentCache import org.koitharu.kotatsu.core.cache.StubContentCache
@@ -47,7 +47,7 @@ import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestorer import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
@@ -91,7 +91,7 @@ interface AppModule {
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor, imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory, pageFetcherFactory: MangaPageFetcher.Factory,
coverRestorerProvider: Lazy<CoverRestorer>, coverRestoreInterceptor: CoverRestoreInterceptor,
): ImageLoader { ): ImageLoader {
val diskCacheFactory = { val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir val rootDir = context.externalCacheDir ?: context.cacheDir
@@ -108,7 +108,7 @@ interface AppModule {
.diskCache(diskCacheFactory) .diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null) .logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice()) .allowRgb565(context.isLowRamDevice())
.eventListenerFactory { coverRestorerProvider.get() } .eventListener(CaptchaNotifier(context))
.components( .components(
ComponentRegistry.Builder() ComponentRegistry.Builder()
.add(SvgDecoder.Factory()) .add(SvgDecoder.Factory())
@@ -116,6 +116,7 @@ interface AppModule {
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory)) .add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
.add(pageFetcherFactory) .add(pageFetcherFactory)
.add(imageProxyInterceptor) .add(imageProxyInterceptor)
.add(coverRestoreInterceptor)
.build(), .build(),
).build() ).build()
} }

View File

@@ -20,25 +20,8 @@ interface ContentCache {
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
class Key( data class Key(
val source: MangaSource, val source: MangaSource,
val url: String, 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.Junction
import androidx.room.Relation import androidx.room.Relation
class MangaWithTags( data class MangaWithTags(
@Embedded val manga: MangaEntity, @Embedded val manga: MangaEntity,
@Relation( @Relation(
parentColumn = "manga_id", parentColumn = "manga_id",
@@ -12,21 +12,4 @@ class MangaWithTags(
associateBy = Junction(MangaTagsEntity::class) associateBy = Junction(MangaTagsEntity::class)
) )
val tags: List<TagEntity>, 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

@@ -6,4 +6,8 @@ import java.util.Date
class TooManyRequestExceptions( class TooManyRequestExceptions(
val url: String, val url: String,
val retryAt: Date?, val retryAt: Date?,
) : IOException() ) : IOException() {
val retryAfter: Long
get() = if (retryAt == null) 0 else (retryAt.time - System.currentTimeMillis()).coerceAtLeast(0)
}

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.github
import java.util.* import java.util.*
class VersionId( data class VersionId(
val major: Int, val major: Int,
val minor: Int, val minor: Int,
val build: Int, val build: Int,
@@ -30,28 +30,6 @@ class VersionId(
return variantNumber.compareTo(other.variantNumber) 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)) { private fun variantWeight(variantType: String) = when (variantType.lowercase(Locale.ROOT)) {
"a", "alpha" -> 1 "a", "alpha" -> 1
"b", "beta" -> 2 "b", "beta" -> 2

View File

@@ -48,10 +48,10 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
if (groups.size == 1) { if (groups.size == 1) {
return groups.keys.first() return groups.keys.first()
} }
val candidates = HashMap<String?, List<MangaChapter>>(groups.size)
for (locale in LocaleListCompat.getAdjustedDefault()) { for (locale in LocaleListCompat.getAdjustedDefault()) {
val displayLanguage = locale.getDisplayLanguage(locale) val displayLanguage = locale.getDisplayLanguage(locale)
val displayName = locale.getDisplayName(locale) val displayName = locale.getDisplayName(locale)
val candidates = HashMap<String?, List<MangaChapter>>(3)
for (branch in groups.keys) { for (branch in groups.keys) {
if (branch != null && ( if (branch != null && (
branch.contains(displayLanguage, ignoreCase = true) || branch.contains(displayLanguage, ignoreCase = true) ||
@@ -61,8 +61,11 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
candidates[branch] = groups[branch] ?: continue 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 val Manga.isLocal: Boolean

View File

@@ -8,7 +8,7 @@ import java.io.ObjectInputStream
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
class CookieWrapper( data class CookieWrapper(
val cookie: Cookie, val cookie: Cookie,
) { ) {
@@ -66,17 +66,4 @@ class CookieWrapper(
fun key(): String { fun key(): String {
return (if (cookie.secure) "https" else "http") + "://" + cookie.domain + cookie.path + "|" + cookie.name 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

@@ -14,6 +14,7 @@ import coil.network.HttpException
import coil.request.Options import coil.request.Options
import coil.size.Size import coil.size.Size
import coil.size.pxOrElse import coil.size.pxOrElse
import kotlinx.coroutines.ensureActive
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@@ -25,11 +26,13 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.util.withExtraCloseable import org.koitharu.kotatsu.local.data.util.withExtraCloseable
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
import java.net.HttpURLConnection import java.net.HttpURLConnection
import kotlin.coroutines.coroutineContext
private const val FALLBACK_SIZE = 9999 // largest icon private const val FALLBACK_SIZE = 9999 // largest icon
@@ -55,13 +58,16 @@ class FaviconFetcher(
options.size.height.pxOrElse { FALLBACK_SIZE }, options.size.height.pxOrElse { FALLBACK_SIZE },
) )
var favicons = repo.getFavicons() var favicons = repo.getFavicons()
var lastError: Exception? = null
while (favicons.isNotEmpty()) { while (favicons.isNotEmpty()) {
val icon = favicons.find(sizePx) ?: throwNSEE() coroutineContext.ensureActive()
val icon = favicons.find(sizePx) ?: throwNSEE(lastError)
val response = try { val response = try {
loadIcon(icon.url, mangaSource) loadIcon(icon.url, mangaSource)
} catch (e: CloudFlareProtectedException) { } catch (e: CloudFlareProtectedException) {
throw e throw e
} catch (e: HttpException) { } catch (e: HttpException) {
lastError = e
favicons -= icon favicons -= icon
continue continue
} }
@@ -75,7 +81,7 @@ class FaviconFetcher(
dataSource = response.toDataSource(), dataSource = response.toDataSource(),
) )
} }
throwNSEE() throwNSEE(lastError)
} }
private suspend fun loadIcon(url: String, source: MangaSource): Response { private suspend fun loadIcon(url: String, source: MangaSource): Response {
@@ -105,14 +111,14 @@ class FaviconFetcher(
) )
} }
private fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? { private suspend fun writeToDiskCache(body: ResponseBody): DiskCache.Snapshot? {
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) { if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
return null return null
} }
val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null
try { try {
fileSystem.write(editor.data) { fileSystem.write(editor.data) {
body.source().readAll(this) writeAllCancellable(body.source())
} }
return editor.commitAndOpenSnapshot() return editor.commitAndOpenSnapshot()
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -154,7 +160,13 @@ class FaviconFetcher(
append(height.toString()) append(height.toString())
} }
private fun throwNSEE(): Nothing = throw NoSuchElementException("No favicons found") private fun throwNSEE(lastError: Exception?): Nothing {
if (lastError != null) {
throw lastError
} else {
throw NoSuchElementException("No favicons found")
}
}
class Factory( class Factory(
context: Context, context: Context,

View File

@@ -1,15 +1,38 @@
package org.koitharu.kotatsu.core.prefs package org.koitharu.kotatsu.core.prefs
import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.os.Build
import androidx.core.content.edit import androidx.core.content.edit
private const val CATEGORY_ID = "cat_id" private const val CATEGORY_ID = "cat_id"
private const val BACKGROUND = "bg"
class AppWidgetConfig(context: Context, val widgetId: Int) { class AppWidgetConfig(
context: Context,
cls: Class<out AppWidgetProvider>,
val widgetId: Int,
) {
private val prefs = context.getSharedPreferences("appwidget_$widgetId", Context.MODE_PRIVATE) private val prefs = context.getSharedPreferences("appwidget_${cls.simpleName}_$widgetId", Context.MODE_PRIVATE)
var categoryId: Long var categoryId: Long
get() = prefs.getLong(CATEGORY_ID, 0L) get() = prefs.getLong(CATEGORY_ID, 0L)
set(value) = prefs.edit { putLong(CATEGORY_ID, value) } set(value) = prefs.edit { putLong(CATEGORY_ID, value) }
var hasBackground: Boolean
get() = prefs.getBoolean(BACKGROUND, Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
set(value) = prefs.edit { putBoolean(BACKGROUND, value) }
fun clear() {
prefs.edit { clear() }
}
fun copyFrom(other: AppWidgetConfig) {
prefs.edit {
clear()
putLong(CATEGORY_ID, other.categoryId)
putBoolean(BACKGROUND, other.hasBackground)
}
}
} }

View File

@@ -3,8 +3,8 @@ package org.koitharu.kotatsu.core.prefs
enum class ReaderMode(val id: Int) { enum class ReaderMode(val id: Int) {
STANDARD(1), STANDARD(1),
WEBTOON(2), REVERSED(3),
REVERSED(3); WEBTOON(2);
companion object { companion object {

View File

@@ -0,0 +1,51 @@
package org.koitharu.kotatsu.core.ui
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
import androidx.annotation.CallSuper
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
abstract class BaseAppWidgetProvider : AppWidgetProvider() {
@CallSuper
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { id ->
val config = AppWidgetConfig(context, javaClass, id)
val views = onUpdateWidget(context, config)
appWidgetManager.updateAppWidget(id, views)
}
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
for (id in appWidgetIds) {
AppWidgetConfig(context, javaClass, id).clear()
}
}
override fun onRestored(context: Context, oldWidgetIds: IntArray, newWidgetIds: IntArray) {
super.onRestored(context, oldWidgetIds, newWidgetIds)
if (oldWidgetIds.size != newWidgetIds.size) {
return
}
for (i in oldWidgetIds.indices) {
val oldId = oldWidgetIds[i]
val newId = newWidgetIds[i]
val oldConfig = AppWidgetConfig(context, javaClass, oldId)
val newConfig = AppWidgetConfig(context, javaClass, newId)
newConfig.copyFrom(oldConfig)
oldConfig.clear()
}
}
protected abstract fun onUpdateWidget(
context: Context,
config: AppWidgetConfig,
): RemoteViews
}

View File

@@ -106,12 +106,7 @@ class TrimTransformation(
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true return this === other || (other is TrimTransformation && other.tolerance == tolerance)
if (javaClass != other?.javaClass) return false
other as TrimTransformation
return tolerance == other.tolerance
} }
override fun hashCode(): Int { override fun hashCode(): Int {

View File

@@ -20,38 +20,19 @@ sealed class DateTimeAgo {
override fun equals(other: Any?): Boolean = other === JustNow 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 { override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.minutes_ago, minutes, minutes) 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" 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 { override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.hours_ago, hours, hours) 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" override fun toString() = "hours_ago_$hours"
} }
@@ -75,26 +56,15 @@ sealed class DateTimeAgo {
override fun equals(other: Any?): Boolean = other === Yesterday 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 { override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.days_ago, days, days) 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" 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 { override fun format(resources: Resources): String {
return if (months == 0) { return if (months == 0) {
resources.getString(R.string.this_month) resources.getString(R.string.this_month)
@@ -102,19 +72,6 @@ sealed class DateTimeAgo {
resources.getQuantityString(R.plurals.months_ago, months, months) 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() { class Absolute(private val date: Date) : DateTimeAgo() {

View File

@@ -139,39 +139,14 @@ class ChipsView @JvmOverloads constructor(
} }
} }
class ChipModel( data class ChipModel(
@ColorRes val tint: Int, @ColorRes val tint: Int,
val title: CharSequence, val title: CharSequence,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
val isCheckable: Boolean, val isCheckable: Boolean,
val isChecked: Boolean, val isChecked: Boolean,
val data: Any? = null, 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 { fun interface OnChipClickListener {

View File

@@ -118,27 +118,10 @@ class SegmentedBarView @JvmOverloads constructor(
segmentsSizes.add(w) segmentsSizes.add(w)
} }
class Segment( data class Segment(
@FloatRange(from = 0.0, to = 1.0) val percent: Float, @FloatRange(from = 0.0, to = 1.0) val percent: Float,
@ColorInt val color: Int, @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() { private class OutlineProvider : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) { override fun getOutline(view: View, outline: Outline) {

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.core.util
import androidx.collection.ArrayMap
import kotlinx.coroutines.sync.Mutex
class CompositeMutex2<T : Any> : Set<T> {
private val delegates = ArrayMap<T, Mutex>()
override val size: Int
get() = delegates.size
override fun contains(element: T): Boolean {
return delegates.containsKey(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return elements.all { x -> delegates.containsKey(x) }
}
override fun isEmpty(): Boolean {
return delegates.isEmpty
}
override fun iterator(): Iterator<T> {
return delegates.keys.iterator()
}
suspend fun lock(element: T) {
val mutex = synchronized(delegates) {
delegates.getOrPut(element) {
Mutex()
}
}
mutex.lock()
}
fun unlock(element: T) {
synchronized(delegates) {
delegates.remove(element)?.unlock()
}
}
}

View File

@@ -12,7 +12,6 @@ import coil.request.SuccessResult
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -29,7 +28,6 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
.data(data) .data(data)
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.crossfade(context) .crossfade(context)
.addListener(CaptchaNotifier(context.applicationContext))
.target(this) .target(this)
} }

View File

@@ -4,7 +4,7 @@ import android.text.format.DateUtils
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
class ChapterListItem( data class ChapterListItem(
val chapter: MangaChapter, val chapter: MangaChapter,
val flags: Int, val flags: Int,
private val uploadDateMs: Long, 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 { companion object {
const val FLAG_UNREAD = 2 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.core.model.MangaHistory
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
class HistoryInfo( data class HistoryInfo(
val totalChapters: Int, val totalChapters: Int,
val currentChapter: Int, val currentChapter: Int,
val history: MangaHistory?, val history: MangaHistory?,
val isIncognitoMode: Boolean, val isIncognitoMode: Boolean,
) { ) {
val isValid: Boolean val isValid: Boolean
get() = totalChapters >= 0 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( 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.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
class MangaBranch( data class MangaBranch(
val name: String?, val name: String?,
val count: Int, val count: Int,
val isSelected: Boolean, 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 { override fun toString(): String {
return "$name: $count" return "$name: $count"
} }

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding import org.koitharu.kotatsu.databinding.ItemScrobblingInfoBinding
@@ -37,8 +36,4 @@ fun scrobblingInfoAD(
context.resources.getStringArray(R.array.scrobbling_statuses).getOrNull(it.ordinal) context.resources.getStringArray(R.array.scrobbling_statuses).getOrNull(it.ordinal)
} }
} }
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
} }

View File

@@ -8,7 +8,6 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -135,8 +134,4 @@ fun downloadItemAD(
} }
} }
} }
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
} }

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import java.util.Date import java.util.Date
import java.util.UUID import java.util.UUID
class DownloadItemModel( data class DownloadItemModel(
val id: UUID, val id: UUID,
val workState: WorkInfo.State, val workState: WorkInfo.State,
val isIndeterminate: Boolean, val isIndeterminate: Boolean,
@@ -64,38 +64,4 @@ class DownloadItemModel(
else -> super.getChangePayload(previousState) 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

@@ -37,6 +37,7 @@ import okio.IOException
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
@@ -277,7 +278,12 @@ class DownloadWorker @AssistedInject constructor(
publishState(currentState.copy(isPaused = false, error = null)) publishState(currentState.copy(isPaused = false, error = null))
} else { } else {
countDown-- countDown--
delay(DOWNLOAD_ERROR_DELAY) val retryDelay = if (e is TooManyRequestExceptions) {
e.retryAfter + DOWNLOAD_ERROR_DELAY
} else {
DOWNLOAD_ERROR_DELAY
}
delay(retryDelay)
} }
} }
} }

View File

@@ -12,7 +12,6 @@ import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -82,10 +81,6 @@ fun exploreRecommendationItemAD(
enqueueWith(coil) enqueueWith(coil)
} }
} }
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
} }
fun exploreSourceListItemAD( fun exploreSourceListItemAD(
@@ -113,10 +108,6 @@ fun exploreSourceListItemAD(
enqueueWith(coil) enqueueWith(coil)
} }
} }
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
} }
fun exploreSourceGridItemAD( fun exploreSourceGridItemAD(
@@ -144,8 +135,4 @@ fun exploreSourceGridItemAD(
enqueueWith(coil) enqueueWith(coil)
} }
} }
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
} }

View File

@@ -2,24 +2,11 @@ package org.koitharu.kotatsu.explore.ui.model
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
class ExploreButtons( data class ExploreButtons(
val isRandomLoading: Boolean, val isRandomLoading: Boolean,
) : ListModel { ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is ExploreButtons 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.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
class MangaSourceItem( data class MangaSourceItem(
val source: MangaSource, val source: MangaSource,
val isGrid: Boolean, val isGrid: Boolean,
) : ListModel { ) : ListModel {
@@ -11,20 +11,4 @@ class MangaSourceItem(
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaSourceItem && other.source == source 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( data class RecommendationsItem(
val manga: Manga val manga: Manga
) : ListModel { ) : ListModel {
val summary: String = manga.tags.joinToString { it.title } val summary: String = manga.tags.joinToString { it.title }
override fun areItemsTheSame(other: ListModel): Boolean { 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.model.MangaSource
import org.koitharu.kotatsu.parsers.util.find import org.koitharu.kotatsu.parsers.util.find
class Cover( data class Cover(
val url: String, val url: String,
val source: String, val source: String,
) { ) {
val mangaSource: MangaSource? val mangaSource: MangaSource?
get() = if (source.isEmpty()) null else MangaSource.entries.find(source) 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.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding 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.adapter.CategoriesAdapter
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@@ -77,6 +78,14 @@ class FavouriteCategoriesActivity :
} }
override fun onItemClick(item: FavouriteCategory, view: View) { 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)) { if (selectionController.onItemClick(item.id)) {
return return
} }
@@ -112,8 +121,8 @@ class FavouriteCategoriesActivity :
) )
} }
private fun onCategoriesChanged(categories: List<ListModel>) { private suspend fun onCategoriesChanged(categories: List<ListModel>) {
adapter.items = categories adapter.emit(categories)
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@@ -128,7 +137,14 @@ class FavouriteCategoriesActivity :
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: 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( override fun canDropOver(
recyclerView: RecyclerView, recyclerView: RecyclerView,
@@ -153,7 +169,8 @@ class FavouriteCategoriesActivity :
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState) 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 package org.koitharu.kotatsu.favourites.ui.categories
import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@@ -7,4 +8,6 @@ import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
interface FavouriteCategoriesListListener : OnListItemClickListener<FavouriteCategory> { interface FavouriteCategoriesListListener : OnListItemClickListener<FavouriteCategory> {
fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean
fun onEditClick(item: FavouriteCategory, view: View)
} }

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import java.util.Collections import org.koitharu.kotatsu.parsers.util.move
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -65,12 +65,11 @@ class FavouritesCategoriesViewModel @Inject constructor(
val prevJob = reorderJob val prevJob = reorderJob
reorderJob = launchJob(Dispatchers.Default) { reorderJob = launchJob(Dispatchers.Default) {
prevJob?.join() prevJob?.join()
val items = categories.requireValue() val snapshot = categories.requireValue().toMutableList()
val ids = items.mapNotNullTo(ArrayList(items.size)) { snapshot.move(oldPos, newPos)
val ids = snapshot.mapNotNullTo(ArrayList(snapshot.size)) {
(it as? CategoryListModel)?.category?.id (it as? CategoryListModel)?.category?.id
} }
Collections.swap(ids, oldPos, newPos)
ids.remove(0L)
repository.reorderCategories(ids) repository.reorderCategories(ids)
} }
} }

View File

@@ -18,8 +18,8 @@ class CategoriesAdapter(
) : BaseListAdapter<ListModel>() { ) : BaseListAdapter<ListModel>() {
init { init {
addDelegate(ListItemType.CATEGORY_LARGE ,categoryAD(coil, lifecycleOwner, onItemClickListener)) addDelegate(ListItemType.CATEGORY_LARGE, categoryAD(coil, lifecycleOwner, onItemClickListener))
addDelegate(ListItemType.STATE_EMPTY ,emptyStateListAD(coil, lifecycleOwner, listListener)) addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listListener))
addDelegate(ListItemType.STATE_LOADING ,loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
} }
} }

View File

@@ -16,7 +16,6 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
@@ -35,8 +34,13 @@ fun categoryAD(
{ inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemCategoryBinding.inflate(inflater, parent, false) },
) { ) {
val eventListener = object : OnClickListener, OnLongClickListener, OnTouchListener { val eventListener = object : OnClickListener, OnLongClickListener, OnTouchListener {
override fun onClick(v: View) = clickListener.onItemClick(item.category, itemView) override fun onClick(v: View) = if (v.id == R.id.imageView_edit) {
override fun onLongClick(v: View) = clickListener.onItemLongClick(item.category, itemView) 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 && override fun onTouch(v: View?, event: MotionEvent): Boolean = event.actionMasked == MotionEvent.ACTION_DOWN &&
clickListener.onDragHandleTouch(this@adapterDelegateViewBinding) clickListener.onDragHandleTouch(this@adapterDelegateViewBinding)
} }
@@ -58,6 +62,7 @@ fun categoryAD(
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt() val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
itemView.setOnClickListener(eventListener) itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener) itemView.setOnLongClickListener(eventListener)
binding.imageViewEdit.setOnClickListener(eventListener)
binding.imageViewHandle.setOnTouchListener(eventListener) binding.imageViewHandle.setOnTouchListener(eventListener)
bind { payloads -> bind { payloads ->
@@ -89,10 +94,4 @@ fun categoryAD(
} }
} }
} }
onViewRecycled {
coverViews.forEach {
it.disposeImageRequest()
}
}
} }

View File

@@ -46,4 +46,8 @@ class CategoryListModel(
result = 31 * result + category.isVisibleInLibrary.hashCode() result = 31 * result + category.isVisibleInLibrary.hashCode()
return result 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 import org.koitharu.kotatsu.list.ui.model.ListModel
class FavouriteTabModel( data class FavouriteTabModel(
val id: Long, val id: Long,
val title: String, val title: String,
) : ListModel { ) : ListModel {
@@ -10,20 +10,4 @@ class FavouriteTabModel(
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is FavouriteTabModel && other.id == id 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 return result
} }
private class TagsWrapper( private data class TagsWrapper(
val tags: Set<MangaTag>, val tags: Set<MangaTag>,
val isLoading: Boolean, val isLoading: Boolean,
val isError: 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> { private class TagTitleComparator(lc: String?) : Comparator<MangaTag> {

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
sealed interface FilterItem : ListModel { sealed interface FilterItem : ListModel {
class Sort( data class Sort(
val order: SortOrder, val order: SortOrder,
val isSelected: Boolean, val isSelected: Boolean,
) : FilterItem { ) : FilterItem {
@@ -24,25 +24,9 @@ sealed interface FilterItem : ListModel {
super.getChangePayload(previousState) 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 tag: MangaTag,
val isChecked: Boolean, val isChecked: Boolean,
) : FilterItem { ) : FilterItem {
@@ -58,43 +42,14 @@ sealed interface FilterItem : ListModel {
super.getChangePayload(previousState) 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, @StringRes val textResId: Int,
) : FilterItem { ) : FilterItem {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is Error && textResId == other.textResId 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.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
class FilterState( data class FilterState(
val sortOrder: SortOrder?, val sortOrder: SortOrder?,
val tags: Set<MangaTag>, 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

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@@ -27,8 +26,4 @@ fun emptyHintAD(
binding.textSecondary.setTextAndVisible(item.textSecondary) binding.textSecondary.setTextAndVisible(item.textSecondary)
binding.buttonRetry.setTextAndVisible(item.actionStringRes) binding.buttonRetry.setTextAndVisible(item.actionStringRes)
} }
onViewRecycled {
binding.icon.disposeImageRequest()
}
} }

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
@@ -31,8 +30,4 @@ fun emptyStateListAD(
binding.buttonRetry.setTextAndVisible(item.actionStringRes) binding.buttonRetry.setTextAndVisible(item.actionStringRes)
} }
} }
onViewRecycled {
binding.icon.disposeImageRequest()
}
} }

View File

@@ -8,12 +8,10 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel
@@ -54,11 +52,4 @@ fun mangaGridItemAD(
} }
badge = itemView.bindBadge(badge, item.counter) badge = itemView.bindBadge(badge, item.counter)
} }
onViewRecycled {
itemView.clearBadge(badge)
binding.progressView.percent = PROGRESS_NONE
badge = null
binding.imageViewCover.disposeImageRequest()
}
} }

View File

@@ -11,13 +11,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding import org.koitharu.kotatsu.databinding.ItemMangaListDetailsBinding
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
@@ -73,11 +71,4 @@ fun mangaListDetailedItemAD(
binding.ratingBar.rating = binding.ratingBar.numStars * item.manga.rating binding.ratingBar.rating = binding.ratingBar.numStars * item.manga.rating
badge = itemView.bindBadge(badge, item.counter) badge = itemView.bindBadge(badge, item.counter)
} }
onViewRecycled {
itemView.clearBadge(badge)
binding.progressView.percent = PROGRESS_NONE
badge = null
binding.imageViewCover.disposeImageRequest()
}
} }

View File

@@ -7,7 +7,6 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -48,10 +47,4 @@ fun mangaListItemAD(
} }
badge = itemView.bindBadge(badge, item.counter) badge = itemView.bindBadge(badge, item.counter)
} }
onViewRecycled {
itemView.clearBadge(badge)
badge = null
binding.imageViewCover.disposeImageRequest()
}
} }

View File

@@ -53,11 +53,11 @@ class LocalStorageManager @Inject constructor(
} }
suspend fun computeStorageSize() = withContext(Dispatchers.IO) { suspend fun computeStorageSize() = withContext(Dispatchers.IO) {
getAvailableStorageDirs().sumOf { it.computeSize() } getConfiguredStorageDirs().sumOf { it.computeSize() }
} }
suspend fun computeAvailableSize() = runInterruptible(Dispatchers.IO) { suspend fun computeAvailableSize() = runInterruptible(Dispatchers.IO) {
getAvailableStorageDirs().mapToSet { it.freeSpace }.sum() getConfiguredStorageDirs().mapToSet { it.freeSpace }.sum()
} }
suspend fun clearCache(cache: CacheDir) = runInterruptible(Dispatchers.IO) { suspend fun clearCache(cache: CacheDir) = runInterruptible(Dispatchers.IO) {

View File

@@ -77,7 +77,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
largeCoverUrl = null, largeCoverUrl = null,
description = null, description = null,
) )
LocalManga(root, manga) LocalManga(manga, root)
} }
override suspend fun getMangaInfo(): Manga? = runInterruptible(Dispatchers.IO) { 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) { override suspend fun getMangaInfo(): Manga? = runInterruptible(Dispatchers.IO) {

View File

@@ -6,13 +6,11 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import java.io.File import java.io.File
class LocalManga( data class LocalManga(
val file: File,
val manga: Manga, val manga: Manga,
val file: File = manga.url.toUri().toFile(),
) { ) {
constructor(manga: Manga) : this(manga.url.toUri().toFile(), manga)
var createdAt: Long = -1L var createdAt: Long = -1L
private set private set
get() { get() {
@@ -31,22 +29,6 @@ class LocalManga(
return manga.tags.containsAll(tags) 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 { override fun toString(): String {
return "LocalManga(${file.path}: ${manga.title})" return "LocalManga(${file.path}: ${manga.title})"
} }

View File

@@ -1,64 +1,65 @@
package org.koitharu.kotatsu.main.domain package org.koitharu.kotatsu.main.domain
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.lifecycle.coroutineScope import coil.intercept.Interceptor
import coil.EventListener
import coil.ImageLoader
import coil.network.HttpException import coil.network.HttpException
import coil.request.ErrorResult import coil.request.ErrorResult
import coil.request.ImageRequest import coil.request.ImageResult
import kotlinx.coroutines.launch
import org.jsoup.HttpStatusException import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Collections
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
class CoverRestorer @Inject constructor( class CoverRestoreInterceptor @Inject constructor(
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
private val repositoryFactory: MangaRepository.Factory, private val repositoryFactory: MangaRepository.Factory,
private val coilProvider: Provider<ImageLoader>, ) : Interceptor {
) : EventListener {
private val blacklist = ArraySet<String>() private val blacklist = Collections.synchronizedSet(ArraySet<String>())
override fun onError(request: ImageRequest, result: ErrorResult) { override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
super.onError(request, result) val request = chain.request
if (!result.throwable.shouldRestore()) { val result = chain.proceed(request)
return if (result is ErrorResult && result.throwable.shouldRestore()) {
} request.tags.tag<Manga>()?.let {
request.tags.tag<Manga>()?.let { if (restoreManga(it)) {
restoreManga(it, request) return chain.proceed(request.newBuilder().build())
} } else {
request.tags.tag<Bookmark>()?.let { return result
restoreBookmark(it, request) }
} }
} request.tags.tag<Bookmark>()?.let {
if (restoreBookmark(it)) {
private fun restoreManga(manga: Manga, request: ImageRequest) { return chain.proceed(request.newBuilder().build())
val key = manga.publicUrl } else {
if (key in blacklist) { return result
return }
}
request.lifecycle.coroutineScope.launch {
val restored = runCatchingCancellable {
restoreMangaImpl(manga)
}.getOrDefault(false)
if (restored) {
request.newBuilder().enqueueWith(coilProvider.get())
} else {
blacklist.add(key)
} }
} }
return result
}
private suspend fun restoreManga(manga: Manga): Boolean {
val key = manga.publicUrl
if (!blacklist.add(key)) {
return false
}
val restored = runCatchingCancellable {
restoreMangaImpl(manga)
}.getOrDefault(false)
if (restored) {
blacklist.remove(key)
}
return restored
} }
private suspend fun restoreMangaImpl(manga: Manga): Boolean { private suspend fun restoreMangaImpl(manga: Manga): Boolean {
@@ -75,21 +76,18 @@ class CoverRestorer @Inject constructor(
} }
} }
private fun restoreBookmark(bookmark: Bookmark, request: ImageRequest) { private suspend fun restoreBookmark(bookmark: Bookmark): Boolean {
val key = bookmark.imageUrl val key = bookmark.imageUrl
if (key in blacklist) { if (!blacklist.add(key)) {
return return false
} }
request.lifecycle.coroutineScope.launch { val restored = runCatchingCancellable {
val restored = runCatchingCancellable { restoreBookmarkImpl(bookmark)
restoreBookmarkImpl(bookmark) }.getOrDefault(false)
}.getOrDefault(false) if (restored) {
if (restored) { blacklist.remove(key)
request.newBuilder().enqueueWith(coilProvider.get())
} else {
blacklist.add(key)
}
} }
return restored
} }
private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean { private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean {

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.reader.domain
import android.graphics.ColorMatrix import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter import android.graphics.ColorMatrixColorFilter
class ReaderColorFilter( data class ReaderColorFilter(
val brightness: Float, val brightness: Float,
val contrast: Float, val contrast: Float,
val isInverted: Boolean, val isInverted: Boolean,
@@ -51,22 +51,4 @@ class ReaderColorFilter(
) )
set(matrix) 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

@@ -12,7 +12,7 @@ class WebtoonFrameLayout @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0, @AttrRes defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) { ) : FrameLayout(context, attrs, defStyleAttr) {
private val target by lazy(LazyThreadSafetyMode.NONE) { val target by lazy(LazyThreadSafetyMode.NONE) {
findViewById<WebtoonImageView>(R.id.ssiv) findViewById<WebtoonImageView>(R.id.ssiv)
} }
@@ -24,4 +24,4 @@ class WebtoonFrameLayout @JvmOverloads constructor(
target.scrollBy(dy) target.scrollBy(dy)
return target.getScroll() - oldScroll return target.getScroll() - oldScroll
} }
} }

View File

@@ -84,10 +84,22 @@ class WebtoonImageView @JvmOverloads constructor(
} }
} }
width = width.coerceAtLeast(suggestedMinimumWidth) width = width.coerceAtLeast(suggestedMinimumWidth)
height = height.coerceIn(suggestedMinimumHeight, parentHeight()) height = height.coerceAtLeast(suggestedMinimumHeight).coerceAtMost(parentHeight())
setMeasuredDimension(width, height) 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) { private fun scrollToInternal(pos: Int) {
scrollPos = pos scrollPos = pos
ct.set(sWidth / 2f, (height / 2f + pos.toFloat()) / minScale) 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.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View
import androidx.core.view.ViewCompat.TYPE_TOUCH import androidx.core.view.ViewCompat.TYPE_TOUCH
import androidx.core.view.forEach
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition import org.koitharu.kotatsu.core.util.ext.findCenterViewPosition
import java.util.LinkedList import java.util.LinkedList
import java.util.WeakHashMap
class WebtoonRecyclerView @JvmOverloads constructor( class WebtoonRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) { ) : RecyclerView(context, attrs, defStyleAttr) {
private var onPageScrollListeners: MutableList<OnPageScrollListener>? = null 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) override fun startNestedScroll(axes: Int) = startNestedScroll(axes, TYPE_TOUCH)
@@ -98,6 +112,15 @@ class WebtoonRecyclerView @JvmOverloads constructor(
listeners.forEach { it.dispatchScroll(this, dy, centerPosition) } 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 { abstract class OnPageScrollListener {
private var lastPosition = NO_POSITION private var lastPosition = NO_POSITION

View File

@@ -1,6 +1,6 @@
package org.koitharu.kotatsu.reader.ui.pager.webtoon package org.koitharu.kotatsu.reader.ui.pager.webtoon
import android.animation.ObjectAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.graphics.Matrix import android.graphics.Matrix
import android.graphics.Rect import android.graphics.Rect
@@ -15,7 +15,7 @@ import android.widget.OverScroller
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
private const val MAX_SCALE = 2.5f 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( class WebtoonScalingFrame @JvmOverloads constructor(
context: Context, context: Context,
@@ -23,7 +23,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
defStyles: Int = 0, defStyles: Int = 0,
) : FrameLayout(context, attrs, defStyles), ScaleGestureDetector.OnScaleGestureListener { ) : 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 scaleDetector = ScaleGestureDetector(context, this)
private val gestureDetector = GestureDetectorCompat(context, GestureListener()) private val gestureDetector = GestureDetectorCompat(context, GestureListener())
@@ -40,7 +40,6 @@ class WebtoonScalingFrame @JvmOverloads constructor(
private var halfHeight = 0f private var halfHeight = 0f
private val translateBounds = RectF() private val translateBounds = RectF()
private val targetHitRect = Rect() private val targetHitRect = Rect()
private var pendingScroll = 0
var isZoomEnable = true var isZoomEnable = true
set(value) { set(value) {
@@ -97,12 +96,11 @@ class WebtoonScalingFrame @JvmOverloads constructor(
if (newHeight != targetChild.height) { if (newHeight != targetChild.height) {
targetChild.layoutParams.height = newHeight targetChild.layoutParams.height = newHeight
targetChild.requestLayout() targetChild.requestLayout()
targetChild.relayoutChildren()
} }
if (scale < 1) { if (scale < 1) {
targetChild.getHitRect(targetHitRect) targetChild.getHitRect(targetHitRect)
targetChild.scrollBy(0, pendingScroll)
pendingScroll = 0
} }
} }
@@ -124,7 +122,6 @@ class WebtoonScalingFrame @JvmOverloads constructor(
else -> 0f else -> 0f
} }
pendingScroll = dy.toInt()
transformMatrix.postTranslate(dx, dy) transformMatrix.postTranslate(dx, dy)
syncMatrixValues() syncMatrixValues()
} }
@@ -159,9 +156,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
override fun onScaleEnd(p0: ScaleGestureDetector) { override fun onScaleEnd(p0: ScaleGestureDetector) = Unit
pendingScroll = 0
}
private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable { private inner class GestureListener : GestureDetector.SimpleOnGestureListener(), Runnable {
@@ -175,7 +170,7 @@ class WebtoonScalingFrame @JvmOverloads constructor(
override fun onDoubleTap(e: MotionEvent): Boolean { override fun onDoubleTap(e: MotionEvent): Boolean {
val newScale = if (scale != 1f) 1f else MAX_SCALE * 0.8f val newScale = if (scale != 1f) 1f else MAX_SCALE * 0.8f
ObjectAnimator.ofFloat(scale, newScale).run { ValueAnimator.ofFloat(scale, newScale).run {
interpolator = AccelerateDecelerateInterpolator() interpolator = AccelerateDecelerateInterpolator()
duration = 300 duration = 300
addUpdateListener { 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.list.ui.model.ListModel
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
class PageThumbnail( data class PageThumbnail(
val isCurrent: Boolean, val isCurrent: Boolean,
val repository: MangaRepository, val repository: MangaRepository,
val page: ReaderPage, val page: ReaderPage,
@@ -16,23 +16,4 @@ class PageThumbnail(
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is PageThumbnail && page == other.page 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

@@ -9,7 +9,6 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setTextColorAttr import org.koitharu.kotatsu.core.util.ext.setTextColorAttr
@@ -56,8 +55,4 @@ fun pageThumbnailAD(
text = (item.number).toString() text = (item.number).toString()
} }
} }
onViewRecycled {
binding.imageViewThumb.disposeImageRequest()
}
} }

View File

@@ -2,40 +2,17 @@ package org.koitharu.kotatsu.scrobbling.common.domain.model
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
class ScrobblerManga( data class ScrobblerManga(
val id: Long, val id: Long,
val name: String, val name: String,
val altName: String?, val altName: String?,
val cover: String, val cover: String,
val url: String, val url: String,
) : ListModel { ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is ScrobblerManga && other.id == id 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 { override fun toString(): String {
return "ScrobblerManga #$id \"$name\" $url" return "ScrobblerManga #$id \"$name\" $url"
} }

View File

@@ -1,29 +1,8 @@
package org.koitharu.kotatsu.scrobbling.common.domain.model package org.koitharu.kotatsu.scrobbling.common.domain.model
class ScrobblerUser( data class ScrobblerUser(
val id: Long, val id: Long,
val nickname: String, val nickname: String,
val avatar: String, val avatar: String,
val service: ScrobblerService, 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 import org.koitharu.kotatsu.list.ui.model.ListModel
class ScrobblingInfo( data class ScrobblingInfo(
val scrobbler: ScrobblerService, val scrobbler: ScrobblerService,
val mangaId: Long, val mangaId: Long,
val targetId: Long, val targetId: Long,
@@ -19,38 +19,4 @@ class ScrobblingInfo(
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is ScrobblingInfo && other.scrobbler == scrobbler 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

@@ -6,7 +6,6 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemScrobblingMangaBinding import org.koitharu.kotatsu.databinding.ItemScrobblingMangaBinding
@@ -34,8 +33,4 @@ fun scrobblingMangaAD(
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
binding.ratingBar.rating = item.rating * binding.ratingBar.numStars binding.ratingBar.rating = item.rating * binding.ratingBar.numStars
} }
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
} }

View File

@@ -5,7 +5,6 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
@@ -35,8 +34,4 @@ fun scrobblingMangaAD(
enqueueWith(coil) enqueueWith(coil)
} }
} }
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
} }

View File

@@ -4,7 +4,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
class ScrobblerHint( data class ScrobblerHint(
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
@StringRes val textPrimary: Int, @StringRes val textPrimary: Int,
@StringRes val textSecondary: Int, @StringRes val textSecondary: Int,
@@ -15,26 +15,4 @@ class ScrobblerHint(
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is ScrobblerHint && other.textPrimary == textPrimary 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.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
class MultiSearchListModel( data class MultiSearchListModel(
val source: MangaSource, val source: MangaSource,
val hasMore: Boolean, val hasMore: Boolean,
val list: List<MangaItemModel>, val list: List<MangaItemModel>,
@@ -23,24 +23,4 @@ class MultiSearchListModel(
super.getChangePayload(previousState) 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

@@ -7,7 +7,6 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.favicon.faviconUri import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -51,8 +50,4 @@ fun searchSuggestionSourceAD(
enqueueWith(coil) enqueueWith(coil)
} }
} }
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
} }

View File

@@ -11,7 +11,6 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -66,10 +65,6 @@ private fun searchSuggestionMangaGridAD(
} }
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
} }
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
} }
private class SuggestionMangaDiffCallback : DiffUtil.ItemCallback<Manga>() { private class SuggestionMangaDiffCallback : DiffUtil.ItemCallback<Manga>() {

View File

@@ -20,7 +20,6 @@ import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.FaviconDrawable import org.koitharu.kotatsu.core.ui.image.FaviconDrawable
import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener import org.koitharu.kotatsu.core.ui.list.OnTipCloseListener
import org.koitharu.kotatsu.core.util.ext.crossfade import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -94,10 +93,6 @@ fun sourceConfigItemCheckableDelegate(
enqueueWith(coil) enqueueWith(coil)
} }
} }
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
} }
fun sourceConfigItemDelegate2( fun sourceConfigItemDelegate2(
@@ -143,10 +138,6 @@ fun sourceConfigItemDelegate2(
enqueueWith(coil) enqueueWith(coil)
} }
} }
onViewRecycled {
binding.imageViewIcon.disposeImageRequest()
}
} }
fun sourceConfigTipDelegate( fun sourceConfigTipDelegate(

View File

@@ -5,14 +5,13 @@ import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import java.io.File import java.io.File
class DirectoryModel( data class DirectoryModel(
val title: String?, val title: String?,
@StringRes val titleRes: Int, @StringRes val titleRes: Int,
val file: File?, val file: File?,
val isChecked: Boolean, val isChecked: Boolean,
val isAvailable: Boolean, val isAvailable: Boolean,
) : ListModel { ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is DirectoryModel && other.file == file && other.title == title && other.titleRes == titleRes return other is DirectoryModel && other.file == file && other.title == title && other.titleRes == titleRes
} }
@@ -24,26 +23,4 @@ class DirectoryModel(
super.getChangePayload(previousState) 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 package org.koitharu.kotatsu.settings.userdata
class StorageUsage( data class StorageUsage(
val savedManga: Item, val savedManga: Item,
val pagesCache: Item, val pagesCache: Item,
val otherCache: Item, val otherCache: Item,
val available: Item, val available: Item,
) { ) {
data class Item(
class Item(
val bytes: Long, val bytes: Long,
val percent: Float, 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 android.widget.HorizontalScrollView
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.customview.view.AbsSavedState import androidx.customview.view.AbsSavedState
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
@@ -52,7 +53,11 @@ class ThemeChooserPreference @JvmOverloads constructor(
binding.linear.removeAllViews() binding.linear.removeAllViews()
for (theme in entries) { for (theme in entries) {
val context = ContextThemeWrapper(context, theme.styleResId) 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 val isSelected = theme == currentValue
item.card.isChecked = isSelected item.card.isChecked = isSelected
item.card.strokeWidth = if (isSelected) context.resources.getDimensionPixelSize( item.card.strokeWidth = if (isSelected) context.resources.getDimensionPixelSize(
@@ -76,7 +81,8 @@ class ThemeChooserPreference @JvmOverloads constructor(
} }
binding.scrollView.viewTreeObserver.run { binding.scrollView.viewTreeObserver.run {
scrollPersistListener?.let { removeOnScrollChangedListener(it) } scrollPersistListener?.let { removeOnScrollChangedListener(it) }
scrollPersistListener = ScrollPersistListener(WeakReference(binding.scrollView), lastScrollPosition) scrollPersistListener =
ScrollPersistListener(WeakReference(binding.scrollView), lastScrollPosition)
addOnScrollChangedListener(scrollPersistListener) addOnScrollChangedListener(scrollPersistListener)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -133,7 +139,7 @@ class ThemeChooserPreference @JvmOverloads constructor(
constructor( constructor(
superState: Parcelable, superState: Parcelable,
scrollPosition: Int scrollPosition: Int,
) : super(superState) { ) : super(superState) {
this.scrollPosition = scrollPosition this.scrollPosition = scrollPosition
} }
@@ -151,7 +157,8 @@ class ThemeChooserPreference @JvmOverloads constructor(
@Suppress("unused") @Suppress("unused")
@JvmField @JvmField
val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> { 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) override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
} }

View File

@@ -1,29 +1,8 @@
package org.koitharu.kotatsu.sync.domain package org.koitharu.kotatsu.sync.domain
class SyncAuthResult( data class SyncAuthResult(
val host: String, val host: String,
val email: String, val email: String,
val password: String, val password: String,
val token: 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

@@ -3,31 +3,12 @@ package org.koitharu.kotatsu.tracker.domain.model
import java.util.* import java.util.*
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
class MangaTracking( data class MangaTracking(
val manga: Manga, val manga: Manga,
val lastChapterId: Long, val lastChapterId: Long,
val lastCheck: Date?, val lastCheck: Date?,
) { ) {
fun isEmpty(): Boolean { fun isEmpty(): Boolean {
return lastChapterId == 0L 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

@@ -5,7 +5,6 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.isBold import org.koitharu.kotatsu.core.util.ext.isBold
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -44,8 +43,4 @@ fun feedItemAD(
item.count, item.count,
) )
} }
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
} }

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.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
class FeedItem( data class FeedItem(
val id: Long, val id: Long,
val imageUrl: String, val imageUrl: String,
val title: String, val title: String,
@@ -11,32 +11,7 @@ class FeedItem(
val count: Int, val count: Int,
val isNew: Boolean, val isNew: Boolean,
) : ListModel { ) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is FeedItem && other.id == id 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

@@ -2,28 +2,7 @@ package org.koitharu.kotatsu.tracker.work
import org.koitharu.kotatsu.tracker.domain.model.MangaTracking import org.koitharu.kotatsu.tracker.domain.model.MangaTracking
class TrackingItem( data class TrackingItem(
val tracking: MangaTracking, val tracking: MangaTracking,
val channelId: String?, 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

@@ -55,6 +55,7 @@ class RecentListFactory(
.data(item.coverUrl) .data(item.coverUrl)
.size(coverSize) .size(coverSize)
.tag(item.source) .tag(item.source)
.tag(item)
.transformations(transformation) .transformations(transformation)
.build(), .build(),
).getDrawableOrThrow().toBitmap() ).getDrawableOrThrow().toBitmap()

View File

@@ -0,0 +1,74 @@
package org.koitharu.kotatsu.widget.recent
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityAppwidgetRecentBinding
import com.google.android.material.R as materialR
@AndroidEntryPoint
class RecentWidgetConfigActivity :
BaseActivity<ActivityAppwidgetRecentBinding>(),
View.OnClickListener {
private lateinit var config: AppWidgetConfig
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityAppwidgetRecentBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
viewBinding.buttonDone.setOnClickListener(this)
val appWidgetId = intent?.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID,
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finishAfterTransition()
return
}
config = AppWidgetConfig(this, RecentWidgetProvider::class.java, appWidgetId)
viewBinding.switchBackground.isChecked = config.hasBackground
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_done -> {
config.hasBackground = viewBinding.switchBackground.isChecked
updateWidget()
setResult(
Activity.RESULT_OK,
Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId),
)
finish()
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
top = insets.top,
)
}
private fun updateWidget() {
val intent = Intent(this, RecentWidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = intArrayOf(config.widgetId)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
}
}

View File

@@ -2,43 +2,52 @@ package org.koitharu.kotatsu.widget.recent
import android.app.PendingIntent import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.core.ui.BaseAppWidgetProvider
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
class RecentWidgetProvider : AppWidgetProvider() { class RecentWidgetProvider : BaseAppWidgetProvider() {
override fun onUpdate( override fun onUpdate(
context: Context, context: Context,
appWidgetManager: AppWidgetManager, appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray appWidgetIds: IntArray
) { ) {
appWidgetIds.forEach { id -> super.onUpdate(context, appWidgetManager, appWidgetIds)
val views = RemoteViews(context.packageName, R.layout.widget_recent)
val adapter = Intent(context, RecentWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.stackView, adapter)
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_MANGA_READ
views.setPendingIntentTemplate(
R.id.stackView,
PendingIntentCompat.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
true,
),
)
views.setEmptyView(R.id.stackView, R.id.textView_holder)
appWidgetManager.updateAppWidget(id, views)
}
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stackView) appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stackView)
} }
override fun onUpdateWidget(context: Context, config: AppWidgetConfig): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.widget_recent)
if (!config.hasBackground) {
views.setInt(R.id.widget_root, "setBackgroundColor", Color.TRANSPARENT)
} else {
views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root)
}
val adapter = Intent(context, RecentWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.stackView, adapter)
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_MANGA_READ
views.setPendingIntentTemplate(
R.id.stackView,
PendingIntentCompat.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
true,
),
)
views.setEmptyView(R.id.stackView, R.id.textView_holder)
return views
}
} }

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaIntent import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -27,7 +28,7 @@ class ShelfListFactory(
) : RemoteViewsService.RemoteViewsFactory { ) : RemoteViewsService.RemoteViewsFactory {
private val dataSet = ArrayList<Manga>() private val dataSet = ArrayList<Manga>()
private val config = AppWidgetConfig(context, widgetId) private val config = AppWidgetConfig(context, ShelfWidgetProvider::class.java, widgetId)
private val transformation = RoundedCornersTransformation( private val transformation = RoundedCornersTransformation(
context.resources.getDimension(R.dimen.appwidget_corner_radius_inner), context.resources.getDimension(R.dimen.appwidget_corner_radius_inner),
) )
@@ -66,7 +67,8 @@ class ShelfListFactory(
.data(item.coverUrl) .data(item.coverUrl)
.size(coverSize) .size(coverSize)
.tag(item.source) .tag(item.source)
.transformations(transformation) .tag(item)
.transformations(transformation, TrimTransformation())
.build(), .build(),
).getDrawableOrThrow().toBitmap() ).getDrawableOrThrow().toBitmap()
}.onSuccess { cover -> }.onSuccess { cover ->

View File

@@ -8,7 +8,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -19,14 +18,14 @@ import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivityCategoriesBinding import org.koitharu.kotatsu.databinding.ActivityAppwidgetShelfBinding
import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter import org.koitharu.kotatsu.widget.shelf.adapter.CategorySelectAdapter
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@AndroidEntryPoint @AndroidEntryPoint
class ShelfConfigActivity : class ShelfWidgetConfigActivity :
BaseActivity<ActivityCategoriesBinding>(), BaseActivity<ActivityAppwidgetShelfBinding>(),
OnListItemClickListener<CategoryItem>, OnListItemClickListener<CategoryItem>,
View.OnClickListener { View.OnClickListener {
@@ -37,16 +36,14 @@ class ShelfConfigActivity :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityCategoriesBinding.inflate(layoutInflater)) setContentView(ActivityAppwidgetShelfBinding.inflate(layoutInflater))
supportActionBar?.run { supportActionBar?.run {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
} }
adapter = CategorySelectAdapter(this) adapter = CategorySelectAdapter(this)
viewBinding.recyclerView.adapter = adapter viewBinding.recyclerView.adapter = adapter
viewBinding.buttonDone.isVisible = true
viewBinding.buttonDone.setOnClickListener(this) viewBinding.buttonDone.setOnClickListener(this)
viewBinding.fabAdd.hide()
val appWidgetId = intent?.getIntExtra( val appWidgetId = intent?.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID,
@@ -55,8 +52,9 @@ class ShelfConfigActivity :
finishAfterTransition() finishAfterTransition()
return return
} }
config = AppWidgetConfig(this, appWidgetId) config = AppWidgetConfig(this, ShelfWidgetProvider::class.java, appWidgetId)
viewModel.checkedId = config.categoryId viewModel.checkedId = config.categoryId
viewBinding.switchBackground.isChecked = config.hasBackground
viewModel.content.observe(this, this::onContentChanged) viewModel.content.observe(this, this::onContentChanged)
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null)) viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
@@ -66,6 +64,7 @@ class ShelfConfigActivity :
when (v.id) { when (v.id) {
R.id.button_done -> { R.id.button_done -> {
config.categoryId = viewModel.checkedId config.categoryId = viewModel.checkedId
config.hasBackground = viewBinding.switchBackground.isChecked
updateWidget() updateWidget()
setResult( setResult(
Activity.RESULT_OK, Activity.RESULT_OK,
@@ -81,11 +80,6 @@ class ShelfConfigActivity :
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.fabAdd.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = topMargin + insets.right
leftMargin = topMargin + insets.left
bottomMargin = topMargin + insets.bottom
}
viewBinding.recyclerView.updatePadding( viewBinding.recyclerView.updatePadding(
left = insets.left, left = insets.left,
right = insets.right, right = insets.right,

View File

@@ -2,43 +2,52 @@ package org.koitharu.kotatsu.widget.shelf
import android.app.PendingIntent import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.core.app.PendingIntentCompat import androidx.core.app.PendingIntentCompat
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppWidgetConfig
import org.koitharu.kotatsu.core.ui.BaseAppWidgetProvider
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
class ShelfWidgetProvider : AppWidgetProvider() { class ShelfWidgetProvider : BaseAppWidgetProvider() {
override fun onUpdate( override fun onUpdate(
context: Context, context: Context,
appWidgetManager: AppWidgetManager, appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray appWidgetIds: IntArray
) { ) {
appWidgetIds.forEach { id -> super.onUpdate(context, appWidgetManager, appWidgetIds)
val views = RemoteViews(context.packageName, R.layout.widget_shelf)
val adapter = Intent(context, ShelfWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, id)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.gridView, adapter)
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_MANGA_READ
views.setPendingIntentTemplate(
R.id.gridView,
PendingIntentCompat.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
true,
),
)
views.setEmptyView(R.id.gridView, R.id.textView_holder)
appWidgetManager.updateAppWidget(id, views)
}
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.gridView) appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.gridView)
} }
override fun onUpdateWidget(context: Context, config: AppWidgetConfig): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.widget_shelf)
if (!config.hasBackground) {
views.setInt(R.id.widget_root, "setBackgroundColor", Color.TRANSPARENT)
} else {
views.setInt(R.id.widget_root, "setBackgroundResource", R.drawable.bg_appwidget_root)
}
val adapter = Intent(context, ShelfWidgetService::class.java)
adapter.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, config.widgetId)
adapter.data = Uri.parse(adapter.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.gridView, adapter)
val intent = Intent(context, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_MANGA_READ
views.setPendingIntentTemplate(
R.id.gridView,
PendingIntentCompat.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT,
true,
),
)
views.setEmptyView(R.id.gridView, R.id.textView_holder)
return views
}
} }

View File

@@ -4,4 +4,4 @@
android:shape="rectangle"> android:shape="rectangle">
<corners android:radius="@dimen/appwidget_corner_radius_inner" /> <corners android:radius="@dimen/appwidget_corner_radius_inner" />
<solid android:color="?android:panelColorBackground" /> <solid android:color="?android:panelColorBackground" />
</shape> </shape>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="@dimen/appwidget_corner_radius_background" />
<solid android:color="?android:colorBackground" />
</shape>

View File

@@ -8,7 +8,7 @@
<item <item
android:id="@+id/checked" android:id="@+id/checked"
android:drawable="@drawable/ic_heart_outline" android:drawable="@drawable/ic_heart"
android:state_checked="true" /> android:state_checked="true" />
<transition <transition

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
tools:title="@string/recent_manga">
<Button
android:id="@+id/button_done"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:text="@string/done" />
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:text="@string/background"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_scrollFlags="scroll" />
</LinearLayout>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
tools:title="@string/manga_shelf">
<Button
android:id="@+id/button_done"
style="@style/Widget.Material3.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:text="@string/done" />
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checked="true"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:text="@string/background"
android:textAppearance="?textAppearanceBodyMedium"
app:layout_scrollFlags="scroll" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingTop="6dp"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:singleLine="true"
android:text="@string/favourites_categories"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingVertical="@dimen/list_spacing_normal"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_checkable_single" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -72,7 +72,7 @@
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge" android:textAppearance="?attr/textAppearanceBodyLarge"
app:layout_constraintBottom_toTopOf="@id/textView_subtitle" 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_constraintStart_toEndOf="@id/imageView_cover3"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
@@ -120,10 +120,23 @@
app:layout_constraintTop_toTopOf="@id/textView_subtitle" app:layout_constraintTop_toTopOf="@id/textView_subtitle"
app:srcCompat="@drawable/ic_eye" /> 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 <ImageView
android:id="@+id/imageView_handle" android:id="@+id/imageView_handle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/reorder" android:contentDescription="@string/reorder"
android:padding="@dimen/margin_normal" android:padding="@dimen/margin_normal"
android:src="@drawable/ic_reorder_handle" android:src="@drawable/ic_reorder_handle"

View File

@@ -9,10 +9,11 @@
android:id="@+id/ssiv" android:id="@+id/ssiv"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="1dp"
app:panEnabled="false" app:panEnabled="false"
app:quickScaleEnabled="false" app:quickScaleEnabled="false"
app:zoomEnabled="false" /> app:zoomEnabled="false" />
<include layout="@layout/layout_page_info" /> <include layout="@layout/layout_page_info" />
</org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonFrameLayout> </org.koitharu.kotatsu.reader.ui.pager.webtoon.WebtoonFrameLayout>

View File

@@ -5,7 +5,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="4dp" android:padding="4dp"
android:theme="@style/Theme.Kotatsu.AppWidgetContainer"> android:theme="@style/Theme.Kotatsu.AppWidgetContainer"
tools:layout_width="116dp">
<LinearLayout <LinearLayout
android:id="@+id/rootLayout" android:id="@+id/rootLayout"
@@ -13,14 +14,15 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:background="@drawable/bg_appwidget_card" android:background="@drawable/bg_appwidget_card"
android:clipToOutline="true"
android:foreground="?android:selectableItemBackground" android:foreground="?android:selectableItemBackground"
android:orientation="vertical" android:orientation="vertical"
android:padding="4dp" android:outlineProvider="background"
tools:ignore="UnusedAttribute,UselessParent"> tools:ignore="UnusedAttribute,UselessParent">
<ImageView <ImageView
android:id="@+id/imageView_cover" android:id="@+id/imageView_cover"
android:layout_width="@dimen/widget_cover_width" android:layout_width="match_parent"
android:layout_height="@dimen/widget_cover_height" android:layout_height="@dimen/widget_cover_height"
android:scaleType="centerCrop" android:scaleType="centerCrop"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
@@ -33,8 +35,11 @@
android:elegantTextHeight="false" android:elegantTextHeight="false"
android:ellipsize="end" android:ellipsize="end"
android:lines="2" android:lines="2"
android:padding="2dp" android:paddingHorizontal="6dp"
android:paddingTop="2dp"
android:paddingBottom="4dp"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="12sp"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />
</LinearLayout> </LinearLayout>

View File

@@ -8,70 +8,53 @@
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall" android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="horizontal" android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart" android:paddingVertical="8dp"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
tools:ignore="PrivateResource"> tools:ignore="PrivateResource">
<include layout="@layout/image_frame" /> <TextView
android:id="@android:id/title"
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:ellipsize="marquee"
android:layout_marginBottom="8dp" android:labelFor="@id/scrollView"
android:clipChildren="false" 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:clipToPadding="false"
android:orientation="vertical"> android:descendantFocusability="blocksDescendants"
android:scrollbars="none">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:id="@+id/linear"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:baselineAligned="true" android:orientation="horizontal"
android:baselineAlignedChildIndex="0" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:orientation="horizontal"> android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" />
<TextView </HorizontalScrollView>
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" />
<TextView <TextView
android:id="@android:id/summary" android:id="@android:id/summary"
style="@style/PreferenceSummaryTextStyle" style="@style/PreferenceSummaryTextStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:layout_marginTop="2dp"
android:textAlignment="viewStart" android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:textColor="?android:attr/textColorSecondary" /> 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> </LinearLayout>

View File

@@ -1,14 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?android:attr/colorBackground" android:clipToOutline="true"
android:outlineProvider="background"
android:padding="4dp" android:padding="4dp"
android:theme="@style/Theme.Kotatsu.AppWidgetContainer"> android:theme="@style/Theme.Kotatsu.AppWidgetContainer"
tools:background="@drawable/bg_appwidget_root">
<StackView <StackView
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/stackView" android:id="@+id/stackView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -23,4 +25,4 @@
android:text="@string/history_is_empty" android:text="@string/history_is_empty"
android:textColor="?android:textColorPrimary" /> android:textColor="?android:textColorPrimary" />
</FrameLayout> </FrameLayout>

View File

@@ -1,21 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?android:attr/colorBackground" android:clipToOutline="true"
android:padding="4dp" android:outlineProvider="background"
android:theme="@style/Theme.Kotatsu.AppWidgetContainer"> android:theme="@style/Theme.Kotatsu.AppWidgetContainer"
tools:background="?android:attr/colorBackground">
<GridView <GridView
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/gridView" android:id="@+id/gridView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false"
android:columnWidth="92dp" android:columnWidth="92dp"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:numColumns="auto_fit" android:numColumns="auto_fit"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="4dp"
android:scrollbarStyle="insideOverlay"
tools:listitem="@layout/item_shelf" /> tools:listitem="@layout/item_shelf" />
<TextView <TextView
@@ -27,4 +32,4 @@
android:text="@string/you_have_not_favourites_yet" android:text="@string/you_have_not_favourites_yet"
android:textColor="?android:attr/textColorPrimary" /> android:textColor="?android:attr/textColorPrimary" />
</FrameLayout> </FrameLayout>

View File

@@ -469,4 +469,7 @@
<string name="disable_nsfw">Desactivar NSFW</string> <string name="disable_nsfw">Desactivar NSFW</string>
<string name="too_many_requests_message">Demasiadas solicitudes. Vuelve a intentarlo más tarde</string> <string name="too_many_requests_message">Demasiadas solicitudes. Vuelve a intentarlo más tarde</string>
<string name="related_manga_summary">Muestra una lista de mangas relacionados. En algunos casos puede ser inexacta o faltar</string> <string name="related_manga_summary">Muestra una lista de mangas relacionados. En algunos casos puede ser inexacta o faltar</string>
<string name="advanced">Avanzado</string>
<string name="default_section">Sección predeterminada</string>
<string name="manga_list">Lista de mangas</string>
</resources> </resources>

View File

@@ -7,29 +7,46 @@
<item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item> <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="colorPrimaryContainer">@color/m3_sys_color_dynamic_dark_primary_container</item>
<item name="colorOnPrimary">@color/m3_sys_color_dynamic_dark_on_primary</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="colorPrimaryInverse">@color/m3_sys_color_dynamic_dark_inverse_primary</item>
<item name="colorSecondary">@color/m3_sys_color_dynamic_dark_secondary</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="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="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="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="colorSurface">@color/m3_sys_color_dynamic_dark_surface</item>
<item name="colorSurfaceDim">@color/m3_sys_color_dynamic_dark_surface_dim</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="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="colorSurfaceContainerLowest">
<item name="colorSurfaceContainerLow">@color/m3_sys_color_dynamic_dark_surface_container_low</item> @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="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="colorSurfaceContainerHigh">
<item name="colorSurfaceContainerHighest">@color/m3_sys_color_dynamic_dark_surface_container_highest</item> @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="colorSurfaceVariant">@color/m3_sys_color_dynamic_dark_surface_variant</item>
<item name="colorOnSurface">@color/m3_sys_color_dynamic_dark_on_surface</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="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="colorOnBackground">@color/m3_sys_color_dynamic_dark_on_background</item>
<item name="colorError">@color/m3_sys_color_dark_error</item> <item name="colorError">@color/m3_sys_color_dark_error</item>
<item name="colorErrorContainer">@color/m3_sys_color_dark_error_container</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="m3ColorExploreButton">@android:color/system_neutral2_800</item>
<item name="m3ColorBottomMenuBackground">@android:color/system_neutral2_800</item> <item name="m3ColorBottomMenuBackground">@android:color/system_neutral2_800</item>
<!-- Default Framework Text Colors. --> <!-- Default Framework Text Colors. -->
<item name="android:textColorPrimary">@color/m3_dynamic_dark_default_color_primary_text</item> <item name="android:textColorPrimary">@color/m3_dynamic_dark_default_color_primary_text
<item name="android:textColorPrimaryInverse">@color/m3_dynamic_default_color_primary_text</item> </item>
<item name="android:textColorSecondary">@color/m3_dynamic_dark_default_color_secondary_text</item> <item name="android:textColorPrimaryInverse">@color/m3_dynamic_default_color_primary_text
<item name="android:textColorSecondaryInverse">@color/m3_dynamic_default_color_secondary_text</item> </item>
<item name="android:textColorTertiary">@color/m3_dynamic_dark_default_color_secondary_text</item> <item name="android:textColorSecondary">
<item name="android:textColorTertiaryInverse">@color/m3_dynamic_default_color_secondary_text</item> @color/m3_dynamic_dark_default_color_secondary_text
<item name="android:textColorPrimaryDisableOnly">@color/m3_dynamic_dark_primary_text_disable_only</item> </item>
<item name="android:textColorPrimaryInverseDisableOnly">@color/m3_dynamic_primary_text_disable_only</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:textColorHint">@color/m3_dynamic_dark_hint_foreground</item>
<item name="android:textColorHintInverse">@color/m3_dynamic_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:textColorHighlight">@color/m3_dynamic_dark_highlighted_text</item>
<item name="android:textColorHighlightInverse">@color/m3_dynamic_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 --> <!-- Fixes -->
<item name="bottomNavigationStyle">@style/Widget.Kotatsu.BottomNavigationView</item> <item name="bottomNavigationStyle">@style/Widget.Kotatsu.BottomNavigationView</item>
</style> </style>
<style name="Theme.Kotatsu.AppWidgetContainer" parent="@android:style/Theme.DeviceDefault.DayNight"> <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:colorBackground">@color/m3_ref_palette_dynamic_secondary20</item>
<item name="android:panelColorBackground">@color/m3_sys_color_dynamic_dark_inverse_primary</item> <item name="android:panelColorBackground">@color/m3_ref_palette_dynamic_secondary40</item>
<item name="colorTertiary">@color/m3_ref_palette_dynamic_secondary40</item>
</style> </style>
</resources> </resources>

View File

@@ -1,33 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<plurals name="new_chapters"> <plurals name="new_chapters">
<item quantity="one">%1$d nowy rozdział</item> <item quantity="one">%1$d nowy rozdział</item>
<item quantity="few">%1$d nowe rozdziały</item> <item quantity="few">%1$d nowe rozdziały</item>
<item quantity="many">%1$d nowych rozdziałów</item> <item quantity="many">%1$d nowych rozdziałów</item>
</plurals> </plurals>
<plurals name="minutes_ago"> <plurals name="minutes_ago">
<item quantity="one">%1$d minutę temu</item> <item quantity="one">%1$d minutę temu</item>
<item quantity="few">%1$d minuty temu</item> <item quantity="few">%1$d minuty temu</item>
<item quantity="many">%1$d minut temu</item> <item quantity="many">%1$d minut temu</item>
</plurals> </plurals>
<plurals name="hours_ago"> <plurals name="hours_ago">
<item quantity="one">%1$d godzinę temu</item> <item quantity="one">%1$d godzinę temu</item>
<item quantity="few">%1$d godziny temu</item> <item quantity="few">%1$d godziny temu</item>
<item quantity="many">%1$d godzin temu</item> <item quantity="many">%1$d godzin temu</item>
</plurals> </plurals>
<plurals name="days_ago"> <plurals name="days_ago">
<item quantity="one">%1$d dzień temu</item> <item quantity="one">%1$d dzień temu</item>
<item quantity="few">%1$d dni temu</item> <item quantity="few">%1$d dni temu</item>
<item quantity="many">%1$d dni temu</item> <item quantity="many">%1$d dni temu</item>
</plurals> </plurals>
<plurals name="items"> <plurals name="items">
<item quantity="one">%1$d przedmiot</item> <item quantity="one">%1$d przedmiot</item>
<item quantity="few">%1$d przedmioty</item> <item quantity="few">%1$d przedmioty</item>
<item quantity="many">%1$d przedmiotów</item> <item quantity="many">%1$d przedmiotów</item>
</plurals> </plurals>
<plurals name="chapters"> <plurals name="chapters">
<item quantity="one">%1$d rozdział</item> <item quantity="one">%1$d rozdział</item>
<item quantity="few">%1$d rozdziały</item> <item quantity="few">%1$d rozdziały</item>
<item quantity="many">%1$d rozdziałów</item> <item quantity="many">%1$d rozdziałów</item>
</plurals> </plurals>
</resources> <plurals name="months_ago">
<item quantity="one">%1$d miesiąc temu</item>
<item quantity="few">%1$d miesiące temu</item>
<item quantity="many">%1$d miesięcy temu</item>
<item quantity="other">%1$d miesięcy temu</item>
</plurals>
</resources>

View File

@@ -443,4 +443,17 @@
<string name="pick_custom_directory">Wybierz katalog niestandardowy</string> <string name="pick_custom_directory">Wybierz katalog niestandardowy</string>
<string name="no_access_to_file">Nie masz dostępu do tego pliku lub katalogu</string> <string name="no_access_to_file">Nie masz dostępu do tego pliku lub katalogu</string>
<string name="local_manga_directories">Lokalne katalogi mangi</string> <string name="local_manga_directories">Lokalne katalogi mangi</string>
<string name="advanced">Zaawansowane</string>
<string name="this_month">Ten miesiąc</string>
<string name="voice_search">Wyszukiwanie głosowe</string>
<string name="color_light">Jasny</string>
<string name="color_dark">Ciemny</string>
<string name="color_white">Biały</string>
<string name="color_black">Czarny</string>
<string name="background">Tło</string>
<string name="progress">Postęp</string>
<string name="order_added">Dodano</string>
<string name="description">Opis</string>
<string name="languages">Języki</string>
<string name="unknown">Nieznane</string>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show More