Merge branch 'devel' into feature/kitsu
# Conflicts: # app/src/main/res/values/strings.xml
This commit is contained in:
@@ -15,8 +15,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 546
|
||||
versionName '5.1.2'
|
||||
versionCode 547
|
||||
versionName '5.1.3'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -79,7 +79,7 @@ afterEvaluate {
|
||||
}
|
||||
dependencies {
|
||||
//noinspection GradleDependency
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:ebcc6391d6') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:44e28b40d3') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.7.1'
|
||||
implementation 'androidx.activity:activity-ktx:1.7.2'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.7'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||
@@ -97,7 +97,7 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
@@ -106,7 +106,7 @@ dependencies {
|
||||
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
//noinspection GradleDependency
|
||||
implementation('com.google.guava:guava:31.1-android') {
|
||||
implementation('com.google.guava:guava:32.0.0-android') {
|
||||
exclude group: 'com.google.guava', module: 'failureaccess'
|
||||
exclude group: 'org.checkerframework', module: 'checker-qual'
|
||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||
@@ -116,8 +116,8 @@ dependencies {
|
||||
implementation 'androidx.room:room-ktx:2.5.1'
|
||||
kapt 'androidx.room:room-compiler:2.5.1'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||
implementation 'com.squareup.okio:okio:3.3.0'
|
||||
|
||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||
@@ -128,8 +128,8 @@ dependencies {
|
||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||
|
||||
implementation 'io.coil-kt:coil-base:2.3.0'
|
||||
implementation 'io.coil-kt:coil-svg:2.3.0'
|
||||
implementation 'io.coil-kt:coil-base:2.4.0'
|
||||
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
@@ -137,7 +137,7 @@ dependencies {
|
||||
implementation 'ch.acra:acra-http:5.9.7'
|
||||
implementation 'ch.acra:acra-dialog:5.9.7'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.json:json:20230227'
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.EnumSet
|
||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||
|
||||
override val configKeyDomain: ConfigKey.Domain
|
||||
get() = ConfigKey.Domain("", null)
|
||||
get() = ConfigKey.Domain()
|
||||
|
||||
override val sortOrders: Set<SortOrder>
|
||||
get() = EnumSet.allOf(SortOrder::class.java)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.koitharu.kotatsu.bookmarks.data
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||
|
||||
@@ -18,7 +22,7 @@ abstract class BookmarksDao {
|
||||
|
||||
@Transaction
|
||||
@Query(
|
||||
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at"
|
||||
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at",
|
||||
)
|
||||
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
||||
|
||||
@@ -29,5 +33,8 @@ abstract class BookmarksDao {
|
||||
abstract suspend fun delete(entity: BookmarkEntity)
|
||||
|
||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||
abstract suspend fun delete(mangaId: Long, pageId: Long)
|
||||
}
|
||||
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
|
||||
|
||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
||||
abstract suspend fun delete(mangaId: Long, chapterId: Long, page: Int): Int
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.koitharu.kotatsu.bookmarks.domain
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
class Bookmark(
|
||||
val manga: Manga,
|
||||
@@ -27,9 +27,7 @@ class Bookmark(
|
||||
if (scroll != other.scroll) return false
|
||||
if (imageUrl != other.imageUrl) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (percent != other.percent) return false
|
||||
|
||||
return true
|
||||
return percent == other.percent
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -43,4 +41,4 @@ class Bookmark(
|
||||
result = 31 * result + percent.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,14 @@ class BookmarksRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeBookmark(mangaId: Long, pageId: Long) {
|
||||
db.bookmarksDao.delete(mangaId, pageId)
|
||||
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
|
||||
check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) {
|
||||
"Bookmark not found"
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeBookmark(bookmark: Bookmark) {
|
||||
removeBookmark(bookmark.manga.id, bookmark.chapterId, bookmark.page)
|
||||
}
|
||||
|
||||
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {
|
||||
|
||||
@@ -19,7 +19,9 @@ class BookmarksAdapter(
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
||||
return oldItem.manga.id == newItem.manga.id && oldItem.pageId == newItem.pageId
|
||||
return oldItem.manga.id == newItem.manga.id &&
|
||||
oldItem.chapterId == newItem.chapterId &&
|
||||
oldItem.page == newItem.page
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
||||
|
||||
@@ -40,10 +40,11 @@ import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
|
||||
import org.koitharu.kotatsu.core.util.ext.activityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProviderImpl
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
@@ -64,6 +65,9 @@ interface AppModule {
|
||||
@Binds
|
||||
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
|
||||
|
||||
@Binds
|
||||
fun bindListExtraProvider(impl: ListExtraProviderImpl): ListExtraProvider
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@@ -86,7 +90,8 @@ interface AppModule {
|
||||
@ApplicationContext context: Context,
|
||||
@MangaHttpClient okHttpClient: OkHttpClient,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
pagesCache: PagesCache,
|
||||
imageProxyInterceptor: ImageProxyInterceptor,
|
||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||
): ImageLoader {
|
||||
val diskCacheFactory = {
|
||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||
@@ -108,7 +113,8 @@ interface AppModule {
|
||||
.add(SvgDecoder.Factory())
|
||||
.add(CbzFetcher.Factory())
|
||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||
.add(MangaPageFetcher.Factory(context, okHttpClient, pagesCache, mangaRepositoryFactory))
|
||||
.add(pageFetcherFactory)
|
||||
.add(imageProxyInterceptor)
|
||||
.build(),
|
||||
).build()
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration11To12
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration12To13
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
||||
@@ -48,7 +49,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||
|
||||
const val DATABASE_VERSION = 15
|
||||
const val DATABASE_VERSION = 16
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
@@ -100,6 +101,7 @@ val databaseMigrations: Array<Migration>
|
||||
Migration12To13(),
|
||||
Migration13To14(),
|
||||
Migration14To15(),
|
||||
Migration15To16(),
|
||||
)
|
||||
|
||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||
|
||||
@@ -23,4 +23,5 @@ data class MangaPrefsEntity(
|
||||
@ColumnInfo(name = "mode") val mode: Int,
|
||||
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
|
||||
@ColumnInfo(name = "cf_contrast") val cfContrast: Float,
|
||||
@ColumnInfo(name = "cf_invert") val cfInvert: Boolean,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration15To16 : Migration(15, 16) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_invert` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ class AppProxySelector(
|
||||
private val settings: AppSettings,
|
||||
) : ProxySelector() {
|
||||
|
||||
init {
|
||||
setDefault(this)
|
||||
}
|
||||
|
||||
private var cachedProxy: Proxy? = null
|
||||
|
||||
override fun select(uri: URI?): List<Proxy> {
|
||||
|
||||
@@ -14,6 +14,7 @@ object CommonHeaders {
|
||||
const val ACCEPT_ENCODING = "Accept-Encoding"
|
||||
const val AUTHORIZATION = "Authorization"
|
||||
const val CACHE_CONTROL = "Cache-Control"
|
||||
const val PROXY_AUTHORIZATION = "Proxy-Authorization"
|
||||
|
||||
val CACHE_CONTROL_NO_STORE: CacheControl
|
||||
get() = CacheControl.Builder().noStore().build()
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import android.util.Log
|
||||
import androidx.collection.ArraySet
|
||||
import coil.intercept.Interceptor
|
||||
import coil.request.ErrorResult
|
||||
import coil.request.ImageResult
|
||||
import coil.request.SuccessResult
|
||||
import coil.size.Dimension
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
|
||||
import org.koitharu.kotatsu.parsers.util.await
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import java.util.Collections
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ImageProxyInterceptor @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) : Interceptor {
|
||||
|
||||
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
||||
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val request = chain.request
|
||||
if (!settings.isImagesProxyEnabled) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
val url: HttpUrl? = when (val data = request.data) {
|
||||
is HttpUrl -> data
|
||||
is String -> data.toHttpUrlOrNull()
|
||||
else -> null
|
||||
}
|
||||
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
val newUrl = HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("wsrv.nl")
|
||||
.addQueryParameter("url", url.toString())
|
||||
.addQueryParameter("fit", "outside")
|
||||
.addQueryParameter("we", null)
|
||||
val size = request.sizeResolver.size()
|
||||
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
||||
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
||||
|
||||
val newRequest = request.newBuilder()
|
||||
.data(newUrl.build())
|
||||
.build()
|
||||
val result = chain.proceed(newRequest)
|
||||
return if (result is SuccessResult) {
|
||||
result
|
||||
} else {
|
||||
logDebug((result as? ErrorResult)?.throwable)
|
||||
chain.proceed(request).also {
|
||||
if (it is SuccessResult) {
|
||||
blacklist.add(url.host)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||
if (!settings.isImagesProxyEnabled) {
|
||||
return okHttp.newCall(request).await()
|
||||
}
|
||||
val sourceUrl = request.url
|
||||
val targetUrl = HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("wsrv.nl")
|
||||
.addQueryParameter("url", sourceUrl.toString())
|
||||
.addQueryParameter("we", null)
|
||||
val newRequest = request.newBuilder()
|
||||
.url(targetUrl.build())
|
||||
.build()
|
||||
return runCatchingCancellable {
|
||||
okHttp.doCall(newRequest)
|
||||
}.recover {
|
||||
logDebug(it)
|
||||
okHttp.doCall(request).also {
|
||||
blacklist.add(sourceUrl.host)
|
||||
}
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
||||
return newCall(request).await().ensureSuccess()
|
||||
}
|
||||
|
||||
private fun logDebug(e: Throwable?) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w("ImageProxy", e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ interface NetworkModule {
|
||||
writeTimeout(20, TimeUnit.SECONDS)
|
||||
cookieJar(cookieJar)
|
||||
proxySelector(AppProxySelector(settings))
|
||||
proxyAuthenticator(ProxyAuthenticator(settings))
|
||||
dns(DoHManager(cache, settings))
|
||||
if (settings.isSSLBypassEnabled) {
|
||||
bypassSSLErrors()
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import java.net.PasswordAuthentication
|
||||
import java.net.Proxy
|
||||
|
||||
class ProxyAuthenticator(
|
||||
private val settings: AppSettings,
|
||||
) : Authenticator, java.net.Authenticator() {
|
||||
|
||||
init {
|
||||
setDefault(this)
|
||||
}
|
||||
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (!isProxyEnabled()) {
|
||||
return null
|
||||
}
|
||||
if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) {
|
||||
return null
|
||||
}
|
||||
val login = settings.proxyLogin ?: return null
|
||||
val password = settings.proxyPassword ?: return null
|
||||
val credential = Credentials.basic(login, password)
|
||||
return response.request.newBuilder()
|
||||
.header(CommonHeaders.PROXY_AUTHORIZATION, credential)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||
if (!isProxyEnabled()) {
|
||||
return null
|
||||
}
|
||||
val login = settings.proxyLogin ?: return null
|
||||
val password = settings.proxyPassword ?: return null
|
||||
return PasswordAuthentication(login, password.toCharArray())
|
||||
}
|
||||
|
||||
private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT
|
||||
}
|
||||
@@ -5,14 +5,12 @@ import dagger.Reusable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
@@ -22,7 +20,6 @@ import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class MangaDataRepository @Inject constructor(
|
||||
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
||||
private val db: MangaDatabase,
|
||||
) {
|
||||
|
||||
@@ -42,6 +39,7 @@ class MangaDataRepository @Inject constructor(
|
||||
entity.copy(
|
||||
cfBrightness = colorFilter?.brightness ?: 0f,
|
||||
cfContrast = colorFilter?.contrast ?: 0f,
|
||||
cfInvert = colorFilter?.isInverted ?: false,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -84,8 +82,8 @@ class MangaDataRepository @Inject constructor(
|
||||
}
|
||||
|
||||
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
|
||||
return if (cfBrightness != 0f || cfContrast != 0f) {
|
||||
ReaderColorFilter(cfBrightness, cfContrast)
|
||||
return if (cfBrightness != 0f || cfContrast != 0f || cfInvert) {
|
||||
ReaderColorFilter(cfBrightness, cfContrast, cfInvert)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -96,5 +94,6 @@ class MangaDataRepository @Inject constructor(
|
||||
mode = -1,
|
||||
cfBrightness = 0f,
|
||||
cfContrast = 0f,
|
||||
cfInvert = false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ interface MangaRepository {
|
||||
|
||||
val sortOrders: Set<SortOrder>
|
||||
|
||||
var defaultSortOrder: SortOrder
|
||||
|
||||
suspend fun getList(offset: Int, query: String): List<Manga>
|
||||
|
||||
suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga>
|
||||
|
||||
@@ -39,8 +39,8 @@ class RemoteMangaRepository(
|
||||
override val sortOrders: Set<SortOrder>
|
||||
get() = parser.sortOrders
|
||||
|
||||
var defaultSortOrder: SortOrder?
|
||||
get() = getConfig().defaultSortOrder ?: sortOrders.firstOrNull()
|
||||
override var defaultSortOrder: SortOrder
|
||||
get() = getConfig().defaultSortOrder ?: sortOrders.first()
|
||||
set(value) {
|
||||
getConfig().defaultSortOrder = value
|
||||
}
|
||||
@@ -101,7 +101,7 @@ class RemoteMangaRepository(
|
||||
}
|
||||
|
||||
fun getAvailableMirrors(): List<String> {
|
||||
return parser.configKeyDomain.presetValues?.toList().orEmpty()
|
||||
return parser.configKeyDomain.presetValues.toList()
|
||||
}
|
||||
|
||||
private fun getConfig() = parser.config as SourceSettings
|
||||
|
||||
@@ -86,7 +86,7 @@ class FaviconFetcher(
|
||||
if (!options.diskCachePolicy.readEnabled) {
|
||||
return null
|
||||
}
|
||||
val snapshot = diskCache.value?.get(diskCacheKey) ?: return null
|
||||
val snapshot = diskCache.value?.openSnapshot(diskCacheKey) ?: return null
|
||||
return SourceResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = null,
|
||||
@@ -98,12 +98,12 @@ class FaviconFetcher(
|
||||
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
|
||||
return null
|
||||
}
|
||||
val editor = diskCache.value?.edit(diskCacheKey) ?: return null
|
||||
val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null
|
||||
try {
|
||||
fileSystem.write(editor.data) {
|
||||
body.source().readAll(this)
|
||||
}
|
||||
return editor.commitAndGet()
|
||||
return editor.commitAndOpenSnapshot()
|
||||
} catch (e: Throwable) {
|
||||
try {
|
||||
editor.abort()
|
||||
|
||||
@@ -277,6 +277,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isReaderSliderEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
||||
|
||||
val isImagesProxyEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
|
||||
|
||||
val dnsOverHttps: DoHProvider
|
||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||
|
||||
@@ -295,6 +298,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val proxyPort: Int
|
||||
get() = prefs.getString(KEY_PROXY_PORT, null)?.toIntOrNull() ?: 0
|
||||
|
||||
val proxyLogin: String?
|
||||
get() = prefs.getString(KEY_PROXY_LOGIN, null)?.takeUnless { it.isEmpty() }
|
||||
|
||||
val proxyPassword: String?
|
||||
get() = prefs.getString(KEY_PROXY_PASSWORD, null)?.takeUnless { it.isEmpty() }
|
||||
|
||||
var localListOrder: SortOrder
|
||||
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
|
||||
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
|
||||
@@ -449,6 +458,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_PROXY_TYPE = "proxy_type"
|
||||
const val KEY_PROXY_ADDRESS = "proxy_address"
|
||||
const val KEY_PROXY_PORT = "proxy_port"
|
||||
const val KEY_PROXY_AUTH = "proxy_auth"
|
||||
const val KEY_PROXY_LOGIN = "proxy_login"
|
||||
const val KEY_PROXY_PASSWORD = "proxy_password"
|
||||
const val KEY_IMAGES_PROXY = "images_proxy"
|
||||
|
||||
// About
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
|
||||
@@ -18,6 +18,10 @@ import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplaySize
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@Deprecated(
|
||||
"Use BaseAdaptiveSheet",
|
||||
replaceWith = ReplaceWith("BaseAdaptiveSheet<B>", "org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet"),
|
||||
)
|
||||
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
|
||||
|
||||
var viewBinding: B? = null
|
||||
|
||||
@@ -3,57 +3,36 @@ package org.koitharu.kotatsu.core.ui
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private const val SYSTEM_UI_FLAGS_SHOWN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private const val SYSTEM_UI_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
|
||||
abstract class BaseFullscreenActivity<B : ViewBinding> :
|
||||
BaseActivity<B>(),
|
||||
View.OnSystemUiVisibilityChangeListener {
|
||||
BaseActivity<B>() {
|
||||
|
||||
private lateinit var insetsControllerCompat: WindowInsetsControllerCompat
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
with(window) {
|
||||
insetsControllerCompat = WindowInsetsControllerCompat(this, decorView)
|
||||
statusBarColor = Color.TRANSPARENT
|
||||
navigationBarColor = Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
attributes.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
|
||||
}
|
||||
insetsControllerCompat.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
showSystemUI()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
|
||||
@Deprecated("Deprecated in Java")
|
||||
final override fun onSystemUiVisibilityChange(visibility: Int) {
|
||||
onSystemUiVisibilityChanged(visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0)
|
||||
}
|
||||
|
||||
// TODO WindowInsetsControllerCompat works incorrect
|
||||
@Suppress("DEPRECATION")
|
||||
protected fun hideSystemUI() {
|
||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
|
||||
insetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
protected fun showSystemUI() {
|
||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_SHOWN
|
||||
insetsControllerCompat.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
|
||||
protected open fun onSystemUiVisibilityChanged(isVisible: Boolean) = Unit
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package org.koitharu.kotatsu.core.ui.list
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class BoundsScrollListener(private val offsetTop: Int, private val offsetBottom: Int) :
|
||||
RecyclerView.OnScrollListener() {
|
||||
abstract class BoundsScrollListener(
|
||||
@JvmField protected val offsetTop: Int,
|
||||
@JvmField protected val offsetBottom: Int
|
||||
) : RecyclerView.OnScrollListener() {
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
@@ -24,9 +26,16 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
|
||||
if (firstVisibleItemPosition <= offsetTop) {
|
||||
onScrolledToStart(recyclerView)
|
||||
}
|
||||
onPostScrolled(recyclerView, firstVisibleItemPosition, visibleItemCount)
|
||||
}
|
||||
|
||||
abstract fun onScrolledToStart(recyclerView: RecyclerView)
|
||||
|
||||
abstract fun onScrolledToEnd(recyclerView: RecyclerView)
|
||||
|
||||
protected open fun onPostScrolled(
|
||||
recyclerView: RecyclerView,
|
||||
firstVisibleItemPosition: Int,
|
||||
visibleItemCount: Int
|
||||
) = Unit
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.ui.list
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class ScrollListenerInvalidationObserver(
|
||||
private val recyclerView: RecyclerView,
|
||||
private val scrollListener: RecyclerView.OnScrollListener,
|
||||
) : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
override fun onChanged() {
|
||||
super.onChanged()
|
||||
invalidateScroll()
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
invalidateScroll()
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeRemoved(positionStart, itemCount)
|
||||
invalidateScroll()
|
||||
}
|
||||
|
||||
private fun invalidateScroll() {
|
||||
recyclerView.post {
|
||||
scrollListener.onScrolled(recyclerView, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
@@ -15,6 +16,12 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||
|
||||
val fastScroller = FastScroller(context, attrs)
|
||||
|
||||
var isFastScrollerEnabled: Boolean = true
|
||||
set(value) {
|
||||
field = value
|
||||
fastScroller.isVisible = value && isVisible
|
||||
}
|
||||
|
||||
init {
|
||||
fastScroller.id = R.id.fast_scroller
|
||||
fastScroller.layoutParams = ViewGroup.LayoutParams(
|
||||
@@ -30,7 +37,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
super.setVisibility(visibility)
|
||||
fastScroller.visibility = visibility
|
||||
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
|
||||
@@ -56,6 +56,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
private var bubbleHeight = 0
|
||||
private var handleHeight = 0
|
||||
private var viewHeight = 0
|
||||
private var offset = 0
|
||||
private var hideScrollbar = true
|
||||
private var showBubble = true
|
||||
private var showBubbleAlways = false
|
||||
@@ -137,6 +138,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
bubbleSize = getBubbleSize(R.styleable.FastScroller_bubbleSize, BubbleSize.NORMAL)
|
||||
val textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize)
|
||||
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||
offset = getDimensionPixelOffset(R.styleable.FastScroller_scrollerOffset, offset)
|
||||
}
|
||||
|
||||
setTrackColor(trackColor)
|
||||
@@ -248,7 +250,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
|
||||
layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
|
||||
height = 0
|
||||
setMargins(0, marginTop, 0, marginBottom)
|
||||
setMargins(offset, marginTop, offset, marginBottom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,13 +258,13 @@ class FastScroller @JvmOverloads constructor(
|
||||
height = LayoutParams.MATCH_PARENT
|
||||
anchorGravity = GravityCompat.END
|
||||
anchorId = recyclerViewId
|
||||
setMargins(0, marginTop, 0, marginBottom)
|
||||
setMargins(offset, marginTop, offset, marginBottom)
|
||||
}
|
||||
|
||||
is FrameLayout -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply {
|
||||
height = LayoutParams.MATCH_PARENT
|
||||
gravity = GravityCompat.END
|
||||
setMargins(0, marginTop, 0, marginBottom)
|
||||
setMargins(offset, marginTop, offset, marginBottom)
|
||||
}
|
||||
|
||||
is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply {
|
||||
@@ -270,7 +272,7 @@ class FastScroller @JvmOverloads constructor(
|
||||
addRule(RelativeLayout.ALIGN_TOP, recyclerViewId)
|
||||
addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId)
|
||||
addRule(RelativeLayout.ALIGN_END, recyclerViewId)
|
||||
setMargins(0, marginTop, 0, marginBottom)
|
||||
setMargins(offset, marginTop, offset, marginBottom)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout")
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.app.Dialog
|
||||
import android.view.View
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.sidesheet.SideSheetBehavior
|
||||
import com.google.android.material.sidesheet.SideSheetCallback
|
||||
import com.google.android.material.sidesheet.SideSheetDialog
|
||||
import java.util.LinkedList
|
||||
|
||||
sealed class AdaptiveSheetBehavior {
|
||||
|
||||
@JvmField
|
||||
protected val callbacks = LinkedList<AdaptiveSheetCallback>()
|
||||
|
||||
abstract var state: Int
|
||||
|
||||
abstract var isDraggable: Boolean
|
||||
|
||||
open val isHideable: Boolean = true
|
||||
|
||||
fun addCallback(callback: AdaptiveSheetCallback) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
|
||||
fun removeCallback(callback: AdaptiveSheetCallback) {
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
class Bottom(
|
||||
private val delegate: BottomSheetBehavior<*>,
|
||||
) : AdaptiveSheetBehavior() {
|
||||
|
||||
override var state: Int
|
||||
get() = delegate.state
|
||||
set(value) {
|
||||
delegate.state = value
|
||||
}
|
||||
|
||||
override var isDraggable: Boolean
|
||||
get() = delegate.isDraggable
|
||||
set(value) {
|
||||
delegate.isDraggable = value
|
||||
}
|
||||
|
||||
override val isHideable: Boolean
|
||||
get() = delegate.isHideable
|
||||
|
||||
var isFitToContents: Boolean
|
||||
get() = delegate.isFitToContents
|
||||
set(value) {
|
||||
delegate.isFitToContents = value
|
||||
}
|
||||
|
||||
init {
|
||||
delegate.addBottomSheetCallback(
|
||||
object : BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
callbacks.forEach { it.onStateChanged(bottomSheet, newState) }
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
callbacks.forEach { it.onSlide(bottomSheet, slideOffset) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Side(
|
||||
private val delegate: SideSheetBehavior<*>,
|
||||
) : AdaptiveSheetBehavior() {
|
||||
|
||||
override var state: Int
|
||||
get() = delegate.state
|
||||
set(value) {
|
||||
delegate.state = value
|
||||
}
|
||||
|
||||
override var isDraggable: Boolean
|
||||
get() = delegate.isDraggable
|
||||
set(value) {
|
||||
delegate.isDraggable = value
|
||||
}
|
||||
|
||||
init {
|
||||
delegate.addCallback(
|
||||
object : SideSheetCallback() {
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
callbacks.forEach { it.onStateChanged(sheet, newState) }
|
||||
}
|
||||
|
||||
override fun onSlide(sheet: View, slideOffset: Float) {
|
||||
callbacks.forEach { it.onSlide(sheet, slideOffset) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val STATE_EXPANDED = SideSheetBehavior.STATE_EXPANDED
|
||||
const val STATE_SETTLING = SideSheetBehavior.STATE_SETTLING
|
||||
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
|
||||
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
|
||||
|
||||
fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) {
|
||||
is BottomSheetDialog -> Bottom(dialog.behavior)
|
||||
is SideSheetDialog -> Side(dialog.behavior)
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun from(lp: CoordinatorLayout.LayoutParams): AdaptiveSheetBehavior? = when (val behavior = lp.behavior) {
|
||||
is BottomSheetBehavior<*> -> Bottom(behavior)
|
||||
is SideSheetBehavior<*> -> Side(behavior)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.view.View
|
||||
|
||||
interface AdaptiveSheetCallback {
|
||||
|
||||
/**
|
||||
* Called when the sheet changes its state.
|
||||
*
|
||||
* @param sheet The sheet view.
|
||||
* @param newState The new state.
|
||||
*/
|
||||
fun onStateChanged(sheet: View, newState: Int)
|
||||
|
||||
/**
|
||||
* Called when the sheet is being dragged.
|
||||
*
|
||||
* @param sheet The sheet view.
|
||||
* @param slideOffset The new offset of this sheet.
|
||||
*/
|
||||
fun onSlide(sheet: View, slideOffset: Float) = Unit
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.parents
|
||||
import org.koitharu.kotatsu.databinding.LayoutSheetHeaderAdaptiveBinding
|
||||
|
||||
class AdaptiveSheetHeaderBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : LinearLayout(context, attrs, defStyleAttr), AdaptiveSheetCallback {
|
||||
|
||||
private val binding = LayoutSheetHeaderAdaptiveBinding.inflate(LayoutInflater.from(context), this)
|
||||
private var sheetBehavior: AdaptiveSheetBehavior? = null
|
||||
|
||||
var title: CharSequence?
|
||||
get() = binding.shTextViewTitle.text
|
||||
set(value) {
|
||||
binding.shTextViewTitle.text = value
|
||||
}
|
||||
|
||||
val isTitleVisible: Boolean
|
||||
get() = binding.shLayoutSidesheet.isVisible
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
binding.shButtonClose.setOnClickListener { dismissSheet() }
|
||||
context.withStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.AdaptiveSheetHeaderBar, defStyleAttr,
|
||||
) {
|
||||
title = getText(R.styleable.AdaptiveSheetHeaderBar_title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (isInEditMode) {
|
||||
val isTabled = resources.getBoolean(R.bool.is_tablet)
|
||||
binding.shDragHandle.isGone = isTabled
|
||||
binding.shLayoutSidesheet.isVisible = isTabled
|
||||
} else {
|
||||
setBottomSheetBehavior(findParentSheetBehavior())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
setBottomSheetBehavior(null)
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
|
||||
}
|
||||
|
||||
fun setTitle(@StringRes resId: Int) {
|
||||
binding.shTextViewTitle.setText(resId)
|
||||
}
|
||||
|
||||
private fun setBottomSheetBehavior(behavior: AdaptiveSheetBehavior?) {
|
||||
binding.shDragHandle.isVisible = behavior is AdaptiveSheetBehavior.Bottom
|
||||
binding.shLayoutSidesheet.isVisible = behavior is AdaptiveSheetBehavior.Side
|
||||
sheetBehavior?.removeCallback(this)
|
||||
sheetBehavior = behavior
|
||||
behavior?.addCallback(this)
|
||||
}
|
||||
|
||||
private fun dismissSheet() {
|
||||
sheetBehavior?.state = AdaptiveSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
|
||||
private fun findParentSheetBehavior(): AdaptiveSheetBehavior? {
|
||||
for (p in parents) {
|
||||
val layoutParams = (p as? View)?.layoutParams
|
||||
if (layoutParams is CoordinatorLayout.LayoutParams) {
|
||||
AdaptiveSheetBehavior.from(layoutParams)?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package org.koitharu.kotatsu.core.ui.sheet
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams
|
||||
import androidx.activity.ComponentDialog
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.sidesheet.SideSheetDialog
|
||||
import org.koitharu.kotatsu.R
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
|
||||
|
||||
private var waitingForDismissAllowingStateLoss = false
|
||||
private var isFitToContentsDisabled = false
|
||||
|
||||
var viewBinding: B? = null
|
||||
private set
|
||||
|
||||
@Deprecated("", ReplaceWith("requireViewBinding()"))
|
||||
protected val binding: B
|
||||
get() = requireViewBinding()
|
||||
|
||||
protected val behavior: AdaptiveSheetBehavior?
|
||||
get() = AdaptiveSheetBehavior.from(dialog)
|
||||
|
||||
val isExpanded: Boolean
|
||||
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||
|
||||
val onBackPressedDispatcher: OnBackPressedDispatcher
|
||||
get() = (requireDialog() as ComponentDialog).onBackPressedDispatcher
|
||||
|
||||
final override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
val binding = onCreateViewBinding(inflater, container)
|
||||
viewBinding = binding
|
||||
return binding.root
|
||||
}
|
||||
|
||||
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val binding = requireViewBinding()
|
||||
onViewBindingCreated(binding, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
viewBinding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val context = requireContext()
|
||||
return if (context.resources.getBoolean(R.bool.is_tablet)) {
|
||||
SideSheetDialog(context, theme)
|
||||
} else {
|
||||
BottomSheetDialog(context, theme)
|
||||
}
|
||||
}
|
||||
|
||||
fun addSheetCallback(callback: AdaptiveSheetCallback) {
|
||||
val b = behavior ?: return
|
||||
b.addCallback(callback)
|
||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
||||
?: dialog?.findViewById(materialR.id.coordinator)
|
||||
if (rootView != null) {
|
||||
callback.onStateChanged(rootView, b.state)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
|
||||
|
||||
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
|
||||
|
||||
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
|
||||
val b = behavior ?: return
|
||||
if (isExpanded) {
|
||||
b.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
if (b is AdaptiveSheetBehavior.Bottom) {
|
||||
b.isFitToContents = !isFitToContentsDisabled && !isExpanded
|
||||
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
|
||||
rootView?.updateLayoutParams {
|
||||
height = if (isFitToContentsDisabled || isExpanded) {
|
||||
LayoutParams.MATCH_PARENT
|
||||
} else {
|
||||
LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
}
|
||||
b.isDraggable = !isLocked
|
||||
}
|
||||
|
||||
protected fun disableFitToContents() {
|
||||
isFitToContentsDisabled = true
|
||||
val b = behavior as? AdaptiveSheetBehavior.Bottom ?: return
|
||||
b.isFitToContents = false
|
||||
dialog?.findViewById<View>(materialR.id.design_bottom_sheet)?.updateLayoutParams {
|
||||
height = LayoutParams.MATCH_PARENT
|
||||
}
|
||||
}
|
||||
|
||||
fun requireViewBinding(): B = checkNotNull(viewBinding) {
|
||||
"Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()."
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
if (!tryDismissWithAnimation(false)) {
|
||||
super.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismissAllowingStateLoss() {
|
||||
if (!tryDismissWithAnimation(true)) {
|
||||
super.dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to dismiss the dialog fragment with the bottom sheet animation. Returns true if possible,
|
||||
* false otherwise.
|
||||
*/
|
||||
private fun tryDismissWithAnimation(allowingStateLoss: Boolean): Boolean {
|
||||
val shouldDismissWithAnimation = when (val dialog = dialog) {
|
||||
is BottomSheetDialog -> dialog.dismissWithAnimation
|
||||
is SideSheetDialog -> dialog.isDismissWithSheetAnimationEnabled
|
||||
else -> false
|
||||
}
|
||||
val behavior = behavior ?: return false
|
||||
return if (shouldDismissWithAnimation && behavior.isHideable) {
|
||||
dismissWithAnimation(behavior, allowingStateLoss)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissWithAnimation(behavior: AdaptiveSheetBehavior, allowingStateLoss: Boolean) {
|
||||
waitingForDismissAllowingStateLoss = allowingStateLoss
|
||||
if (behavior.state == AdaptiveSheetBehavior.STATE_HIDDEN) {
|
||||
dismissAfterAnimation()
|
||||
} else {
|
||||
behavior.addCallback(SheetDismissCallback())
|
||||
behavior.state = AdaptiveSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissAfterAnimation() {
|
||||
if (waitingForDismissAllowingStateLoss) {
|
||||
super.dismissAllowingStateLoss()
|
||||
} else {
|
||||
super.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SheetDismissCallback : AdaptiveSheetCallback {
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
dismissAfterAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(sheet: View, slideOffset: Float) {}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import com.google.android.material.R as materialR
|
||||
|
||||
private const val THROTTLE_DELAY = 200L
|
||||
|
||||
@Deprecated("")
|
||||
class BottomSheetHeaderBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.View.OnClickListener
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.getColorStateListOrThrow
|
||||
import androidx.core.view.children
|
||||
@@ -101,6 +102,13 @@ class ChipsView @JvmOverloads constructor(
|
||||
chip.setTextColor(tint ?: defaultChipTextColor)
|
||||
chip.isClickable = onChipClickListener != null || model.isCheckable
|
||||
chip.isCheckable = model.isCheckable
|
||||
if (model.icon == 0) {
|
||||
chip.chipIcon = null
|
||||
chip.isChipIconVisible = false
|
||||
} else {
|
||||
chip.setChipIconResource(model.icon)
|
||||
chip.isChipIconVisible = true
|
||||
}
|
||||
chip.isChecked = model.isChecked
|
||||
chip.tag = model.data
|
||||
}
|
||||
@@ -134,6 +142,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
class ChipModel(
|
||||
@ColorRes val tint: Int,
|
||||
val title: CharSequence,
|
||||
@DrawableRes val icon: Int,
|
||||
val isCheckable: Boolean,
|
||||
val isChecked: Boolean,
|
||||
val data: Any? = null,
|
||||
@@ -147,6 +156,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
|
||||
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
|
||||
@@ -155,6 +165,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.util
|
||||
|
||||
class CompositeRunnable(
|
||||
private val children: List<Runnable>,
|
||||
) : Runnable, Collection<Runnable> by children {
|
||||
|
||||
override fun run() {
|
||||
for (child in children) {
|
||||
child.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ class Event<T>(
|
||||
private var isConsumed = false
|
||||
|
||||
suspend fun consume(collector: FlowCollector<T>) {
|
||||
if (isConsumed) {
|
||||
if (!isConsumed) {
|
||||
collector.emit(data)
|
||||
isConsumed = true
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class RetainedLifecycleCoroutineScope(
|
||||
private val lifecycle: RetainedLifecycle,
|
||||
val lifecycle: RetainedLifecycle,
|
||||
) : CoroutineScope, RetainedLifecycle.OnClearedListener {
|
||||
|
||||
override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.MemoryInfo
|
||||
import android.app.ActivityOptions
|
||||
import android.content.Context
|
||||
import android.content.Context.ACTIVITY_SERVICE
|
||||
@@ -140,6 +141,13 @@ fun Context.isLowRamDevice(): Boolean {
|
||||
return activityManager?.isLowRamDevice ?: false
|
||||
}
|
||||
|
||||
val Context.ramAvailable: Long
|
||||
get() {
|
||||
val result = MemoryInfo()
|
||||
activityManager?.getMemoryInfo(result)
|
||||
return result.availMem
|
||||
}
|
||||
|
||||
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
|
||||
view,
|
||||
0,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.view.View
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
|
||||
fun BottomSheetBehavior<*>.doOnExpansionsChanged(callback: (isExpanded: Boolean) -> Unit) {
|
||||
var isExpended = state == BottomSheetBehavior.STATE_EXPANDED
|
||||
callback(isExpended)
|
||||
addBottomSheetCallback(
|
||||
object : BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
val expanded = newState == BottomSheetBehavior.STATE_EXPANDED
|
||||
if (expanded != isExpended) {
|
||||
isExpended = expanded
|
||||
callback(expanded)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -8,9 +8,11 @@ import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
@@ -18,6 +20,9 @@ import kotlin.coroutines.resumeWithException
|
||||
val processLifecycleScope: LifecycleCoroutineScope
|
||||
inline get() = ProcessLifecycleOwner.get().lifecycleScope
|
||||
|
||||
val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope
|
||||
inline get() = RetainedLifecycleCoroutineScope(this)
|
||||
|
||||
suspend fun Lifecycle.awaitStateAtLeast(state: Lifecycle.State) {
|
||||
if (currentState.isAtLeast(state)) {
|
||||
return
|
||||
|
||||
@@ -9,13 +9,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.util.Event
|
||||
|
||||
fun <T> Flow<T>.observe(owner: LifecycleOwner, collector: FlowCollector<T>) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
require((this as? StateFlow)?.value !is Event<*>)
|
||||
}
|
||||
val start = if (this is StateFlow) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT
|
||||
owner.lifecycleScope.launch(start = start) {
|
||||
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
@@ -23,3 +24,17 @@ fun Response.parseJsonOrNull(): JSONObject? {
|
||||
closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
val HttpUrl.isHttpOrHttps: Boolean
|
||||
get() {
|
||||
val s = scheme.lowercase()
|
||||
return s == "https" || s == "http"
|
||||
}
|
||||
|
||||
fun Response.ensureSuccess() = apply {
|
||||
if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) {
|
||||
val message = "Invalid response: $code $message at ${request.url}"
|
||||
closeQuietly()
|
||||
throw IllegalStateException(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import org.koitharu.kotatsu.core.util.CompositeRunnable
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||
if (obj == null || !isInstance(obj)) {
|
||||
@@ -7,3 +9,15 @@ fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||
}
|
||||
return obj as T
|
||||
}
|
||||
|
||||
/* CompositeRunnable */
|
||||
|
||||
operator fun Runnable.plus(other: Runnable): Runnable {
|
||||
val list = ArrayList<Runnable>(this.size + other.size)
|
||||
if (this is CompositeRunnable) list.addAll(this) else list.add(this)
|
||||
if (other is CompositeRunnable) list.addAll(other) else list.add(other)
|
||||
return CompositeRunnable(list)
|
||||
}
|
||||
|
||||
private val Runnable.size: Int
|
||||
get() = if (this is CompositeRunnable) size else 1
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.Color
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.graphics.ColorUtils
|
||||
|
||||
@@ -22,6 +23,22 @@ fun Context.getThemeColor(
|
||||
it.getColor(0, fallback)
|
||||
}
|
||||
|
||||
@Px
|
||||
fun Context.getThemeDimensionPixelSize(
|
||||
@AttrRes resId: Int,
|
||||
@ColorInt fallback: Int = 0,
|
||||
) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getDimensionPixelSize(0, fallback)
|
||||
}
|
||||
|
||||
@Px
|
||||
fun Context.getThemeDimensionPixelOffset(
|
||||
@AttrRes resId: Int,
|
||||
@ColorInt fallback: Int = 0,
|
||||
) = obtainStyledAttributes(intArrayOf(resId)).use {
|
||||
it.getDimensionPixelOffset(0, fallback)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.getThemeColor(
|
||||
@AttrRes resId: Int,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.util.ext
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.createViewModelLazy
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
|
||||
@MainThread
|
||||
@@ -17,3 +19,7 @@ inline fun <reified VM : ViewModel> Fragment.parentFragmentViewModels(
|
||||
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
|
||||
factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory },
|
||||
)
|
||||
|
||||
val ViewModelStore.values: Collection<ViewModel>
|
||||
@SuppressLint("RestrictedApi")
|
||||
get() = this.keys().mapNotNull { get(it) }
|
||||
|
||||
@@ -6,18 +6,25 @@ import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
|
||||
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
|
||||
|
||||
class ChaptersBottomSheetMediator(
|
||||
bottomSheet: View,
|
||||
private val behavior: BottomSheetBehavior<*>,
|
||||
) : OnBackPressedCallback(false),
|
||||
ActionModeListener,
|
||||
BottomSheetHeaderBar.OnExpansionChangeListener,
|
||||
OnLayoutChangeListener {
|
||||
|
||||
private val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||
private var lockCounter = 0
|
||||
|
||||
init {
|
||||
behavior.doOnExpansionsChanged { isExpanded ->
|
||||
isEnabled = isExpanded
|
||||
if (!isExpanded) {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
@@ -30,13 +37,6 @@ class ChaptersBottomSheetMediator(
|
||||
unlock()
|
||||
}
|
||||
|
||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
||||
isEnabled = isExpanded
|
||||
if (!isExpanded) {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChange(
|
||||
v: View?,
|
||||
left: Int,
|
||||
@@ -61,6 +61,9 @@ class ChaptersBottomSheetMediator(
|
||||
|
||||
fun unlock() {
|
||||
lockCounter--
|
||||
if (lockCounter < 0) {
|
||||
lockCounter = 0
|
||||
}
|
||||
behavior.isDraggable = lockCounter <= 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
@@ -56,6 +57,9 @@ class ChaptersFragment :
|
||||
checkNotNull(selectionController).attachToRecyclerView(this)
|
||||
setHasFixedSize(true)
|
||||
adapter = chaptersAdapter
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
scrollIndicators = if (resources.getBoolean(R.bool.is_tablet)) 0 else View.SCROLL_INDICATOR_TOP
|
||||
}
|
||||
}
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
|
||||
fun mapChapters(
|
||||
remoteManga: Manga?,
|
||||
@@ -11,12 +13,14 @@ fun mapChapters(
|
||||
history: MangaHistory?,
|
||||
newCount: Int,
|
||||
branch: String?,
|
||||
bookmarks: List<Bookmark>,
|
||||
): List<ChapterListItem> {
|
||||
val remoteChapters = remoteManga?.getChapters(branch).orEmpty()
|
||||
val localChapters = localManga?.getChapters(branch).orEmpty()
|
||||
if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val bookmarked = bookmarks.mapToSet { it.chapterId }
|
||||
val currentId = history?.chapterId ?: 0L
|
||||
val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount
|
||||
val chaptersSize = maxOf(remoteChapters.size, localChapters.size)
|
||||
@@ -41,6 +45,7 @@ fun mapChapters(
|
||||
isUnread = isUnread,
|
||||
isNew = isUnread && result.size >= newFrom,
|
||||
isDownloaded = local != null,
|
||||
isBookmarked = chapter.id in bookmarked,
|
||||
)
|
||||
}
|
||||
if (!localMap.isNullOrEmpty()) {
|
||||
@@ -53,6 +58,7 @@ fun mapChapters(
|
||||
isUnread = isUnread,
|
||||
isNew = false,
|
||||
isDownloaded = remoteManga != null,
|
||||
isBookmarked = chapter.id in bookmarked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ package org.koitharu.kotatsu.details.ui
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.MenuProvider
|
||||
import org.koitharu.kotatsu.R
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class ChaptersMenuProvider(
|
||||
private val viewModel: DetailsViewModel,
|
||||
private val bottomSheetMediator: ChaptersBottomSheetMediator?,
|
||||
) : MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
|
||||
) : OnBackPressedCallback(false), MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
|
||||
|
||||
private var searchItemRef: WeakReference<MenuItem>? = null
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_chapters, menu)
|
||||
@@ -20,6 +24,7 @@ class ChaptersMenuProvider(
|
||||
searchView.setOnQueryTextListener(this)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
searchItemRef = WeakReference(searchMenuItem)
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
@@ -32,15 +37,22 @@ class ChaptersMenuProvider(
|
||||
viewModel.setChaptersReversed(!menuItem.isChecked)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
searchItemRef?.get()?.collapseActionView()
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
bottomSheetMediator?.lock()
|
||||
isEnabled = true
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
isEnabled = false
|
||||
(item.actionView as? SearchView)?.setQuery("", false)
|
||||
viewModel.performChapterSearch(null)
|
||||
bottomSheetMediator?.unlock()
|
||||
|
||||
@@ -4,12 +4,14 @@ import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.transition.AutoTransition
|
||||
import android.transition.Slide
|
||||
import android.transition.TransitionManager
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
@@ -17,7 +19,9 @@ import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -31,8 +35,11 @@ import org.koitharu.kotatsu.core.parser.MangaIntent
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
|
||||
import org.koitharu.kotatsu.core.util.ViewBadge
|
||||
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
|
||||
@@ -49,19 +56,16 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DetailsActivity :
|
||||
BaseActivity<ActivityDetailsBinding>(),
|
||||
View.OnClickListener,
|
||||
BottomSheetHeaderBar.OnExpansionChangeListener,
|
||||
NoModalBottomSheetOwner,
|
||||
View.OnLongClickListener,
|
||||
PopupMenu.OnMenuItemClickListener {
|
||||
|
||||
override val bsHeader: BottomSheetHeaderBar?
|
||||
get() = viewBinding.headerChapters
|
||||
|
||||
@Inject
|
||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||
|
||||
@@ -82,16 +86,22 @@ class DetailsActivity :
|
||||
viewBinding.buttonDropdown.setOnClickListener(this)
|
||||
viewBadge = ViewBadge(viewBinding.buttonRead, this)
|
||||
|
||||
chaptersMenuProvider = if (viewBinding.layoutBottom != null) {
|
||||
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(viewBinding.layoutBottom))
|
||||
if (viewBinding.layoutBottom != null) {
|
||||
val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom))
|
||||
val bsMediator = ChaptersBottomSheetMediator(behavior)
|
||||
actionModeDelegate.addListener(bsMediator)
|
||||
checkNotNull(viewBinding.headerChapters).addOnExpansionChangeListener(bsMediator)
|
||||
checkNotNull(viewBinding.headerChapters).addOnLayoutChangeListener(bsMediator)
|
||||
checkNotNull(viewBinding.layoutBsHeader).addOnLayoutChangeListener(bsMediator)
|
||||
onBackPressedDispatcher.addCallback(bsMediator)
|
||||
ChaptersMenuProvider(viewModel, bsMediator)
|
||||
chaptersMenuProvider = ChaptersMenuProvider(viewModel, bsMediator)
|
||||
behavior.doOnExpansionsChanged(::onChaptersSheetStateChanged)
|
||||
viewBinding.toolbarChapters?.setNavigationOnClickListener {
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
} else {
|
||||
ChaptersMenuProvider(viewModel, null)
|
||||
chaptersMenuProvider = ChaptersMenuProvider(viewModel, null)
|
||||
addMenuProvider(chaptersMenuProvider)
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(chaptersMenuProvider)
|
||||
|
||||
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
|
||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||
@@ -114,11 +124,11 @@ class DetailsActivity :
|
||||
}
|
||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||
viewModel.selectedBranch.observe(this) {
|
||||
viewBinding.headerChapters?.subtitle = it
|
||||
viewBinding.toolbarChapters?.subtitle = it
|
||||
viewBinding.textViewSubtitle?.textAndVisible = it
|
||||
}
|
||||
viewModel.isChaptersReversed.observe(this) {
|
||||
viewBinding.headerChapters?.invalidateMenu() ?: invalidateOptionsMenu()
|
||||
viewBinding.toolbarChapters?.invalidateMenu() ?: invalidateOptionsMenu()
|
||||
}
|
||||
viewModel.favouriteCategories.observe(this) {
|
||||
invalidateOptionsMenu()
|
||||
@@ -137,7 +147,10 @@ class DetailsActivity :
|
||||
shortcutsUpdater = shortcutsUpdater,
|
||||
),
|
||||
)
|
||||
viewBinding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider)
|
||||
}
|
||||
|
||||
override fun getBottomSheetCollapsedHeight(): Int {
|
||||
return viewBinding.layoutBsHeader?.measureHeight() ?: 0
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
@@ -184,11 +197,19 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
||||
private fun onChaptersSheetStateChanged(isExpanded: Boolean) {
|
||||
val toolbar = viewBinding.toolbarChapters ?: return
|
||||
if (isAnimationsEnabled) {
|
||||
val transition = AutoTransition()
|
||||
transition.duration = getAnimationDuration(R.integer.config_shorterAnimTime)
|
||||
TransitionManager.beginDelayedTransition(toolbar, transition)
|
||||
}
|
||||
if (isExpanded) {
|
||||
headerBar.addMenuProvider(chaptersMenuProvider)
|
||||
toolbar.addMenuProvider(chaptersMenuProvider)
|
||||
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material)
|
||||
} else {
|
||||
headerBar.removeMenuProvider(chaptersMenuProvider)
|
||||
toolbar.removeMenuProvider(chaptersMenuProvider)
|
||||
toolbar.navigationIcon = null
|
||||
}
|
||||
viewBinding.buttonRead.isGone = isExpanded
|
||||
}
|
||||
@@ -219,6 +240,12 @@ class DetailsActivity :
|
||||
if (insets.bottom > 0) {
|
||||
window.setNavigationBarTransparentCompat(this, viewBinding.layoutBottom?.elevation ?: 0f, 0.9f)
|
||||
}
|
||||
viewBinding.cardChapters?.updateLayoutParams<MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom + marginEnd
|
||||
}
|
||||
viewBinding.dragHandle?.updateLayoutParams<MarginLayoutParams> {
|
||||
bottomMargin = insets.bottom
|
||||
}
|
||||
}
|
||||
|
||||
private fun onHistoryChanged(info: HistoryInfo) {
|
||||
@@ -237,7 +264,7 @@ class DetailsActivity :
|
||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
||||
}
|
||||
viewBinding.headerChapters?.title = text
|
||||
viewBinding.toolbarChapters?.title = text
|
||||
viewBinding.textViewTitle?.text = text
|
||||
}
|
||||
|
||||
@@ -282,8 +309,6 @@ class DetailsActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTabletLayout() = viewBinding.layoutBottom == null
|
||||
|
||||
private fun showBottomSheet(isVisible: Boolean) {
|
||||
val view = viewBinding.layoutBottom ?: return
|
||||
if (view.isVisible == isVisible) return
|
||||
@@ -297,7 +322,7 @@ class DetailsActivity :
|
||||
private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar {
|
||||
val sb = Snackbar.make(viewBinding.containerDetails, text, duration)
|
||||
if (viewBinding.layoutBottom?.isVisible == true) {
|
||||
sb.anchorView = viewBinding.headerChapters
|
||||
sb.anchorView = viewBinding.toolbarChapters
|
||||
}
|
||||
return sb
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.koitharu.kotatsu.core.util.ext.crossfade
|
||||
import org.koitharu.kotatsu.core.util.ext.drawableTop
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||
@@ -84,6 +83,7 @@ class DetailsFragment :
|
||||
binding.infoLayout.textViewSource.setOnClickListener(this)
|
||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
TitleScrollCoordinator(binding.textViewTitle).attach(binding.scrollView)
|
||||
viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||
@@ -271,7 +271,7 @@ class DetailsFragment :
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
requireViewBinding().root.updatePadding(
|
||||
bottom = (
|
||||
(activity as? NoModalBottomSheetOwner)?.bsHeader?.measureHeight()
|
||||
(activity as? NoModalBottomSheetOwner)?.getBottomSheetCollapsedHeight()
|
||||
?.plus(insets.bottom)?.plus(resources.resolveDp(16))
|
||||
)
|
||||
?: insets.bottom,
|
||||
@@ -284,6 +284,7 @@ class DetailsFragment :
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
tint = tagHighlighter.getTint(tag),
|
||||
icon = 0,
|
||||
data = tag,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
|
||||
@@ -18,11 +18,11 @@ import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||
|
||||
class DetailsMenuProvider(
|
||||
@@ -63,7 +63,7 @@ class DetailsMenuProvider(
|
||||
|
||||
R.id.action_favourite -> {
|
||||
viewModel.manga.value?.let {
|
||||
FavouriteCategoriesBottomSheet.show(activity.supportFragmentManager, it)
|
||||
FavouriteCategoriesSheet.show(activity.supportFragmentManager, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class DetailsMenuProvider(
|
||||
|
||||
R.id.action_scrobbling -> {
|
||||
viewModel.manga.value?.let {
|
||||
ScrobblingSelectorBottomSheet.show(activity.supportFragmentManager, it, null)
|
||||
ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,8 +171,9 @@ class DetailsViewModel @Inject constructor(
|
||||
history,
|
||||
selectedBranch,
|
||||
newChaptersCount,
|
||||
) { manga, history, branch, news ->
|
||||
mapChapters(manga?.remote, manga?.local, history, news, branch)
|
||||
bookmarks,
|
||||
) { manga, history, branch, news, bookmarks ->
|
||||
mapChapters(manga?.remote, manga?.local, history, news, branch, bookmarks)
|
||||
},
|
||||
isChaptersReversed,
|
||||
chaptersQuery,
|
||||
@@ -209,8 +210,8 @@ class DetailsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun removeBookmark(bookmark: Bookmark) {
|
||||
launchJob {
|
||||
bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId)
|
||||
launchJob(Dispatchers.Default) {
|
||||
bookmarksRepository.removeBookmark(bookmark)
|
||||
onShowToast.call(R.string.bookmark_removed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.koitharu.kotatsu.details.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.doOnLayout
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class TitleScrollCoordinator(
|
||||
private val titleView: TextView,
|
||||
) : NestedScrollView.OnScrollChangeListener {
|
||||
|
||||
private val location = IntArray(2)
|
||||
private var activityRef: WeakReference<AppCompatActivity>? = null
|
||||
|
||||
override fun onScrollChange(v: NestedScrollView, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int) {
|
||||
val actionBar = getActivity(v.context)?.supportActionBar ?: return
|
||||
titleView.getLocationOnScreen(location)
|
||||
var top = location[1] + titleView.height
|
||||
v.getLocationOnScreen(location)
|
||||
top -= location[1]
|
||||
actionBar.setDisplayShowTitleEnabled(top < 0)
|
||||
}
|
||||
|
||||
fun attach(scrollView: NestedScrollView) {
|
||||
scrollView.setOnScrollChangeListener(this)
|
||||
scrollView.doOnLayout {
|
||||
onScrollChange(scrollView, 0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActivity(context: Context): AppCompatActivity? {
|
||||
activityRef?.get()?.let {
|
||||
if (!it.isDestroyed) return it
|
||||
}
|
||||
val activity = context.findActivity() as? AppCompatActivity
|
||||
if (activity == null || activity.isDestroyed) {
|
||||
return null
|
||||
}
|
||||
activityRef = WeakReference(activity)
|
||||
return activity
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.koitharu.kotatsu.details.ui.adapter
|
||||
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.util.ext.drawableStart
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
||||
@@ -43,7 +45,13 @@ fun chapterListItemAD(
|
||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
|
||||
}
|
||||
}
|
||||
binding.imageViewBookmarked.isVisible = item.isBookmarked
|
||||
binding.imageViewDownloaded.isVisible = item.isDownloaded
|
||||
binding.imageViewNew.isVisible = item.isNew
|
||||
// binding.imageViewNew.isVisible = item.isNew
|
||||
binding.textViewTitle.drawableStart = if (item.isNew) {
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_new)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ class ChapterListItem(
|
||||
val isDownloaded: Boolean
|
||||
get() = hasFlag(FLAG_DOWNLOADED)
|
||||
|
||||
val isBookmarked: Boolean
|
||||
get() = hasFlag(FLAG_BOOKMARKED)
|
||||
|
||||
val isNew: Boolean
|
||||
get() = hasFlag(FLAG_NEW)
|
||||
|
||||
@@ -70,6 +73,7 @@ class ChapterListItem(
|
||||
const val FLAG_UNREAD = 2
|
||||
const val FLAG_CURRENT = 4
|
||||
const val FLAG_NEW = 8
|
||||
const val FLAG_BOOKMARKED = 16
|
||||
const val FLAG_DOWNLOADED = 32
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.details.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_BOOKMARKED
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
|
||||
@@ -11,11 +12,13 @@ fun MangaChapter.toListItem(
|
||||
isUnread: Boolean,
|
||||
isNew: Boolean,
|
||||
isDownloaded: Boolean,
|
||||
isBookmarked: Boolean,
|
||||
): ChapterListItem {
|
||||
var flags = 0
|
||||
if (isCurrent) flags = flags or FLAG_CURRENT
|
||||
if (isUnread) flags = flags or FLAG_UNREAD
|
||||
if (isNew) flags = flags or FLAG_NEW
|
||||
if (isBookmarked) flags = flags or FLAG_BOOKMARKED
|
||||
if (isDownloaded) flags = flags or FLAG_DOWNLOADED
|
||||
return ChapterListItem(
|
||||
chapter = this,
|
||||
|
||||
@@ -19,7 +19,7 @@ fun scrobblingInfoAD(
|
||||
{ layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
binding.root.setOnClickListener {
|
||||
ScrobblingInfoBottomSheet.show(fragmentManager, bindingAdapterPosition)
|
||||
ScrobblingInfoSheet.show(fragmentManager, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
bind {
|
||||
|
||||
@@ -17,7 +17,7 @@ import androidx.fragment.app.activityViewModels
|
||||
import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
@@ -30,12 +30,12 @@ import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.image.ui.ImageActivity
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorBottomSheet
|
||||
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ScrobblingInfoBottomSheet :
|
||||
BaseBottomSheet<SheetScrobblingBinding>(),
|
||||
class ScrobblingInfoSheet :
|
||||
BaseAdaptiveSheet<SheetScrobblingBinding>(),
|
||||
AdapterView.OnItemSelectedListener,
|
||||
RatingBar.OnRatingBarChangeListener,
|
||||
View.OnClickListener,
|
||||
@@ -74,7 +74,7 @@ class ScrobblingInfoBottomSheet :
|
||||
|
||||
menu = PopupMenu(binding.root.context, binding.buttonMenu).apply {
|
||||
inflate(R.menu.opt_scrobbling)
|
||||
setOnMenuItemClickListener(this@ScrobblingInfoBottomSheet)
|
||||
setOnMenuItemClickListener(this@ScrobblingInfoSheet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class ScrobblingInfoBottomSheet :
|
||||
R.id.action_edit -> {
|
||||
val manga = viewModel.manga.value ?: return false
|
||||
val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler
|
||||
ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService)
|
||||
ScrobblingSelectorSheet.show(parentFragmentManager, manga, scrobblerService)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ class ScrobblingInfoBottomSheet :
|
||||
private const val TAG = "ScrobblingInfoBottomSheet"
|
||||
private const val ARG_INDEX = "index"
|
||||
|
||||
fun show(fm: FragmentManager, index: Int) = ScrobblingInfoBottomSheet().withArgs(1) {
|
||||
fun show(fm: FragmentManager, index: Int) = ScrobblingInfoSheet().withArgs(1) {
|
||||
putInt(ARG_INDEX, index)
|
||||
}.show(fm, TAG)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class ExploreFragment :
|
||||
viewModel.onOpenManga.observeEvent(viewLifecycleOwner, ::onOpenManga)
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
|
||||
viewModel.onShowSuggestionsTip.observe(viewLifecycleOwner) {
|
||||
viewModel.onShowSuggestionsTip.observeEvent(viewLifecycleOwner) {
|
||||
showSuggestionsTip()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@ sealed interface ExploreItem : ListModel {
|
||||
|
||||
other as Buttons
|
||||
|
||||
if (isSuggestionsEnabled != other.isSuggestionsEnabled) return false
|
||||
|
||||
return true
|
||||
return isSuggestionsEnabled == other.isSuggestionsEnabled
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -40,9 +38,7 @@ sealed interface ExploreItem : ListModel {
|
||||
other as Header
|
||||
|
||||
if (titleResId != other.titleResId) return false
|
||||
if (isButtonVisible != other.isButtonVisible) return false
|
||||
|
||||
return true
|
||||
return isButtonVisible == other.isButtonVisible
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -64,9 +60,7 @@ sealed interface ExploreItem : ListModel {
|
||||
other as Source
|
||||
|
||||
if (source != other.source) return false
|
||||
if (isGrid != other.isGrid) return false
|
||||
|
||||
return true
|
||||
return isGrid == other.isGrid
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -76,7 +70,6 @@ sealed interface ExploreItem : ListModel {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
class EmptyHint(
|
||||
@DrawableRes icon: Int,
|
||||
@StringRes textPrimary: Int,
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
@Dao
|
||||
@@ -71,12 +72,12 @@ abstract class FavouritesDao {
|
||||
)
|
||||
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
||||
|
||||
suspend fun findCovers(categoryId: Long, order: SortOrder): List<String> {
|
||||
suspend fun findCovers(categoryId: Long, order: SortOrder): List<Cover> {
|
||||
val orderBy = getOrderBy(order)
|
||||
|
||||
@Language("RoomSql")
|
||||
val query = SimpleSQLiteQuery(
|
||||
"SELECT m.cover_url FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
|
||||
"SELECT m.cover_url AS url, m.source AS source FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
|
||||
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
|
||||
arrayOf<Any>(categoryId),
|
||||
)
|
||||
@@ -145,7 +146,7 @@ abstract class FavouritesDao {
|
||||
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
||||
|
||||
@RawQuery
|
||||
protected abstract suspend fun findCoversImpl(query: SupportSQLiteQuery): List<String>
|
||||
protected abstract suspend fun findCoversImpl(query: SupportSQLiteQuery): List<Cover>
|
||||
|
||||
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
|
||||
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||
import org.koitharu.kotatsu.favourites.data.toManga
|
||||
import org.koitharu.kotatsu.favourites.data.toMangaList
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||
@@ -66,11 +67,11 @@ class FavouritesRepository @Inject constructor(
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<String>>> {
|
||||
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<Cover>>> {
|
||||
return db.favouriteCategoriesDao.observeAll()
|
||||
.map {
|
||||
db.withTransaction {
|
||||
val res = LinkedHashMap<FavouriteCategory, List<String>>()
|
||||
val res = LinkedHashMap<FavouriteCategory, List<Cover>>()
|
||||
for (entity in it) {
|
||||
val cat = entity.toFavouriteCategory()
|
||||
res[cat] = db.favouritesDao.findCovers(
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.koitharu.kotatsu.favourites.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
class Cover(
|
||||
val url: String,
|
||||
val source: String,
|
||||
) {
|
||||
|
||||
val mangaSource: MangaSource?
|
||||
get() = if (source.isEmpty()) null else MangaSource.values().find { it.name == source }
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Cover
|
||||
|
||||
if (url != other.url) return false
|
||||
return source == other.source
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = url.hashCode()
|
||||
result = 31 * result + source.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Cover(url='$url', source=$source)"
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,12 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.animatorDurationScale
|
||||
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||
import org.koitharu.kotatsu.core.util.ext.source
|
||||
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
@@ -53,10 +54,7 @@ fun categoryAD(
|
||||
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
|
||||
val fallback = ColorDrawable(Color.TRANSPARENT)
|
||||
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
|
||||
val crossFadeDuration = (
|
||||
context.resources.getInteger(R.integer.config_defaultAnimTime) *
|
||||
context.animatorDurationScale
|
||||
).toInt()
|
||||
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
|
||||
itemView.setOnClickListener(eventListener)
|
||||
itemView.setOnLongClickListener(eventListener)
|
||||
itemView.setOnTouchListener(eventListener)
|
||||
@@ -77,9 +75,11 @@ fun categoryAD(
|
||||
)
|
||||
}
|
||||
repeat(coverViews.size) { i ->
|
||||
coverViews[i].newImageRequest(lifecycleOwner, item.covers.getOrNull(i))?.run {
|
||||
val cover = item.covers.getOrNull(i)
|
||||
coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run {
|
||||
placeholder(R.drawable.ic_placeholder)
|
||||
fallback(fallback)
|
||||
source(cover?.mangaSource)
|
||||
crossfade(crossFadeDuration * (i + 1))
|
||||
error(R.drawable.ic_error_placeholder)
|
||||
allowRgb565(true)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
||||
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class CategoryListModel(
|
||||
val mangaCount: Int,
|
||||
val covers: List<String>,
|
||||
val covers: List<Cover>,
|
||||
val category: FavouriteCategory,
|
||||
val isReorderMode: Boolean,
|
||||
) : ListModel {
|
||||
@@ -21,9 +22,7 @@ class CategoryListModel(
|
||||
if (covers != other.covers) return false
|
||||
if (category.id != other.category.id) return false
|
||||
if (category.title != other.category.title) return false
|
||||
if (category.order != other.category.order) return false
|
||||
|
||||
return true
|
||||
return category.order == other.category.order
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -35,4 +34,4 @@ class CategoryListModel(
|
||||
result = 31 * result + category.order.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,34 +2,29 @@ package org.koitharu.kotatsu.favourites.ui.categories.select
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.adapter.MangaCategoriesAdapter
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FavouriteCategoriesBottomSheet :
|
||||
BaseBottomSheet<SheetFavoriteCategoriesBinding>(),
|
||||
OnListItemClickListener<MangaCategoryItem>,
|
||||
View.OnClickListener,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
class FavouriteCategoriesSheet :
|
||||
BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(),
|
||||
OnListItemClickListener<MangaCategoryItem> {
|
||||
|
||||
private val viewModel: MangaCategoriesViewModel by viewModels()
|
||||
|
||||
@@ -40,13 +35,13 @@ class FavouriteCategoriesBottomSheet :
|
||||
container: ViewGroup?,
|
||||
) = SheetFavoriteCategoriesBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFavoriteCategoriesBinding, savedInstanceState: Bundle?) {
|
||||
override fun onViewBindingCreated(
|
||||
binding: SheetFavoriteCategoriesBinding,
|
||||
savedInstanceState: Bundle?,
|
||||
) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
adapter = MangaCategoriesAdapter(this)
|
||||
binding.recyclerViewCategories.adapter = adapter
|
||||
binding.buttonDone.setOnClickListener(this)
|
||||
binding.headerBar.toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
|
||||
}
|
||||
@@ -56,25 +51,11 @@ class FavouriteCategoriesBottomSheet :
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_done -> dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_create -> startActivity(FavouritesCategoryEditActivity.newIntent(requireContext()))
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MangaCategoryItem, view: View) {
|
||||
viewModel.setChecked(item.id, !item.isChecked)
|
||||
}
|
||||
|
||||
private fun onContentChanged(categories: List<MangaCategoryItem>) {
|
||||
private fun onContentChanged(categories: List<ListModel>) {
|
||||
adapter?.items = categories
|
||||
}
|
||||
|
||||
@@ -89,11 +70,17 @@ class FavouriteCategoriesBottomSheet :
|
||||
|
||||
fun show(fm: FragmentManager, manga: Manga) = Companion.show(fm, listOf(manga))
|
||||
|
||||
fun show(fm: FragmentManager, manga: Collection<Manga>) = FavouriteCategoriesBottomSheet().withArgs(1) {
|
||||
putParcelableArrayList(
|
||||
KEY_MANGA_LIST,
|
||||
manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withChapters = false) },
|
||||
)
|
||||
}.show(fm, TAG)
|
||||
fun show(fm: FragmentManager, manga: Collection<Manga>) =
|
||||
FavouriteCategoriesSheet().withArgs(1) {
|
||||
putParcelableArrayList(
|
||||
KEY_MANGA_LIST,
|
||||
manga.mapTo(ArrayList(manga.size)) {
|
||||
ParcelableManga(
|
||||
it,
|
||||
withChapters = false,
|
||||
)
|
||||
},
|
||||
)
|
||||
}.show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
@@ -12,8 +13,10 @@ import org.koitharu.kotatsu.core.model.ids
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet.Companion.KEY_MANGA_LIST
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet.Companion.KEY_MANGA_LIST
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -23,17 +26,21 @@ class MangaCategoriesViewModel @Inject constructor(
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val manga = requireNotNull(savedStateHandle.get<List<ParcelableManga>>(KEY_MANGA_LIST)).map { it.manga }
|
||||
private val header = CategoriesHeaderItem()
|
||||
|
||||
val content = combine(
|
||||
val content: StateFlow<List<ListModel>> = combine(
|
||||
favouritesRepository.observeCategories(),
|
||||
observeCategoriesIds(),
|
||||
) { all, checked ->
|
||||
all.map {
|
||||
MangaCategoryItem(
|
||||
id = it.id,
|
||||
name = it.title,
|
||||
isChecked = it.id in checked,
|
||||
)
|
||||
buildList(all.size + 1) {
|
||||
add(header)
|
||||
all.mapTo(this) {
|
||||
MangaCategoryItem(
|
||||
id = it.id,
|
||||
name = it.title,
|
||||
isChecked = it.id in checked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
|
||||
|
||||
import android.view.View
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.ItemCategoriesHeaderBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun categoriesHeaderAD() = adapterDelegateViewBinding<CategoriesHeaderItem, ListModel, ItemCategoriesHeaderBinding>(
|
||||
{ inflater, parent -> ItemCategoriesHeaderBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
val onClickListener = View.OnClickListener { v ->
|
||||
val intent = when (v.id) {
|
||||
R.id.button_create -> FavouritesCategoryEditActivity.newIntent(v.context)
|
||||
R.id.button_manage -> FavouriteCategoriesActivity.newIntent(v.context)
|
||||
else -> return@OnClickListener
|
||||
}
|
||||
v.context.startActivity(intent)
|
||||
}
|
||||
|
||||
binding.buttonCreate.setOnClickListener(onClickListener)
|
||||
binding.buttonManage.setOnClickListener(onClickListener)
|
||||
}
|
||||
@@ -3,32 +3,39 @@ package org.koitharu.kotatsu.favourites.ui.categories.select.adapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.CategoriesHeaderItem
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class MangaCategoriesAdapter(
|
||||
clickListener: OnListItemClickListener<MangaCategoryItem>
|
||||
) : AsyncListDifferDelegationAdapter<MangaCategoryItem>(DiffCallback()) {
|
||||
clickListener: OnListItemClickListener<MangaCategoryItem>,
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
|
||||
.addDelegate(categoriesHeaderAD())
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<MangaCategoryItem>() {
|
||||
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: MangaCategoryItem,
|
||||
newItem: MangaCategoryItem
|
||||
): Boolean = oldItem.id == newItem.id
|
||||
oldItem: ListModel,
|
||||
newItem: ListModel,
|
||||
): Boolean = when {
|
||||
oldItem is MangaCategoryItem && newItem is MangaCategoryItem -> oldItem.id == newItem.id
|
||||
oldItem is CategoriesHeaderItem && newItem is CategoriesHeaderItem -> oldItem == newItem
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: MangaCategoryItem,
|
||||
newItem: MangaCategoryItem
|
||||
oldItem: ListModel,
|
||||
newItem: ListModel,
|
||||
): Boolean = oldItem == newItem
|
||||
|
||||
override fun getChangePayload(
|
||||
oldItem: MangaCategoryItem,
|
||||
newItem: MangaCategoryItem
|
||||
oldItem: ListModel,
|
||||
newItem: ListModel,
|
||||
): Any? {
|
||||
if (oldItem.isChecked != newItem.isChecked) {
|
||||
if (oldItem is MangaCategoryItem && newItem is MangaCategoryItem && oldItem.isChecked != newItem.isChecked) {
|
||||
return newItem.isChecked
|
||||
}
|
||||
return super.getChangePayload(oldItem, newItem)
|
||||
|
||||
@@ -4,10 +4,11 @@ import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun mangaCategoryAD(
|
||||
clickListener: OnListItemClickListener<MangaCategoryItem>
|
||||
) = adapterDelegateViewBinding<MangaCategoryItem, MangaCategoryItem, ItemCheckableNewBinding>(
|
||||
clickListener: OnListItemClickListener<MangaCategoryItem>,
|
||||
) = adapterDelegateViewBinding<MangaCategoryItem, ListModel, ItemCheckableNewBinding>(
|
||||
{ inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class CategoriesHeaderItem : ListModel {
|
||||
|
||||
override fun equals(other: Any?): Boolean = other?.javaClass == CategoriesHeaderItem::class.java
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class MangaCategoryItem(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val isChecked: Boolean
|
||||
)
|
||||
val isChecked: Boolean,
|
||||
) : ListModel
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.list.ui.adapter.listSimpleHeaderAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class FilterAdapter(
|
||||
listener: OnFilterChangedListener,
|
||||
listListener: ListListener<ListModel>,
|
||||
) : AsyncListDifferDelegationAdapter<ListModel>(FilterDiffCallback()), FastScroller.SectionIndexer {
|
||||
|
||||
init {
|
||||
delegatesManager
|
||||
.addDelegate(filterSortDelegate(listener))
|
||||
.addDelegate(filterTagDelegate(listener))
|
||||
.addDelegate(listSimpleHeaderAD())
|
||||
.addDelegate(loadingStateAD())
|
||||
.addDelegate(loadingFooterAD())
|
||||
.addDelegate(filterErrorDelegate())
|
||||
differ.addListListener(listListener)
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val list = items
|
||||
for (i in (0..position).reversed()) {
|
||||
val item = list.getOrNull(i) ?: continue
|
||||
if (item is FilterItem.Tag) {
|
||||
return item.tag.title.firstOrNull()?.toString()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val ITEM_TYPE_HEADER = 0
|
||||
const val ITEM_TYPE_SORT = 1
|
||||
const val ITEM_TYPE_TAG = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import android.widget.TextView
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||
import org.koitharu.kotatsu.core.util.ext.setChecked
|
||||
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemCheckableSingleBinding
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
fun filterSortDelegate(
|
||||
listener: OnFilterChangedListener,
|
||||
) = adapterDelegateViewBinding<FilterItem.Sort, ListModel, ItemCheckableSingleBinding>(
|
||||
{ layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onSortItemClick(item)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
binding.root.setText(item.order.titleRes)
|
||||
binding.root.setChecked(item.isSelected, payloads.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun filterTagDelegate(
|
||||
listener: OnFilterChangedListener,
|
||||
) = adapterDelegateViewBinding<FilterItem.Tag, ListModel, ItemCheckableMultipleBinding>(
|
||||
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onTagItemClick(item)
|
||||
}
|
||||
|
||||
bind { payloads ->
|
||||
binding.root.text = item.tag.title
|
||||
binding.root.setChecked(item.isChecked, payloads.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, ListModel>(R.layout.item_sources_empty) {
|
||||
|
||||
bind {
|
||||
(itemView as TextView).setText(item.textResId)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.koitharu.kotatsu.list.ui.filter
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.ViewModelLifecycle
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -17,21 +19,40 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.require
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.text.Collator
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import java.util.TreeSet
|
||||
import javax.inject.Inject
|
||||
|
||||
class FilterCoordinator(
|
||||
private val repository: RemoteMangaRepository,
|
||||
@ViewModelScoped
|
||||
class FilterCoordinator @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
dataRepository: MangaDataRepository,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) : OnFilterChangedListener {
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
lifecycle: ViewModelLifecycle,
|
||||
) : FilterOwner {
|
||||
|
||||
private val coroutineScope = lifecycle.lifecycleScope
|
||||
private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE))
|
||||
private val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet()))
|
||||
private var searchQuery = MutableStateFlow("")
|
||||
private val localTags = SuspendLazy {
|
||||
@@ -39,13 +60,23 @@ class FilterCoordinator(
|
||||
}
|
||||
private var availableTagsDeferred = loadTagsAsync()
|
||||
|
||||
val items: StateFlow<List<FilterItem>> = getItemsFlow()
|
||||
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(FilterItem.Loading))
|
||||
override val filterItems: StateFlow<List<ListModel>> = getItemsFlow()
|
||||
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
|
||||
|
||||
override val header: StateFlow<FilterHeaderModel> = getHeaderFlow().stateIn(
|
||||
scope = coroutineScope + Dispatchers.Default,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = FilterHeaderModel(emptyList(), repository.defaultSortOrder, false),
|
||||
)
|
||||
|
||||
init {
|
||||
observeState()
|
||||
}
|
||||
|
||||
override fun applyFilter(tags: Set<MangaTag>) {
|
||||
setTags(tags)
|
||||
}
|
||||
|
||||
override fun onSortItemClick(item: FilterItem.Sort) {
|
||||
currentState.update { oldValue ->
|
||||
FilterState(item.order, oldValue.tags)
|
||||
@@ -91,6 +122,14 @@ class FilterCoordinator(
|
||||
searchQuery.value = query
|
||||
}
|
||||
|
||||
private fun getHeaderFlow() = combine(
|
||||
observeState(),
|
||||
observeAvailableTags(),
|
||||
) { state, available ->
|
||||
val chips = createChipsList(state, available.orEmpty())
|
||||
FilterHeaderModel(chips, state.sortOrder, state.tags.isNotEmpty())
|
||||
}
|
||||
|
||||
private fun getItemsFlow() = combine(
|
||||
getTagsAsFlow(),
|
||||
currentState,
|
||||
@@ -110,24 +149,66 @@ class FilterCoordinator(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createChipsList(
|
||||
filterState: FilterState,
|
||||
availableTags: Set<MangaTag>,
|
||||
): List<ChipsView.ChipModel> {
|
||||
val selectedTags = filterState.tags.toMutableSet()
|
||||
var tags = searchRepository.getTagsSuggestion("", 6, repository.source)
|
||||
if (tags.isEmpty()) {
|
||||
tags = availableTags.take(6)
|
||||
}
|
||||
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
val result = LinkedList<ChipsView.ChipModel>()
|
||||
for (tag in tags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = selectedTags.remove(tag),
|
||||
data = tag,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
result.addFirst(model)
|
||||
} else {
|
||||
result.addLast(model)
|
||||
}
|
||||
}
|
||||
for (tag in selectedTags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
icon = 0,
|
||||
isCheckable = true,
|
||||
isChecked = true,
|
||||
data = tag,
|
||||
)
|
||||
result.addFirst(model)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun buildFilterList(
|
||||
allTags: TagsWrapper,
|
||||
state: FilterState,
|
||||
query: String,
|
||||
): List<FilterItem> {
|
||||
): List<ListModel> {
|
||||
val sortOrders = repository.sortOrders.sortedBy { it.ordinal }
|
||||
val tags = mergeTags(state.tags, allTags.tags).toList()
|
||||
val list = ArrayList<FilterItem>(tags.size + sortOrders.size + 3)
|
||||
val list = ArrayList<ListModel>(tags.size + sortOrders.size + 3)
|
||||
if (query.isEmpty()) {
|
||||
if (sortOrders.isNotEmpty()) {
|
||||
list.add(FilterItem.Header(R.string.sort_order, 0))
|
||||
list.add(ListHeader(R.string.sort_order, 0, null))
|
||||
sortOrders.mapTo(list) {
|
||||
FilterItem.Sort(it, isSelected = it == state.sortOrder)
|
||||
}
|
||||
}
|
||||
if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) {
|
||||
list.add(FilterItem.Header(R.string.genres, state.tags.size))
|
||||
list.add(ListHeader(R.string.genres, 0, null))
|
||||
tags.mapTo(list) {
|
||||
FilterItem.Tag(it, isChecked = it in state.tags)
|
||||
}
|
||||
@@ -135,7 +216,7 @@ class FilterCoordinator(
|
||||
if (allTags.isError) {
|
||||
list.add(FilterItem.Error(R.string.filter_load_error))
|
||||
} else if (allTags.isLoading) {
|
||||
list.add(FilterItem.Loading)
|
||||
list.add(LoadingFooter())
|
||||
}
|
||||
} else {
|
||||
tags.mapNotNullTo(list) {
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class FilterDiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return when {
|
||||
oldItem === newItem -> true
|
||||
oldItem.javaClass != newItem.javaClass -> false
|
||||
oldItem is ListHeader && newItem is ListHeader -> {
|
||||
oldItem == newItem
|
||||
}
|
||||
|
||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||
oldItem.tag == newItem.tag
|
||||
}
|
||||
|
||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||
oldItem.order == newItem.order
|
||||
}
|
||||
|
||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
|
||||
oldItem.textResId == newItem.textResId
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
||||
val hasPayload = when {
|
||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||
oldItem.isChecked != newItem.isChecked
|
||||
}
|
||||
|
||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||
oldItem.isSelected != newItem.isSelected
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.chip.Chip
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener {
|
||||
|
||||
private val owner by lazy(LazyThreadSafetyMode.NONE) {
|
||||
FilterOwner.from(requireActivity())
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding {
|
||||
return FragmentFilterHeaderBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
owner.header.observe(viewLifecycleOwner, ::onDataChanged)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) = Unit
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val tag = data as? MangaTag
|
||||
if (tag == null) {
|
||||
FilterSheetFragment.show(parentFragmentManager)
|
||||
} else {
|
||||
owner.onTagItemClick(FilterItem.Tag(tag, !chip.isChecked))
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDataChanged(header: FilterHeaderModel) {
|
||||
val binding = viewBinding ?: return
|
||||
val chips = header.chips
|
||||
if (chips.isEmpty()) {
|
||||
binding.chipsTags.setChips(emptyList())
|
||||
binding.root.isVisible = false
|
||||
return
|
||||
}
|
||||
if (binding.root.context.isAnimationsEnabled) {
|
||||
binding.scrollView.smoothScrollTo(0, 0)
|
||||
} else {
|
||||
binding.scrollView.scrollTo(0, 0)
|
||||
}
|
||||
binding.chipsTags.setChips(header.chips + moreTagsChip())
|
||||
binding.root.isVisible = true
|
||||
}
|
||||
|
||||
private fun moreTagsChip() = ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = getString(R.string.more),
|
||||
icon = materialR.drawable.abc_ic_menu_overflow_material,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.values
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
|
||||
interface FilterOwner : OnFilterChangedListener {
|
||||
|
||||
val filterItems: StateFlow<List<ListModel>>
|
||||
|
||||
val header: StateFlow<FilterHeaderModel>
|
||||
|
||||
fun applyFilter(tags: Set<MangaTag>)
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(activity: FragmentActivity): FilterOwner {
|
||||
for (f in activity.supportFragmentManager.fragments) {
|
||||
return find(f) ?: continue
|
||||
}
|
||||
error("Cannot find FilterOwner")
|
||||
}
|
||||
|
||||
fun find(fragment: Fragment): FilterOwner? {
|
||||
return fragment.viewModelStore.values.firstNotNullOfOrNull { it as? FilterOwner }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
|
||||
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class FilterSheetFragment :
|
||||
BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
AdaptiveSheetCallback,
|
||||
AsyncListDiffer.ListListener<ListModel> {
|
||||
|
||||
private val owner by lazy(LazyThreadSafetyMode.NONE) {
|
||||
FilterOwner.from(requireActivity())
|
||||
}
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||
return SheetFilterBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
addSheetCallback(this)
|
||||
val adapter = FilterAdapter(owner, this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
owner.filterItems.observe(viewLifecycleOwner, adapter::setItems)
|
||||
|
||||
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.recyclerView.scrollIndicators = 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged(previousList: MutableList<ListModel>, currentList: MutableList<ListModel>) {
|
||||
if (currentList.size > previousList.size && view != null) {
|
||||
(requireViewBinding().recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FilterBottomSheet"
|
||||
|
||||
fun show(fm: FragmentManager) = FilterSheetFragment().show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.koitharu.kotatsu.list.ui.filter
|
||||
package org.koitharu.kotatsu.filter.ui
|
||||
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterItem
|
||||
|
||||
interface OnFilterChangedListener {
|
||||
|
||||
fun onSortItemClick(item: FilterItem.Sort)
|
||||
|
||||
fun onTagItemClick(item: FilterItem.Tag)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,23 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
package org.koitharu.kotatsu.filter.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
class ListHeader2(
|
||||
class FilterHeaderModel(
|
||||
val chips: Collection<ChipsView.ChipModel>,
|
||||
val sortOrder: SortOrder?,
|
||||
val hasSelectedTags: Boolean,
|
||||
) : ListModel {
|
||||
|
||||
val textSummary: String
|
||||
get() = chips.mapNotNull { if (it.isChecked) it.title else null }.joinToString()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ListHeader2
|
||||
other as FilterHeaderModel
|
||||
|
||||
if (chips != other.chips) return false
|
||||
return sortOrder == other.sortOrder
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.koitharu.kotatsu.filter.ui.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
sealed interface FilterItem : ListModel {
|
||||
|
||||
class Sort(
|
||||
val order: SortOrder,
|
||||
val isSelected: Boolean,
|
||||
) : FilterItem {
|
||||
|
||||
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(
|
||||
val tag: MangaTag,
|
||||
val isChecked: Boolean,
|
||||
) : FilterItem {
|
||||
|
||||
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(
|
||||
@StringRes val textResId: Int,
|
||||
) : FilterItem {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.koitharu.kotatsu.list.ui.filter
|
||||
package org.koitharu.kotatsu.filter.ui.model
|
||||
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
@@ -15,9 +15,7 @@ class FilterState(
|
||||
other as FilterState
|
||||
|
||||
if (sortOrder != other.sortOrder) return false
|
||||
if (tags != other.tags) return false
|
||||
|
||||
return true
|
||||
return tags == other.tags
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -25,4 +23,4 @@ class FilterState(
|
||||
result = 31 * result + tags.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.koitharu.kotatsu.list.domain
|
||||
|
||||
import dagger.Reusable
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class ListExtraProviderImpl @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
) : ListExtraProvider {
|
||||
|
||||
override suspend fun getCounter(mangaId: Long): Int {
|
||||
return if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getProgress(mangaId: Long): Float {
|
||||
return if (settings.isReadingIndicatorsEnabled) {
|
||||
historyRepository.getProgress(mangaId)
|
||||
} else {
|
||||
PROGRESS_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
|
||||
import org.koitharu.kotatsu.databinding.DialogListModeBinding
|
||||
@@ -19,7 +19,7 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ListModeBottomSheet :
|
||||
BaseBottomSheet<DialogListModeBinding>(),
|
||||
BaseAdaptiveSheet<DialogListModeBinding>(),
|
||||
Slider.OnChangeListener,
|
||||
MaterialButtonToggleGroup.OnButtonCheckedListener {
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||
@@ -296,7 +296,7 @@ abstract class MangaListFragment :
|
||||
}
|
||||
|
||||
R.id.action_favourite -> {
|
||||
FavouriteCategoriesBottomSheet.show(childFragmentManager, selectedItems)
|
||||
FavouriteCategoriesSheet.show(childFragmentManager, selectedItems)
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemHeader2Binding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
||||
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
|
||||
@Deprecated("")
|
||||
fun listHeader2AD(
|
||||
listener: MangaListListener,
|
||||
) = adapterDelegateViewBinding<ListHeader2, ListModel, ItemHeader2Binding>(
|
||||
{ layoutInflater, parent -> ItemHeader2Binding.inflate(layoutInflater, parent, false) },
|
||||
) = adapterDelegateViewBinding<FilterHeaderModel, ListModel, FragmentFilterHeaderBinding>(
|
||||
{ layoutInflater, parent -> FragmentFilterHeaderBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
var ignoreChecking = false
|
||||
binding.textViewFilter.setOnClickListener {
|
||||
listener.onFilterClick(it)
|
||||
}
|
||||
binding.chipsTags.setOnCheckedStateChangeListener { _, _ ->
|
||||
if (!ignoreChecking) {
|
||||
listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java))
|
||||
@@ -36,6 +32,5 @@ fun listHeader2AD(
|
||||
ignoreChecking = true
|
||||
binding.chipsTags.setChips(item.chips) // TODO use recyclerview
|
||||
ignoreChecking = false
|
||||
binding.textViewFilter.setTextAndVisible(item.sortOrder?.titleRes ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.ui.adapter
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemHeaderSingleBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
@@ -22,3 +23,12 @@ fun listHeaderAD(
|
||||
binding.buttonMore.setTextAndVisible(item.buttonTextRes)
|
||||
}
|
||||
}
|
||||
|
||||
fun listSimpleHeaderAD() = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderSingleBinding>(
|
||||
{ inflater, parent -> ItemHeaderSingleBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.text = item.getText(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||
@@ -82,7 +82,7 @@ open class MangaListAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
is ListHeader2 -> Unit
|
||||
is FilterHeaderModel -> Unit
|
||||
else -> super.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.koitharu.kotatsu.list.ui.filter
|
||||
|
||||
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
|
||||
class FilterAdapter(
|
||||
listener: OnFilterChangedListener,
|
||||
listListener: ListListener<FilterItem>,
|
||||
) : AsyncListDifferDelegationAdapter<FilterItem>(
|
||||
FilterDiffCallback(),
|
||||
filterSortDelegate(listener),
|
||||
filterTagDelegate(listener),
|
||||
filterHeaderDelegate(),
|
||||
filterLoadingDelegate(),
|
||||
filterErrorDelegate(),
|
||||
) {
|
||||
|
||||
init {
|
||||
differ.addListListener(listListener)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.koitharu.kotatsu.list.ui.filter
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemFilterHeaderBinding
|
||||
|
||||
fun filterSortDelegate(
|
||||
listener: OnFilterChangedListener,
|
||||
) = adapterDelegateViewBinding<FilterItem.Sort, FilterItem, ItemCheckableNewBinding>(
|
||||
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onSortItemClick(item)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.root.setText(item.order.titleRes)
|
||||
binding.root.isChecked = item.isSelected
|
||||
}
|
||||
}
|
||||
|
||||
fun filterTagDelegate(
|
||||
listener: OnFilterChangedListener,
|
||||
) = adapterDelegateViewBinding<FilterItem.Tag, FilterItem, ItemCheckableNewBinding>(
|
||||
{ layoutInflater, parent -> ItemCheckableNewBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
itemView.setOnClickListener {
|
||||
listener.onTagItemClick(item)
|
||||
}
|
||||
|
||||
bind {
|
||||
binding.root.text = item.tag.title
|
||||
binding.root.isChecked = item.isChecked
|
||||
}
|
||||
}
|
||||
|
||||
fun filterHeaderDelegate() = adapterDelegateViewBinding<FilterItem.Header, FilterItem, ItemFilterHeaderBinding>(
|
||||
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
bind {
|
||||
binding.textViewTitle.setText(item.titleResId)
|
||||
binding.badge.isVisible = if (item.counter == 0) {
|
||||
false
|
||||
} else {
|
||||
binding.badge.text = item.counter.toString()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filterLoadingDelegate() = adapterDelegate<FilterItem.Loading, FilterItem>(R.layout.item_loading_footer) {}
|
||||
|
||||
fun filterErrorDelegate() = adapterDelegate<FilterItem.Error, FilterItem>(R.layout.item_sources_empty) {
|
||||
|
||||
bind {
|
||||
(itemView as TextView).setText(item.textResId)
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.koitharu.kotatsu.list.ui.filter
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.core.ui.util.CollapseActionViewCallback
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.parentFragmentViewModels
|
||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||
|
||||
class FilterBottomSheet :
|
||||
BaseBottomSheet<SheetFilterBinding>(),
|
||||
MenuItem.OnActionExpandListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
AsyncListDiffer.ListListener<FilterItem> {
|
||||
|
||||
private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
|
||||
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||
return SheetFilterBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
val adapter = FilterAdapter(viewModel, this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
viewModel.filterItems.observe(viewLifecycleOwner, adapter::setItems)
|
||||
initOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
collapsibleActionViewCallback = null
|
||||
}
|
||||
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
setExpanded(isExpanded = true, isLocked = true)
|
||||
collapsibleActionViewCallback?.onMenuItemActionExpand(item)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
val searchView = (item.actionView as? SearchView) ?: return false
|
||||
searchView.setQuery("", false)
|
||||
searchView.post { setExpanded(isExpanded = false, isLocked = false) }
|
||||
collapsibleActionViewCallback?.onMenuItemActionCollapse(item)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
viewModel.filterSearch(newText?.trim().orEmpty())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged(previousList: MutableList<FilterItem>, currentList: MutableList<FilterItem>) {
|
||||
if (currentList.size > previousList.size && view != null) {
|
||||
(requireViewBinding().recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initOptionsMenu() {
|
||||
requireViewBinding().headerBar.inflateMenu(R.menu.opt_filter)
|
||||
val searchMenuItem = requireViewBinding().headerBar.menu.findItem(R.id.action_search)
|
||||
searchMenuItem.setOnActionExpandListener(this)
|
||||
val searchView = searchMenuItem.actionView as SearchView
|
||||
searchView.setOnQueryTextListener(this)
|
||||
searchView.setIconifiedByDefault(false)
|
||||
searchView.queryHint = searchMenuItem.title
|
||||
collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also {
|
||||
onBackPressedDispatcher.addCallback(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "FilterBottomSheet"
|
||||
|
||||
fun show(fm: FragmentManager) = FilterBottomSheet().show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.koitharu.kotatsu.list.ui.filter
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
||||
class FilterDiffCallback : DiffUtil.ItemCallback<FilterItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
||||
return when {
|
||||
oldItem === newItem -> true
|
||||
oldItem.javaClass != newItem.javaClass -> false
|
||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
||||
oldItem.titleResId == newItem.titleResId
|
||||
}
|
||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||
oldItem.tag == newItem.tag
|
||||
}
|
||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||
oldItem.order == newItem.order
|
||||
}
|
||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> {
|
||||
oldItem.textResId == newItem.textResId
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: FilterItem, newItem: FilterItem): Boolean {
|
||||
return when {
|
||||
oldItem == FilterItem.Loading && newItem == FilterItem.Loading -> true
|
||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
||||
oldItem.counter == newItem.counter
|
||||
}
|
||||
oldItem is FilterItem.Error && newItem is FilterItem.Error -> true
|
||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||
oldItem.isChecked == newItem.isChecked
|
||||
}
|
||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||
oldItem.isSelected == newItem.isSelected
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: FilterItem, newItem: FilterItem): Any? {
|
||||
val hasPayload = when {
|
||||
oldItem is FilterItem.Tag && newItem is FilterItem.Tag -> {
|
||||
oldItem.isChecked != newItem.isChecked
|
||||
}
|
||||
oldItem is FilterItem.Sort && newItem is FilterItem.Sort -> {
|
||||
oldItem.isSelected != newItem.isSelected
|
||||
}
|
||||
oldItem is FilterItem.Header && newItem is FilterItem.Header -> {
|
||||
oldItem.counter != newItem.counter
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
return if (hasPayload) Unit else super.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.koitharu.kotatsu.list.ui.filter
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
|
||||
sealed interface FilterItem {
|
||||
|
||||
class Header(
|
||||
@StringRes val titleResId: Int,
|
||||
val counter: Int,
|
||||
) : FilterItem
|
||||
|
||||
class Sort(
|
||||
val order: SortOrder,
|
||||
val isSelected: Boolean,
|
||||
) : FilterItem
|
||||
|
||||
class Tag(
|
||||
val tag: MangaTag,
|
||||
val isChecked: Boolean,
|
||||
) : FilterItem
|
||||
|
||||
object Loading : FilterItem
|
||||
|
||||
class Error(
|
||||
@StringRes val textResId: Int,
|
||||
) : FilterItem
|
||||
}
|
||||
@@ -43,6 +43,7 @@ fun Manga.toListDetailedModel(
|
||||
ChipsView.ChipModel(
|
||||
tint = tagHighlighter?.getTint(it) ?: 0,
|
||||
title = it.title,
|
||||
icon = 0,
|
||||
isCheckable = false,
|
||||
isChecked = false,
|
||||
data = it,
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.core.model.isLocal
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.CompositeMutex
|
||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
||||
@@ -28,6 +29,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||
import java.io.File
|
||||
import java.util.EnumSet
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -37,11 +39,20 @@ private const val MAX_PARALLELISM = 4
|
||||
class LocalMangaRepository @Inject constructor(
|
||||
private val storageManager: LocalStorageManager,
|
||||
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
||||
private val settings: AppSettings,
|
||||
) : MangaRepository {
|
||||
|
||||
override val source = MangaSource.LOCAL
|
||||
private val locks = CompositeMutex<Long>()
|
||||
|
||||
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST)
|
||||
|
||||
override var defaultSortOrder: SortOrder
|
||||
get() = settings.localListOrder
|
||||
set(value) {
|
||||
settings.localListOrder = value
|
||||
}
|
||||
|
||||
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
||||
if (offset > 0) {
|
||||
return emptyList()
|
||||
@@ -137,8 +148,6 @@ class LocalMangaRepository @Inject constructor(
|
||||
}.firstOrNull()?.getManga()
|
||||
}
|
||||
|
||||
override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING)
|
||||
|
||||
override suspend fun getPageUrl(page: MangaPage) = page.url
|
||||
|
||||
override suspend fun getTags() = emptySet<MangaTag>()
|
||||
|
||||
@@ -54,9 +54,10 @@ class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) {
|
||||
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
||||
try {
|
||||
file.sink(append = false).buffer().use {
|
||||
val bytes = file.sink(append = false).buffer().use {
|
||||
it.writeAllCancellable(source)
|
||||
}
|
||||
check(bytes != 0L) { "No data has been written" }
|
||||
lruCache.get().put(url, file)
|
||||
} finally {
|
||||
file.delete()
|
||||
|
||||
@@ -130,7 +130,6 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
||||
}
|
||||
val cbz = root.listFilesRecursive(CbzFilter()).firstOrNull() ?: return null
|
||||
return ZipFile(cbz).use { zip ->
|
||||
val filter = ImageFileFilter()
|
||||
zip.entries().asSequence()
|
||||
.firstOrNull { x -> !x.isDirectory && filter.accept(x) }
|
||||
?.let { entry -> zipUri(cbz, entry.name) }
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -16,11 +15,14 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
|
||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
||||
|
||||
class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener {
|
||||
class LocalListFragment : MangaListFragment() {
|
||||
|
||||
override val viewModel by viewModels<LocalListViewModel>()
|
||||
|
||||
@@ -35,11 +37,7 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
|
||||
}
|
||||
|
||||
override fun onFilterClick(view: View?) {
|
||||
super.onFilterClick(view)
|
||||
val menu = PopupMenu(requireContext(), view ?: requireViewBinding().recyclerView)
|
||||
menu.inflate(R.menu.popup_order)
|
||||
menu.setOnMenuItemClickListener(this)
|
||||
menu.show()
|
||||
FilterSheetFragment.show(childFragmentManager)
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() = Unit
|
||||
@@ -67,17 +65,6 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
val order = when (item.itemId) {
|
||||
R.id.action_order_new -> SortOrder.NEWEST
|
||||
R.id.action_order_abs -> SortOrder.ALPHABETICAL
|
||||
R.id.action_order_rating -> SortOrder.RATING
|
||||
else -> return false
|
||||
}
|
||||
viewModel.setSortOrder(order)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.delete_manga)
|
||||
@@ -96,6 +83,8 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = LocalListFragment()
|
||||
fun newInstance() = LocalListFragment().withArgs(1) {
|
||||
putSerializable(RemoteListFragment.ARG_SOURCE, MangaSource.LOCAL) // required by FilterCoordinator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +1,57 @@
|
||||
package org.koitharu.kotatsu.local.ui
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import java.util.LinkedList
|
||||
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LocalListViewModel @Inject constructor(
|
||||
private val repository: LocalMangaRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val settings: AppSettings,
|
||||
private val tagHighlighter: MangaTagHighlighter,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
filter: FilterCoordinator,
|
||||
tagHighlighter: MangaTagHighlighter,
|
||||
settings: AppSettings,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler), ListExtraProvider {
|
||||
listExtraProvider: ListExtraProvider,
|
||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) : RemoteListViewModel(
|
||||
savedStateHandle,
|
||||
mangaRepositoryFactory,
|
||||
filter,
|
||||
tagHighlighter,
|
||||
settings,
|
||||
listExtraProvider,
|
||||
downloadScheduler,
|
||||
) {
|
||||
|
||||
val onMangaRemoved = MutableEventFlow<Unit>()
|
||||
val sortOrder = MutableStateFlow(settings.localListOrder)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val selectedTags = MutableStateFlow<Set<MangaTag>>(emptySet())
|
||||
private var refreshJob: Job? = null
|
||||
|
||||
override val content = combine(
|
||||
mangaList,
|
||||
listMode,
|
||||
sortOrder,
|
||||
selectedTags,
|
||||
listError,
|
||||
) { list, mode, order, tags, error ->
|
||||
when {
|
||||
error != null -> listOf(error.toErrorState(canRetry = true))
|
||||
list == null -> listOf(LoadingState)
|
||||
list.isEmpty() -> listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_local,
|
||||
textPrimary = R.string.text_local_holder_primary,
|
||||
textSecondary = R.string.text_local_holder_secondary,
|
||||
actionStringRes = R.string._import,
|
||||
),
|
||||
)
|
||||
|
||||
else -> buildList(list.size + 1) {
|
||||
add(createHeader(list, tags, order))
|
||||
list.toUi(this, mode, this@LocalListViewModel, tagHighlighter)
|
||||
}
|
||||
}
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||
|
||||
init {
|
||||
onRefresh()
|
||||
launchJob(Dispatchers.Default) {
|
||||
localStorageChanges
|
||||
.collectLatest {
|
||||
if (refreshJob?.isActive != true) {
|
||||
doRefresh()
|
||||
}
|
||||
.collect {
|
||||
loadList(filter.snapshot(), append = false).join()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdateFilter(tags: Set<MangaTag>) {
|
||||
selectedTags.value = tags
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
val prevJob = refreshJob
|
||||
refreshJob = launchLoadingJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
doRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRetry() = onRefresh()
|
||||
|
||||
fun setSortOrder(value: SortOrder) {
|
||||
sortOrder.value = value
|
||||
settings.localListOrder = value
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
fun delete(ids: Set<Long>) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
deleteLocalMangaUseCase(ids)
|
||||
@@ -125,60 +59,12 @@ class LocalListViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doRefresh() {
|
||||
try {
|
||||
listError.value = null
|
||||
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Throwable) {
|
||||
listError.value = e
|
||||
}
|
||||
}
|
||||
|
||||
private fun createHeader(mangaList: List<Manga>, selectedTags: Set<MangaTag>, order: SortOrder): ListHeader2 {
|
||||
val tags = HashMap<MangaTag, Int>()
|
||||
for (item in mangaList) {
|
||||
for (tag in item.tags) {
|
||||
tags[tag] = tags[tag]?.plus(1) ?: 1
|
||||
}
|
||||
}
|
||||
val topTags = tags.entries.sortedByDescending { it.value }.take(6)
|
||||
val chips = LinkedList<ChipsView.ChipModel>()
|
||||
for ((tag, _) in topTags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
tint = 0,
|
||||
title = tag.title,
|
||||
isCheckable = true,
|
||||
isChecked = tag in selectedTags,
|
||||
data = tag,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
chips.addFirst(model)
|
||||
} else {
|
||||
chips.addLast(model)
|
||||
}
|
||||
}
|
||||
return ListHeader2(
|
||||
chips = chips,
|
||||
sortOrder = order,
|
||||
hasSelectedTags = selectedTags.isNotEmpty(),
|
||||
override fun createEmptyState(canResetFilter: Boolean): EmptyState {
|
||||
return EmptyState(
|
||||
icon = R.drawable.ic_empty_local,
|
||||
textPrimary = R.string.text_local_holder_primary,
|
||||
textSecondary = R.string.text_local_holder_secondary,
|
||||
actionStringRes = R.string._import,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCounter(mangaId: Long): Int {
|
||||
return if (settings.isTrackerEnabled) {
|
||||
trackingRepository.getNewChaptersCount(mangaId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getProgress(mangaId: Long): Float {
|
||||
return if (settings.isReadingIndicatorsEnabled) {
|
||||
historyRepository.getProgress(mangaId)
|
||||
} else {
|
||||
PROGRESS_NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ class MainActivity :
|
||||
navigationDelegate =
|
||||
MainNavigationDelegate(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager)
|
||||
navigationDelegate.addOnFragmentChangedListener(this)
|
||||
navigationDelegate.onCreate(savedInstanceState)
|
||||
navigationDelegate.onCreate()
|
||||
|
||||
onBackPressedDispatcher.addCallback(ExitCallback(this, viewBinding.container))
|
||||
onBackPressedDispatcher.addCallback(navigationDelegate)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user