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'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 546
|
versionCode 547
|
||||||
versionName '5.1.2'
|
versionName '5.1.3'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:ebcc6391d6') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:44e28b40d3') {
|
||||||
exclude group: 'org.json', module: 'json'
|
exclude group: 'org.json', module: 'json'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.core:core-ktx:1.10.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.fragment:fragment-ktx:1.5.7'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-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.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.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.preference:preference-ktx:1.2.0'
|
||||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
|
||||||
implementation 'com.google.android.material:material:1.9.0'
|
implementation 'com.google.android.material:material:1.9.0'
|
||||||
@@ -106,7 +106,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||||
//noinspection GradleDependency
|
//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: 'com.google.guava', module: 'failureaccess'
|
||||||
exclude group: 'org.checkerframework', module: 'checker-qual'
|
exclude group: 'org.checkerframework', module: 'checker-qual'
|
||||||
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
|
||||||
@@ -116,8 +116,8 @@ dependencies {
|
|||||||
implementation 'androidx.room:room-ktx:2.5.1'
|
implementation 'androidx.room:room-ktx:2.5.1'
|
||||||
kapt 'androidx.room:room-compiler:2.5.1'
|
kapt 'androidx.room:room-compiler:2.5.1'
|
||||||
|
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
|
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.3'
|
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
|
||||||
implementation 'com.squareup.okio:okio:3.3.0'
|
implementation 'com.squareup.okio:okio:3.3.0'
|
||||||
|
|
||||||
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
|
||||||
@@ -128,8 +128,8 @@ dependencies {
|
|||||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||||
kapt 'androidx.hilt:hilt-compiler: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-base:2.4.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.3.0'
|
implementation 'io.coil-kt:coil-svg:2.4.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
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-http:5.9.7'
|
||||||
implementation 'ch.acra:acra-dialog: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 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.json:json:20230227'
|
testImplementation 'org.json:json:20230227'
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import java.util.EnumSet
|
|||||||
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val configKeyDomain: ConfigKey.Domain
|
||||||
get() = ConfigKey.Domain("", null)
|
get() = ConfigKey.Domain()
|
||||||
|
|
||||||
override val sortOrders: Set<SortOrder>
|
override val sortOrders: Set<SortOrder>
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
get() = EnumSet.allOf(SortOrder::class.java)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.bookmarks.data
|
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 kotlinx.coroutines.flow.Flow
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
|
||||||
|
|
||||||
@@ -18,7 +22,7 @@ abstract class BookmarksDao {
|
|||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query(
|
@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>>>
|
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
||||||
|
|
||||||
@@ -29,5 +33,8 @@ abstract class BookmarksDao {
|
|||||||
abstract suspend fun delete(entity: BookmarkEntity)
|
abstract suspend fun delete(entity: BookmarkEntity)
|
||||||
|
|
||||||
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
@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
|
package org.koitharu.kotatsu.bookmarks.domain
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
class Bookmark(
|
class Bookmark(
|
||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
@@ -27,9 +27,7 @@ class Bookmark(
|
|||||||
if (scroll != other.scroll) return false
|
if (scroll != other.scroll) return false
|
||||||
if (imageUrl != other.imageUrl) return false
|
if (imageUrl != other.imageUrl) return false
|
||||||
if (createdAt != other.createdAt) return false
|
if (createdAt != other.createdAt) return false
|
||||||
if (percent != other.percent) return false
|
return percent == other.percent
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -43,4 +41,4 @@ class Bookmark(
|
|||||||
result = 31 * result + percent.hashCode()
|
result = 31 * result + percent.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,14 @@ class BookmarksRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeBookmark(mangaId: Long, pageId: Long) {
|
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
|
||||||
db.bookmarksDao.delete(mangaId, pageId)
|
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 {
|
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ class BookmarksAdapter(
|
|||||||
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
|
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
|
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 {
|
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.activityManager
|
||||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||||
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
|
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.CacheDir
|
||||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
@@ -64,6 +65,9 @@ interface AppModule {
|
|||||||
@Binds
|
@Binds
|
||||||
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
|
fun bindImageGetter(coilImageGetter: CoilImageGetter): Html.ImageGetter
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
fun bindListExtraProvider(impl: ListExtraProviderImpl): ListExtraProvider
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -86,7 +90,8 @@ interface AppModule {
|
|||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
@MangaHttpClient okHttpClient: OkHttpClient,
|
@MangaHttpClient okHttpClient: OkHttpClient,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
pagesCache: PagesCache,
|
imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
|
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||||
): ImageLoader {
|
): ImageLoader {
|
||||||
val diskCacheFactory = {
|
val diskCacheFactory = {
|
||||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||||
@@ -108,7 +113,8 @@ interface AppModule {
|
|||||||
.add(SvgDecoder.Factory())
|
.add(SvgDecoder.Factory())
|
||||||
.add(CbzFetcher.Factory())
|
.add(CbzFetcher.Factory())
|
||||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||||
.add(MangaPageFetcher.Factory(context, okHttpClient, pagesCache, mangaRepositoryFactory))
|
.add(pageFetcherFactory)
|
||||||
|
.add(imageProxyInterceptor)
|
||||||
.build(),
|
.build(),
|
||||||
).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.Migration12To13
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
|
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.Migration1To2
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
|
||||||
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
|
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.TrackLogEntity
|
||||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||||
|
|
||||||
const val DATABASE_VERSION = 15
|
const val DATABASE_VERSION = 16
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
@@ -100,6 +101,7 @@ val databaseMigrations: Array<Migration>
|
|||||||
Migration12To13(),
|
Migration12To13(),
|
||||||
Migration13To14(),
|
Migration13To14(),
|
||||||
Migration14To15(),
|
Migration14To15(),
|
||||||
|
Migration15To16(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun MangaDatabase(context: Context): MangaDatabase = Room
|
fun MangaDatabase(context: Context): MangaDatabase = Room
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ data class MangaPrefsEntity(
|
|||||||
@ColumnInfo(name = "mode") val mode: Int,
|
@ColumnInfo(name = "mode") val mode: Int,
|
||||||
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
|
@ColumnInfo(name = "cf_brightness") val cfBrightness: Float,
|
||||||
@ColumnInfo(name = "cf_contrast") val cfContrast: 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,
|
private val settings: AppSettings,
|
||||||
) : ProxySelector() {
|
) : ProxySelector() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
setDefault(this)
|
||||||
|
}
|
||||||
|
|
||||||
private var cachedProxy: Proxy? = null
|
private var cachedProxy: Proxy? = null
|
||||||
|
|
||||||
override fun select(uri: URI?): List<Proxy> {
|
override fun select(uri: URI?): List<Proxy> {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ object CommonHeaders {
|
|||||||
const val ACCEPT_ENCODING = "Accept-Encoding"
|
const val ACCEPT_ENCODING = "Accept-Encoding"
|
||||||
const val AUTHORIZATION = "Authorization"
|
const val AUTHORIZATION = "Authorization"
|
||||||
const val CACHE_CONTROL = "Cache-Control"
|
const val CACHE_CONTROL = "Cache-Control"
|
||||||
|
const val PROXY_AUTHORIZATION = "Proxy-Authorization"
|
||||||
|
|
||||||
val CACHE_CONTROL_NO_STORE: CacheControl
|
val CACHE_CONTROL_NO_STORE: CacheControl
|
||||||
get() = CacheControl.Builder().noStore().build()
|
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)
|
writeTimeout(20, TimeUnit.SECONDS)
|
||||||
cookieJar(cookieJar)
|
cookieJar(cookieJar)
|
||||||
proxySelector(AppProxySelector(settings))
|
proxySelector(AppProxySelector(settings))
|
||||||
|
proxyAuthenticator(ProxyAuthenticator(settings))
|
||||||
dns(DoHManager(cache, settings))
|
dns(DoHManager(cache, settings))
|
||||||
if (settings.isSSLBypassEnabled) {
|
if (settings.isSSLBypassEnabled) {
|
||||||
bypassSSLErrors()
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.toEntities
|
import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
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.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -22,7 +20,6 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@Reusable
|
@Reusable
|
||||||
class MangaDataRepository @Inject constructor(
|
class MangaDataRepository @Inject constructor(
|
||||||
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
|
||||||
private val db: MangaDatabase,
|
private val db: MangaDatabase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -42,6 +39,7 @@ class MangaDataRepository @Inject constructor(
|
|||||||
entity.copy(
|
entity.copy(
|
||||||
cfBrightness = colorFilter?.brightness ?: 0f,
|
cfBrightness = colorFilter?.brightness ?: 0f,
|
||||||
cfContrast = colorFilter?.contrast ?: 0f,
|
cfContrast = colorFilter?.contrast ?: 0f,
|
||||||
|
cfInvert = colorFilter?.isInverted ?: false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -84,8 +82,8 @@ class MangaDataRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
|
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
|
||||||
return if (cfBrightness != 0f || cfContrast != 0f) {
|
return if (cfBrightness != 0f || cfContrast != 0f || cfInvert) {
|
||||||
ReaderColorFilter(cfBrightness, cfContrast)
|
ReaderColorFilter(cfBrightness, cfContrast, cfInvert)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -96,5 +94,6 @@ class MangaDataRepository @Inject constructor(
|
|||||||
mode = -1,
|
mode = -1,
|
||||||
cfBrightness = 0f,
|
cfBrightness = 0f,
|
||||||
cfContrast = 0f,
|
cfContrast = 0f,
|
||||||
|
cfInvert = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ interface MangaRepository {
|
|||||||
|
|
||||||
val sortOrders: Set<SortOrder>
|
val sortOrders: Set<SortOrder>
|
||||||
|
|
||||||
|
var defaultSortOrder: SortOrder
|
||||||
|
|
||||||
suspend fun getList(offset: Int, query: String): List<Manga>
|
suspend fun getList(offset: Int, query: String): List<Manga>
|
||||||
|
|
||||||
suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): 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>
|
override val sortOrders: Set<SortOrder>
|
||||||
get() = parser.sortOrders
|
get() = parser.sortOrders
|
||||||
|
|
||||||
var defaultSortOrder: SortOrder?
|
override var defaultSortOrder: SortOrder
|
||||||
get() = getConfig().defaultSortOrder ?: sortOrders.firstOrNull()
|
get() = getConfig().defaultSortOrder ?: sortOrders.first()
|
||||||
set(value) {
|
set(value) {
|
||||||
getConfig().defaultSortOrder = value
|
getConfig().defaultSortOrder = value
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ class RemoteMangaRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getAvailableMirrors(): List<String> {
|
fun getAvailableMirrors(): List<String> {
|
||||||
return parser.configKeyDomain.presetValues?.toList().orEmpty()
|
return parser.configKeyDomain.presetValues.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getConfig() = parser.config as SourceSettings
|
private fun getConfig() = parser.config as SourceSettings
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class FaviconFetcher(
|
|||||||
if (!options.diskCachePolicy.readEnabled) {
|
if (!options.diskCachePolicy.readEnabled) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val snapshot = diskCache.value?.get(diskCacheKey) ?: return null
|
val snapshot = diskCache.value?.openSnapshot(diskCacheKey) ?: return null
|
||||||
return SourceResult(
|
return SourceResult(
|
||||||
source = snapshot.toImageSource(),
|
source = snapshot.toImageSource(),
|
||||||
mimeType = null,
|
mimeType = null,
|
||||||
@@ -98,12 +98,12 @@ class FaviconFetcher(
|
|||||||
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
|
if (!options.diskCachePolicy.writeEnabled || body.contentLength() == 0L) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val editor = diskCache.value?.edit(diskCacheKey) ?: return null
|
val editor = diskCache.value?.openEditor(diskCacheKey) ?: return null
|
||||||
try {
|
try {
|
||||||
fileSystem.write(editor.data) {
|
fileSystem.write(editor.data) {
|
||||||
body.source().readAll(this)
|
body.source().readAll(this)
|
||||||
}
|
}
|
||||||
return editor.commitAndGet()
|
return editor.commitAndOpenSnapshot()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
try {
|
try {
|
||||||
editor.abort()
|
editor.abort()
|
||||||
|
|||||||
@@ -277,6 +277,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isReaderSliderEnabled: Boolean
|
val isReaderSliderEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
||||||
|
|
||||||
|
val isImagesProxyEnabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
|
||||||
|
|
||||||
val dnsOverHttps: DoHProvider
|
val dnsOverHttps: DoHProvider
|
||||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||||
|
|
||||||
@@ -295,6 +298,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val proxyPort: Int
|
val proxyPort: Int
|
||||||
get() = prefs.getString(KEY_PROXY_PORT, null)?.toIntOrNull() ?: 0
|
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
|
var localListOrder: SortOrder
|
||||||
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
|
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
|
||||||
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
|
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_TYPE = "proxy_type"
|
||||||
const val KEY_PROXY_ADDRESS = "proxy_address"
|
const val KEY_PROXY_ADDRESS = "proxy_address"
|
||||||
const val KEY_PROXY_PORT = "proxy_port"
|
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
|
// About
|
||||||
const val KEY_APP_UPDATE = "app_update"
|
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 org.koitharu.kotatsu.core.util.ext.getDisplaySize
|
||||||
import com.google.android.material.R as materialR
|
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() {
|
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
|
||||||
|
|
||||||
var viewBinding: B? = null
|
var viewBinding: B? = null
|
||||||
|
|||||||
@@ -3,57 +3,36 @@ package org.koitharu.kotatsu.core.ui
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.viewbinding.ViewBinding
|
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> :
|
abstract class BaseFullscreenActivity<B : ViewBinding> :
|
||||||
BaseActivity<B>(),
|
BaseActivity<B>() {
|
||||||
View.OnSystemUiVisibilityChangeListener {
|
|
||||||
|
private lateinit var insetsControllerCompat: WindowInsetsControllerCompat
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
with(window) {
|
with(window) {
|
||||||
|
insetsControllerCompat = WindowInsetsControllerCompat(this, decorView)
|
||||||
statusBarColor = Color.TRANSPARENT
|
statusBarColor = Color.TRANSPARENT
|
||||||
navigationBarColor = Color.TRANSPARENT
|
navigationBarColor = Color.TRANSPARENT
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
attributes.layoutInDisplayCutoutMode =
|
attributes.layoutInDisplayCutoutMode =
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
}
|
}
|
||||||
decorView.setOnSystemUiVisibilityChangeListener(this@BaseFullscreenActivity)
|
|
||||||
}
|
}
|
||||||
|
insetsControllerCompat.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
showSystemUI()
|
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() {
|
protected fun hideSystemUI() {
|
||||||
window.decorView.systemUiVisibility = SYSTEM_UI_FLAGS_HIDDEN
|
insetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
protected fun showSystemUI() {
|
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.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
abstract class BoundsScrollListener(private val offsetTop: Int, private val offsetBottom: Int) :
|
abstract class BoundsScrollListener(
|
||||||
RecyclerView.OnScrollListener() {
|
@JvmField protected val offsetTop: Int,
|
||||||
|
@JvmField protected val offsetBottom: Int
|
||||||
|
) : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
@@ -24,9 +26,16 @@ abstract class BoundsScrollListener(private val offsetTop: Int, private val offs
|
|||||||
if (firstVisibleItemPosition <= offsetTop) {
|
if (firstVisibleItemPosition <= offsetTop) {
|
||||||
onScrolledToStart(recyclerView)
|
onScrolledToStart(recyclerView)
|
||||||
}
|
}
|
||||||
|
onPostScrolled(recyclerView, firstVisibleItemPosition, visibleItemCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun onScrolledToStart(recyclerView: RecyclerView)
|
abstract fun onScrolledToStart(recyclerView: RecyclerView)
|
||||||
|
|
||||||
abstract fun onScrolledToEnd(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.util.AttributeSet
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
@@ -15,6 +16,12 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||||||
|
|
||||||
val fastScroller = FastScroller(context, attrs)
|
val fastScroller = FastScroller(context, attrs)
|
||||||
|
|
||||||
|
var isFastScrollerEnabled: Boolean = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
fastScroller.isVisible = value && isVisible
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fastScroller.id = R.id.fast_scroller
|
fastScroller.id = R.id.fast_scroller
|
||||||
fastScroller.layoutParams = ViewGroup.LayoutParams(
|
fastScroller.layoutParams = ViewGroup.LayoutParams(
|
||||||
@@ -30,7 +37,7 @@ class FastScrollRecyclerView @JvmOverloads constructor(
|
|||||||
|
|
||||||
override fun setVisibility(visibility: Int) {
|
override fun setVisibility(visibility: Int) {
|
||||||
super.setVisibility(visibility)
|
super.setVisibility(visibility)
|
||||||
fastScroller.visibility = visibility
|
fastScroller.visibility = if (isFastScrollerEnabled) visibility else GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
private var bubbleHeight = 0
|
private var bubbleHeight = 0
|
||||||
private var handleHeight = 0
|
private var handleHeight = 0
|
||||||
private var viewHeight = 0
|
private var viewHeight = 0
|
||||||
|
private var offset = 0
|
||||||
private var hideScrollbar = true
|
private var hideScrollbar = true
|
||||||
private var showBubble = true
|
private var showBubble = true
|
||||||
private var showBubbleAlways = false
|
private var showBubbleAlways = false
|
||||||
@@ -137,6 +138,7 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
bubbleSize = getBubbleSize(R.styleable.FastScroller_bubbleSize, BubbleSize.NORMAL)
|
bubbleSize = getBubbleSize(R.styleable.FastScroller_bubbleSize, BubbleSize.NORMAL)
|
||||||
val textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize)
|
val textSize = getDimension(R.styleable.FastScroller_bubbleTextSize, bubbleSize.textSize)
|
||||||
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||||
|
offset = getDimensionPixelOffset(R.styleable.FastScroller_scrollerOffset, offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
setTrackColor(trackColor)
|
setTrackColor(trackColor)
|
||||||
@@ -248,7 +250,7 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
|
|
||||||
layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
|
layoutParams = (layoutParams as ConstraintLayout.LayoutParams).apply {
|
||||||
height = 0
|
height = 0
|
||||||
setMargins(0, marginTop, 0, marginBottom)
|
setMargins(offset, marginTop, offset, marginBottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,13 +258,13 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
height = LayoutParams.MATCH_PARENT
|
height = LayoutParams.MATCH_PARENT
|
||||||
anchorGravity = GravityCompat.END
|
anchorGravity = GravityCompat.END
|
||||||
anchorId = recyclerViewId
|
anchorId = recyclerViewId
|
||||||
setMargins(0, marginTop, 0, marginBottom)
|
setMargins(offset, marginTop, offset, marginBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
is FrameLayout -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply {
|
is FrameLayout -> layoutParams = (layoutParams as FrameLayout.LayoutParams).apply {
|
||||||
height = LayoutParams.MATCH_PARENT
|
height = LayoutParams.MATCH_PARENT
|
||||||
gravity = GravityCompat.END
|
gravity = GravityCompat.END
|
||||||
setMargins(0, marginTop, 0, marginBottom)
|
setMargins(offset, marginTop, offset, marginBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply {
|
is RelativeLayout -> layoutParams = (layoutParams as RelativeLayout.LayoutParams).apply {
|
||||||
@@ -270,7 +272,7 @@ class FastScroller @JvmOverloads constructor(
|
|||||||
addRule(RelativeLayout.ALIGN_TOP, recyclerViewId)
|
addRule(RelativeLayout.ALIGN_TOP, recyclerViewId)
|
||||||
addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId)
|
addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId)
|
||||||
addRule(RelativeLayout.ALIGN_END, 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")
|
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
|
private const val THROTTLE_DELAY = 200L
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
class BottomSheetHeaderBar @JvmOverloads constructor(
|
class BottomSheetHeaderBar @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.content.res.ColorStateList
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View.OnClickListener
|
import android.view.View.OnClickListener
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.getColorStateListOrThrow
|
import androidx.core.content.res.getColorStateListOrThrow
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
@@ -101,6 +102,13 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
chip.setTextColor(tint ?: defaultChipTextColor)
|
chip.setTextColor(tint ?: defaultChipTextColor)
|
||||||
chip.isClickable = onChipClickListener != null || model.isCheckable
|
chip.isClickable = onChipClickListener != null || model.isCheckable
|
||||||
chip.isCheckable = 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.isChecked = model.isChecked
|
||||||
chip.tag = model.data
|
chip.tag = model.data
|
||||||
}
|
}
|
||||||
@@ -134,6 +142,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
class ChipModel(
|
class ChipModel(
|
||||||
@ColorRes val tint: Int,
|
@ColorRes val tint: Int,
|
||||||
val title: CharSequence,
|
val title: CharSequence,
|
||||||
|
@DrawableRes val icon: Int,
|
||||||
val isCheckable: Boolean,
|
val isCheckable: Boolean,
|
||||||
val isChecked: Boolean,
|
val isChecked: Boolean,
|
||||||
val data: Any? = null,
|
val data: Any? = null,
|
||||||
@@ -147,6 +156,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
|
|
||||||
if (tint != other.tint) return false
|
if (tint != other.tint) return false
|
||||||
if (title != other.title) return false
|
if (title != other.title) return false
|
||||||
|
if (icon != other.icon) return false
|
||||||
if (isCheckable != other.isCheckable) return false
|
if (isCheckable != other.isCheckable) return false
|
||||||
if (isChecked != other.isChecked) return false
|
if (isChecked != other.isChecked) return false
|
||||||
return data == other.data
|
return data == other.data
|
||||||
@@ -155,6 +165,7 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = tint.hashCode()
|
var result = tint.hashCode()
|
||||||
result = 31 * result + title.hashCode()
|
result = 31 * result + title.hashCode()
|
||||||
|
result = 31 * result + icon.hashCode()
|
||||||
result = 31 * result + isCheckable.hashCode()
|
result = 31 * result + isCheckable.hashCode()
|
||||||
result = 31 * result + isChecked.hashCode()
|
result = 31 * result + isChecked.hashCode()
|
||||||
result = 31 * result + (data?.hashCode() ?: 0)
|
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
|
private var isConsumed = false
|
||||||
|
|
||||||
suspend fun consume(collector: FlowCollector<T>) {
|
suspend fun consume(collector: FlowCollector<T>) {
|
||||||
if (isConsumed) {
|
if (!isConsumed) {
|
||||||
collector.emit(data)
|
collector.emit(data)
|
||||||
isConsumed = true
|
isConsumed = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class RetainedLifecycleCoroutineScope(
|
class RetainedLifecycleCoroutineScope(
|
||||||
private val lifecycle: RetainedLifecycle,
|
val lifecycle: RetainedLifecycle,
|
||||||
) : CoroutineScope, RetainedLifecycle.OnClearedListener {
|
) : CoroutineScope, RetainedLifecycle.OnClearedListener {
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
|
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.Activity
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
|
import android.app.ActivityManager.MemoryInfo
|
||||||
import android.app.ActivityOptions
|
import android.app.ActivityOptions
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.ACTIVITY_SERVICE
|
import android.content.Context.ACTIVITY_SERVICE
|
||||||
@@ -140,6 +141,13 @@ fun Context.isLowRamDevice(): Boolean {
|
|||||||
return activityManager?.isLowRamDevice ?: false
|
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(
|
fun scaleUpActivityOptionsOf(view: View): ActivityOptions = ActivityOptions.makeScaleUpAnimation(
|
||||||
view,
|
view,
|
||||||
0,
|
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.LifecycleOwner
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||||
import kotlinx.coroutines.CancellableContinuation
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
@@ -18,6 +20,9 @@ import kotlin.coroutines.resumeWithException
|
|||||||
val processLifecycleScope: LifecycleCoroutineScope
|
val processLifecycleScope: LifecycleCoroutineScope
|
||||||
inline get() = ProcessLifecycleOwner.get().lifecycleScope
|
inline get() = ProcessLifecycleOwner.get().lifecycleScope
|
||||||
|
|
||||||
|
val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope
|
||||||
|
inline get() = RetainedLifecycleCoroutineScope(this)
|
||||||
|
|
||||||
suspend fun Lifecycle.awaitStateAtLeast(state: Lifecycle.State) {
|
suspend fun Lifecycle.awaitStateAtLeast(state: Lifecycle.State) {
|
||||||
if (currentState.isAtLeast(state)) {
|
if (currentState.isAtLeast(state)) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,13 +9,9 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.FlowCollector
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
|
||||||
import org.koitharu.kotatsu.core.util.Event
|
import org.koitharu.kotatsu.core.util.Event
|
||||||
|
|
||||||
fun <T> Flow<T>.observe(owner: LifecycleOwner, collector: FlowCollector<T>) {
|
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
|
val start = if (this is StateFlow) CoroutineStart.UNDISPATCHED else CoroutineStart.DEFAULT
|
||||||
owner.lifecycleScope.launch(start = start) {
|
owner.lifecycleScope.launch(start = start) {
|
||||||
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -23,3 +24,17 @@ fun Response.parseJsonOrNull(): JSONObject? {
|
|||||||
closeQuietly()
|
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
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.util.CompositeRunnable
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
||||||
if (obj == null || !isInstance(obj)) {
|
if (obj == null || !isInstance(obj)) {
|
||||||
@@ -7,3 +9,15 @@ fun <T> Class<T>.castOrNull(obj: Any?): T? {
|
|||||||
}
|
}
|
||||||
return obj as 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.AttrRes
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.FloatRange
|
import androidx.annotation.FloatRange
|
||||||
|
import androidx.annotation.Px
|
||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
|
|
||||||
@@ -22,6 +23,22 @@ fun Context.getThemeColor(
|
|||||||
it.getColor(0, fallback)
|
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
|
@ColorInt
|
||||||
fun Context.getThemeColor(
|
fun Context.getThemeColor(
|
||||||
@AttrRes resId: Int,
|
@AttrRes resId: Int,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.createViewModelLazy
|
import androidx.fragment.app.createViewModelLazy
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.ViewModelStore
|
||||||
import androidx.lifecycle.viewmodel.CreationExtras
|
import androidx.lifecycle.viewmodel.CreationExtras
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
@@ -17,3 +19,7 @@ inline fun <reified VM : ViewModel> Fragment.parentFragmentViewModels(
|
|||||||
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
|
extrasProducer = { extrasProducer?.invoke() ?: requireParentFragment().defaultViewModelCreationExtras },
|
||||||
factoryProducer = factoryProducer ?: { requireParentFragment().defaultViewModelProviderFactory },
|
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 androidx.appcompat.view.ActionMode
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
|
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(
|
class ChaptersBottomSheetMediator(
|
||||||
bottomSheet: View,
|
private val behavior: BottomSheetBehavior<*>,
|
||||||
) : OnBackPressedCallback(false),
|
) : OnBackPressedCallback(false),
|
||||||
ActionModeListener,
|
ActionModeListener,
|
||||||
BottomSheetHeaderBar.OnExpansionChangeListener,
|
|
||||||
OnLayoutChangeListener {
|
OnLayoutChangeListener {
|
||||||
|
|
||||||
private val behavior = BottomSheetBehavior.from(bottomSheet)
|
|
||||||
private var lockCounter = 0
|
private var lockCounter = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
behavior.doOnExpansionsChanged { isExpanded ->
|
||||||
|
isEnabled = isExpanded
|
||||||
|
if (!isExpanded) {
|
||||||
|
unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
}
|
}
|
||||||
@@ -30,13 +37,6 @@ class ChaptersBottomSheetMediator(
|
|||||||
unlock()
|
unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
|
|
||||||
isEnabled = isExpanded
|
|
||||||
if (!isExpanded) {
|
|
||||||
unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLayoutChange(
|
override fun onLayoutChange(
|
||||||
v: View?,
|
v: View?,
|
||||||
left: Int,
|
left: Int,
|
||||||
@@ -61,6 +61,9 @@ class ChaptersBottomSheetMediator(
|
|||||||
|
|
||||||
fun unlock() {
|
fun unlock() {
|
||||||
lockCounter--
|
lockCounter--
|
||||||
|
if (lockCounter < 0) {
|
||||||
|
lockCounter = 0
|
||||||
|
}
|
||||||
behavior.isDraggable = lockCounter <= 0
|
behavior.isDraggable = lockCounter <= 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
package org.koitharu.kotatsu.details.ui
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@@ -56,6 +57,9 @@ class ChaptersFragment :
|
|||||||
checkNotNull(selectionController).attachToRecyclerView(this)
|
checkNotNull(selectionController).attachToRecyclerView(this)
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
adapter = chaptersAdapter
|
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.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
|
||||||
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
|
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package org.koitharu.kotatsu.details.ui
|
package org.koitharu.kotatsu.details.ui
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||||
import org.koitharu.kotatsu.details.ui.model.toListItem
|
import org.koitharu.kotatsu.details.ui.model.toListItem
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
|
||||||
fun mapChapters(
|
fun mapChapters(
|
||||||
remoteManga: Manga?,
|
remoteManga: Manga?,
|
||||||
@@ -11,12 +13,14 @@ fun mapChapters(
|
|||||||
history: MangaHistory?,
|
history: MangaHistory?,
|
||||||
newCount: Int,
|
newCount: Int,
|
||||||
branch: String?,
|
branch: String?,
|
||||||
|
bookmarks: List<Bookmark>,
|
||||||
): List<ChapterListItem> {
|
): List<ChapterListItem> {
|
||||||
val remoteChapters = remoteManga?.getChapters(branch).orEmpty()
|
val remoteChapters = remoteManga?.getChapters(branch).orEmpty()
|
||||||
val localChapters = localManga?.getChapters(branch).orEmpty()
|
val localChapters = localManga?.getChapters(branch).orEmpty()
|
||||||
if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
|
if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
val bookmarked = bookmarks.mapToSet { it.chapterId }
|
||||||
val currentId = history?.chapterId ?: 0L
|
val currentId = history?.chapterId ?: 0L
|
||||||
val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount
|
val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount
|
||||||
val chaptersSize = maxOf(remoteChapters.size, localChapters.size)
|
val chaptersSize = maxOf(remoteChapters.size, localChapters.size)
|
||||||
@@ -41,6 +45,7 @@ fun mapChapters(
|
|||||||
isUnread = isUnread,
|
isUnread = isUnread,
|
||||||
isNew = isUnread && result.size >= newFrom,
|
isNew = isUnread && result.size >= newFrom,
|
||||||
isDownloaded = local != null,
|
isDownloaded = local != null,
|
||||||
|
isBookmarked = chapter.id in bookmarked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!localMap.isNullOrEmpty()) {
|
if (!localMap.isNullOrEmpty()) {
|
||||||
@@ -53,6 +58,7 @@ fun mapChapters(
|
|||||||
isUnread = isUnread,
|
isUnread = isUnread,
|
||||||
isNew = false,
|
isNew = false,
|
||||||
isDownloaded = remoteManga != null,
|
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.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
class ChaptersMenuProvider(
|
class ChaptersMenuProvider(
|
||||||
private val viewModel: DetailsViewModel,
|
private val viewModel: DetailsViewModel,
|
||||||
private val bottomSheetMediator: ChaptersBottomSheetMediator?,
|
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) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
menuInflater.inflate(R.menu.opt_chapters, menu)
|
menuInflater.inflate(R.menu.opt_chapters, menu)
|
||||||
@@ -20,6 +24,7 @@ class ChaptersMenuProvider(
|
|||||||
searchView.setOnQueryTextListener(this)
|
searchView.setOnQueryTextListener(this)
|
||||||
searchView.setIconifiedByDefault(false)
|
searchView.setIconifiedByDefault(false)
|
||||||
searchView.queryHint = searchMenuItem.title
|
searchView.queryHint = searchMenuItem.title
|
||||||
|
searchItemRef = WeakReference(searchMenuItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPrepareMenu(menu: Menu) {
|
override fun onPrepareMenu(menu: Menu) {
|
||||||
@@ -32,15 +37,22 @@ class ChaptersMenuProvider(
|
|||||||
viewModel.setChaptersReversed(!menuItem.isChecked)
|
viewModel.setChaptersReversed(!menuItem.isChecked)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
searchItemRef?.get()?.collapseActionView()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||||
bottomSheetMediator?.lock()
|
bottomSheetMediator?.lock()
|
||||||
|
isEnabled = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||||
|
isEnabled = false
|
||||||
(item.actionView as? SearchView)?.setQuery("", false)
|
(item.actionView as? SearchView)?.setQuery("", false)
|
||||||
viewModel.performChapterSearch(null)
|
viewModel.performChapterSearch(null)
|
||||||
bottomSheetMediator?.unlock()
|
bottomSheetMediator?.unlock()
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import android.content.Context
|
|||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.transition.AutoTransition
|
||||||
import android.transition.Slide
|
import android.transition.Slide
|
||||||
import android.transition.TransitionManager
|
import android.transition.TransitionManager
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@@ -17,7 +19,9 @@ import androidx.appcompat.widget.PopupMenu
|
|||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
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.BaseTransientBottomBar
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
|
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
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.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.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
|
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.ReaderActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DetailsActivity :
|
class DetailsActivity :
|
||||||
BaseActivity<ActivityDetailsBinding>(),
|
BaseActivity<ActivityDetailsBinding>(),
|
||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
BottomSheetHeaderBar.OnExpansionChangeListener,
|
|
||||||
NoModalBottomSheetOwner,
|
NoModalBottomSheetOwner,
|
||||||
View.OnLongClickListener,
|
View.OnLongClickListener,
|
||||||
PopupMenu.OnMenuItemClickListener {
|
PopupMenu.OnMenuItemClickListener {
|
||||||
|
|
||||||
override val bsHeader: BottomSheetHeaderBar?
|
|
||||||
get() = viewBinding.headerChapters
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var shortcutsUpdater: ShortcutsUpdater
|
lateinit var shortcutsUpdater: ShortcutsUpdater
|
||||||
|
|
||||||
@@ -82,16 +86,22 @@ class DetailsActivity :
|
|||||||
viewBinding.buttonDropdown.setOnClickListener(this)
|
viewBinding.buttonDropdown.setOnClickListener(this)
|
||||||
viewBadge = ViewBadge(viewBinding.buttonRead, this)
|
viewBadge = ViewBadge(viewBinding.buttonRead, this)
|
||||||
|
|
||||||
chaptersMenuProvider = if (viewBinding.layoutBottom != null) {
|
if (viewBinding.layoutBottom != null) {
|
||||||
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(viewBinding.layoutBottom))
|
val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom))
|
||||||
|
val bsMediator = ChaptersBottomSheetMediator(behavior)
|
||||||
actionModeDelegate.addListener(bsMediator)
|
actionModeDelegate.addListener(bsMediator)
|
||||||
checkNotNull(viewBinding.headerChapters).addOnExpansionChangeListener(bsMediator)
|
checkNotNull(viewBinding.layoutBsHeader).addOnLayoutChangeListener(bsMediator)
|
||||||
checkNotNull(viewBinding.headerChapters).addOnLayoutChangeListener(bsMediator)
|
|
||||||
onBackPressedDispatcher.addCallback(bsMediator)
|
onBackPressedDispatcher.addCallback(bsMediator)
|
||||||
ChaptersMenuProvider(viewModel, bsMediator)
|
chaptersMenuProvider = ChaptersMenuProvider(viewModel, bsMediator)
|
||||||
|
behavior.doOnExpansionsChanged(::onChaptersSheetStateChanged)
|
||||||
|
viewBinding.toolbarChapters?.setNavigationOnClickListener {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ChaptersMenuProvider(viewModel, null)
|
chaptersMenuProvider = ChaptersMenuProvider(viewModel, null)
|
||||||
|
addMenuProvider(chaptersMenuProvider)
|
||||||
}
|
}
|
||||||
|
onBackPressedDispatcher.addCallback(chaptersMenuProvider)
|
||||||
|
|
||||||
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
|
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
|
||||||
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
|
||||||
@@ -114,11 +124,11 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
viewModel.historyInfo.observe(this, ::onHistoryChanged)
|
||||||
viewModel.selectedBranch.observe(this) {
|
viewModel.selectedBranch.observe(this) {
|
||||||
viewBinding.headerChapters?.subtitle = it
|
viewBinding.toolbarChapters?.subtitle = it
|
||||||
viewBinding.textViewSubtitle?.textAndVisible = it
|
viewBinding.textViewSubtitle?.textAndVisible = it
|
||||||
}
|
}
|
||||||
viewModel.isChaptersReversed.observe(this) {
|
viewModel.isChaptersReversed.observe(this) {
|
||||||
viewBinding.headerChapters?.invalidateMenu() ?: invalidateOptionsMenu()
|
viewBinding.toolbarChapters?.invalidateMenu() ?: invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
viewModel.favouriteCategories.observe(this) {
|
viewModel.favouriteCategories.observe(this) {
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
@@ -137,7 +147,10 @@ class DetailsActivity :
|
|||||||
shortcutsUpdater = shortcutsUpdater,
|
shortcutsUpdater = shortcutsUpdater,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
viewBinding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider)
|
}
|
||||||
|
|
||||||
|
override fun getBottomSheetCollapsedHeight(): Int {
|
||||||
|
return viewBinding.layoutBsHeader?.measureHeight() ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
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) {
|
if (isExpanded) {
|
||||||
headerBar.addMenuProvider(chaptersMenuProvider)
|
toolbar.addMenuProvider(chaptersMenuProvider)
|
||||||
|
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material)
|
||||||
} else {
|
} else {
|
||||||
headerBar.removeMenuProvider(chaptersMenuProvider)
|
toolbar.removeMenuProvider(chaptersMenuProvider)
|
||||||
|
toolbar.navigationIcon = null
|
||||||
}
|
}
|
||||||
viewBinding.buttonRead.isGone = isExpanded
|
viewBinding.buttonRead.isGone = isExpanded
|
||||||
}
|
}
|
||||||
@@ -219,6 +240,12 @@ class DetailsActivity :
|
|||||||
if (insets.bottom > 0) {
|
if (insets.bottom > 0) {
|
||||||
window.setNavigationBarTransparentCompat(this, viewBinding.layoutBottom?.elevation ?: 0f, 0.9f)
|
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) {
|
private fun onHistoryChanged(info: HistoryInfo) {
|
||||||
@@ -237,7 +264,7 @@ class DetailsActivity :
|
|||||||
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
info.totalChapters == 0 -> getString(R.string.no_chapters)
|
||||||
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
|
||||||
}
|
}
|
||||||
viewBinding.headerChapters?.title = text
|
viewBinding.toolbarChapters?.title = text
|
||||||
viewBinding.textViewTitle?.text = text
|
viewBinding.textViewTitle?.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,8 +309,6 @@ class DetailsActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isTabletLayout() = viewBinding.layoutBottom == null
|
|
||||||
|
|
||||||
private fun showBottomSheet(isVisible: Boolean) {
|
private fun showBottomSheet(isVisible: Boolean) {
|
||||||
val view = viewBinding.layoutBottom ?: return
|
val view = viewBinding.layoutBottom ?: return
|
||||||
if (view.isVisible == isVisible) return
|
if (view.isVisible == isVisible) return
|
||||||
@@ -297,7 +322,7 @@ class DetailsActivity :
|
|||||||
private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar {
|
private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar {
|
||||||
val sb = Snackbar.make(viewBinding.containerDetails, text, duration)
|
val sb = Snackbar.make(viewBinding.containerDetails, text, duration)
|
||||||
if (viewBinding.layoutBottom?.isVisible == true) {
|
if (viewBinding.layoutBottom?.isVisible == true) {
|
||||||
sb.anchorView = viewBinding.headerChapters
|
sb.anchorView = viewBinding.toolbarChapters
|
||||||
}
|
}
|
||||||
return sb
|
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.drawableTop
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||||
import org.koitharu.kotatsu.core.util.ext.measureHeight
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
import org.koitharu.kotatsu.core.util.ext.resolveDp
|
||||||
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
|
||||||
@@ -84,6 +83,7 @@ class DetailsFragment :
|
|||||||
binding.infoLayout.textViewSource.setOnClickListener(this)
|
binding.infoLayout.textViewSource.setOnClickListener(this)
|
||||||
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||||
binding.chipsTags.onChipClickListener = this
|
binding.chipsTags.onChipClickListener = this
|
||||||
|
TitleScrollCoordinator(binding.textViewTitle).attach(binding.scrollView)
|
||||||
viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated)
|
viewModel.manga.filterNotNull().observe(viewLifecycleOwner, ::onMangaUpdated)
|
||||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||||
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
|
viewModel.historyInfo.observe(viewLifecycleOwner, ::onHistoryChanged)
|
||||||
@@ -271,7 +271,7 @@ class DetailsFragment :
|
|||||||
override fun onWindowInsetsChanged(insets: Insets) {
|
override fun onWindowInsetsChanged(insets: Insets) {
|
||||||
requireViewBinding().root.updatePadding(
|
requireViewBinding().root.updatePadding(
|
||||||
bottom = (
|
bottom = (
|
||||||
(activity as? NoModalBottomSheetOwner)?.bsHeader?.measureHeight()
|
(activity as? NoModalBottomSheetOwner)?.getBottomSheetCollapsedHeight()
|
||||||
?.plus(insets.bottom)?.plus(resources.resolveDp(16))
|
?.plus(insets.bottom)?.plus(resources.resolveDp(16))
|
||||||
)
|
)
|
||||||
?: insets.bottom,
|
?: insets.bottom,
|
||||||
@@ -284,6 +284,7 @@ class DetailsFragment :
|
|||||||
ChipsView.ChipModel(
|
ChipsView.ChipModel(
|
||||||
title = tag.title,
|
title = tag.title,
|
||||||
tint = tagHighlighter.getTint(tag),
|
tint = tagHighlighter.getTint(tag),
|
||||||
|
icon = 0,
|
||||||
data = tag,
|
data = tag,
|
||||||
isCheckable = false,
|
isCheckable = false,
|
||||||
isChecked = 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.os.ShortcutsUpdater
|
||||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||||
import org.koitharu.kotatsu.details.ui.model.MangaBranch
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
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
|
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
|
||||||
|
|
||||||
class DetailsMenuProvider(
|
class DetailsMenuProvider(
|
||||||
@@ -63,7 +63,7 @@ class DetailsMenuProvider(
|
|||||||
|
|
||||||
R.id.action_favourite -> {
|
R.id.action_favourite -> {
|
||||||
viewModel.manga.value?.let {
|
viewModel.manga.value?.let {
|
||||||
FavouriteCategoriesBottomSheet.show(activity.supportFragmentManager, it)
|
FavouriteCategoriesSheet.show(activity.supportFragmentManager, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class DetailsMenuProvider(
|
|||||||
|
|
||||||
R.id.action_scrobbling -> {
|
R.id.action_scrobbling -> {
|
||||||
viewModel.manga.value?.let {
|
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,
|
history,
|
||||||
selectedBranch,
|
selectedBranch,
|
||||||
newChaptersCount,
|
newChaptersCount,
|
||||||
) { manga, history, branch, news ->
|
bookmarks,
|
||||||
mapChapters(manga?.remote, manga?.local, history, news, branch)
|
) { manga, history, branch, news, bookmarks ->
|
||||||
|
mapChapters(manga?.remote, manga?.local, history, news, branch, bookmarks)
|
||||||
},
|
},
|
||||||
isChaptersReversed,
|
isChaptersReversed,
|
||||||
chaptersQuery,
|
chaptersQuery,
|
||||||
@@ -209,8 +210,8 @@ class DetailsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun removeBookmark(bookmark: Bookmark) {
|
fun removeBookmark(bookmark: Bookmark) {
|
||||||
launchJob {
|
launchJob(Dispatchers.Default) {
|
||||||
bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId)
|
bookmarksRepository.removeBookmark(bookmark)
|
||||||
onShowToast.call(R.string.bookmark_removed)
|
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
|
package org.koitharu.kotatsu.details.ui.adapter
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.drawableStart
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
import org.koitharu.kotatsu.databinding.ItemChapterBinding
|
||||||
@@ -43,7 +45,13 @@ fun chapterListItemAD(
|
|||||||
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
|
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.imageViewBookmarked.isVisible = item.isBookmarked
|
||||||
binding.imageViewDownloaded.isVisible = item.isDownloaded
|
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
|
val isDownloaded: Boolean
|
||||||
get() = hasFlag(FLAG_DOWNLOADED)
|
get() = hasFlag(FLAG_DOWNLOADED)
|
||||||
|
|
||||||
|
val isBookmarked: Boolean
|
||||||
|
get() = hasFlag(FLAG_BOOKMARKED)
|
||||||
|
|
||||||
val isNew: Boolean
|
val isNew: Boolean
|
||||||
get() = hasFlag(FLAG_NEW)
|
get() = hasFlag(FLAG_NEW)
|
||||||
|
|
||||||
@@ -70,6 +73,7 @@ class ChapterListItem(
|
|||||||
const val FLAG_UNREAD = 2
|
const val FLAG_UNREAD = 2
|
||||||
const val FLAG_CURRENT = 4
|
const val FLAG_CURRENT = 4
|
||||||
const val FLAG_NEW = 8
|
const val FLAG_NEW = 8
|
||||||
|
const val FLAG_BOOKMARKED = 16
|
||||||
const val FLAG_DOWNLOADED = 32
|
const val FLAG_DOWNLOADED = 32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.details.ui.model
|
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_CURRENT
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
|
||||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
|
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
|
||||||
@@ -11,11 +12,13 @@ fun MangaChapter.toListItem(
|
|||||||
isUnread: Boolean,
|
isUnread: Boolean,
|
||||||
isNew: Boolean,
|
isNew: Boolean,
|
||||||
isDownloaded: Boolean,
|
isDownloaded: Boolean,
|
||||||
|
isBookmarked: Boolean,
|
||||||
): ChapterListItem {
|
): ChapterListItem {
|
||||||
var flags = 0
|
var flags = 0
|
||||||
if (isCurrent) flags = flags or FLAG_CURRENT
|
if (isCurrent) flags = flags or FLAG_CURRENT
|
||||||
if (isUnread) flags = flags or FLAG_UNREAD
|
if (isUnread) flags = flags or FLAG_UNREAD
|
||||||
if (isNew) flags = flags or FLAG_NEW
|
if (isNew) flags = flags or FLAG_NEW
|
||||||
|
if (isBookmarked) flags = flags or FLAG_BOOKMARKED
|
||||||
if (isDownloaded) flags = flags or FLAG_DOWNLOADED
|
if (isDownloaded) flags = flags or FLAG_DOWNLOADED
|
||||||
return ChapterListItem(
|
return ChapterListItem(
|
||||||
chapter = this,
|
chapter = this,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ fun scrobblingInfoAD(
|
|||||||
{ layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) },
|
{ layoutInflater, parent -> ItemScrobblingInfoBinding.inflate(layoutInflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
ScrobblingInfoBottomSheet.show(fragmentManager, bindingAdapterPosition)
|
ScrobblingInfoSheet.show(fragmentManager, bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
bind {
|
bind {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import androidx.fragment.app.activityViewModels
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
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.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
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.image.ui.ImageActivity
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
|
||||||
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ScrobblingInfoBottomSheet :
|
class ScrobblingInfoSheet :
|
||||||
BaseBottomSheet<SheetScrobblingBinding>(),
|
BaseAdaptiveSheet<SheetScrobblingBinding>(),
|
||||||
AdapterView.OnItemSelectedListener,
|
AdapterView.OnItemSelectedListener,
|
||||||
RatingBar.OnRatingBarChangeListener,
|
RatingBar.OnRatingBarChangeListener,
|
||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
@@ -74,7 +74,7 @@ class ScrobblingInfoBottomSheet :
|
|||||||
|
|
||||||
menu = PopupMenu(binding.root.context, binding.buttonMenu).apply {
|
menu = PopupMenu(binding.root.context, binding.buttonMenu).apply {
|
||||||
inflate(R.menu.opt_scrobbling)
|
inflate(R.menu.opt_scrobbling)
|
||||||
setOnMenuItemClickListener(this@ScrobblingInfoBottomSheet)
|
setOnMenuItemClickListener(this@ScrobblingInfoSheet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class ScrobblingInfoBottomSheet :
|
|||||||
R.id.action_edit -> {
|
R.id.action_edit -> {
|
||||||
val manga = viewModel.manga.value ?: return false
|
val manga = viewModel.manga.value ?: return false
|
||||||
val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler
|
val scrobblerService = viewModel.scrobblingInfo.value.getOrNull(scrobblerIndex)?.scrobbler
|
||||||
ScrobblingSelectorBottomSheet.show(parentFragmentManager, manga, scrobblerService)
|
ScrobblingSelectorSheet.show(parentFragmentManager, manga, scrobblerService)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ class ScrobblingInfoBottomSheet :
|
|||||||
private const val TAG = "ScrobblingInfoBottomSheet"
|
private const val TAG = "ScrobblingInfoBottomSheet"
|
||||||
private const val ARG_INDEX = "index"
|
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)
|
putInt(ARG_INDEX, index)
|
||||||
}.show(fm, TAG)
|
}.show(fm, TAG)
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ class ExploreFragment :
|
|||||||
viewModel.onOpenManga.observeEvent(viewLifecycleOwner, ::onOpenManga)
|
viewModel.onOpenManga.observeEvent(viewLifecycleOwner, ::onOpenManga)
|
||||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||||
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
|
viewModel.isGrid.observe(viewLifecycleOwner, ::onGridModeChanged)
|
||||||
viewModel.onShowSuggestionsTip.observe(viewLifecycleOwner) {
|
viewModel.onShowSuggestionsTip.observeEvent(viewLifecycleOwner) {
|
||||||
showSuggestionsTip()
|
showSuggestionsTip()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ sealed interface ExploreItem : ListModel {
|
|||||||
|
|
||||||
other as Buttons
|
other as Buttons
|
||||||
|
|
||||||
if (isSuggestionsEnabled != other.isSuggestionsEnabled) return false
|
return isSuggestionsEnabled == other.isSuggestionsEnabled
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -40,9 +38,7 @@ sealed interface ExploreItem : ListModel {
|
|||||||
other as Header
|
other as Header
|
||||||
|
|
||||||
if (titleResId != other.titleResId) return false
|
if (titleResId != other.titleResId) return false
|
||||||
if (isButtonVisible != other.isButtonVisible) return false
|
return isButtonVisible == other.isButtonVisible
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -64,9 +60,7 @@ sealed interface ExploreItem : ListModel {
|
|||||||
other as Source
|
other as Source
|
||||||
|
|
||||||
if (source != other.source) return false
|
if (source != other.source) return false
|
||||||
if (isGrid != other.isGrid) return false
|
return isGrid == other.isGrid
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -76,7 +70,6 @@ sealed interface ExploreItem : ListModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
class EmptyHint(
|
class EmptyHint(
|
||||||
@DrawableRes icon: Int,
|
@DrawableRes icon: Int,
|
||||||
@StringRes textPrimary: Int,
|
@StringRes textPrimary: Int,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.sqlite.db.SupportSQLiteQuery
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.intellij.lang.annotations.Language
|
import org.intellij.lang.annotations.Language
|
||||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -71,12 +72,12 @@ abstract class FavouritesDao {
|
|||||||
)
|
)
|
||||||
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
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)
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
@Language("RoomSql")
|
@Language("RoomSql")
|
||||||
val query = SimpleSQLiteQuery(
|
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",
|
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
|
||||||
arrayOf<Any>(categoryId),
|
arrayOf<Any>(categoryId),
|
||||||
)
|
)
|
||||||
@@ -145,7 +146,7 @@ abstract class FavouritesDao {
|
|||||||
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
||||||
|
|
||||||
@RawQuery
|
@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")
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
|
||||||
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
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.toFavouriteCategory
|
||||||
import org.koitharu.kotatsu.favourites.data.toManga
|
import org.koitharu.kotatsu.favourites.data.toManga
|
||||||
import org.koitharu.kotatsu.favourites.data.toMangaList
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
|
||||||
@@ -66,11 +67,11 @@ class FavouritesRepository @Inject constructor(
|
|||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<String>>> {
|
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<Cover>>> {
|
||||||
return db.favouriteCategoriesDao.observeAll()
|
return db.favouriteCategoriesDao.observeAll()
|
||||||
.map {
|
.map {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val res = LinkedHashMap<FavouriteCategory, List<String>>()
|
val res = LinkedHashMap<FavouriteCategory, List<Cover>>()
|
||||||
for (entity in it) {
|
for (entity in it) {
|
||||||
val cat = entity.toFavouriteCategory()
|
val cat = entity.toFavouriteCategory()
|
||||||
res[cat] = db.favouritesDao.findCovers(
|
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 coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.util.ext.animatorDurationScale
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
|
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
import org.koitharu.kotatsu.core.util.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||||
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
import org.koitharu.kotatsu.core.util.ext.getThemeColor
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
@@ -53,10 +54,7 @@ fun categoryAD(
|
|||||||
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
|
ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153))
|
||||||
val fallback = ColorDrawable(Color.TRANSPARENT)
|
val fallback = ColorDrawable(Color.TRANSPARENT)
|
||||||
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
|
val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3)
|
||||||
val crossFadeDuration = (
|
val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt()
|
||||||
context.resources.getInteger(R.integer.config_defaultAnimTime) *
|
|
||||||
context.animatorDurationScale
|
|
||||||
).toInt()
|
|
||||||
itemView.setOnClickListener(eventListener)
|
itemView.setOnClickListener(eventListener)
|
||||||
itemView.setOnLongClickListener(eventListener)
|
itemView.setOnLongClickListener(eventListener)
|
||||||
itemView.setOnTouchListener(eventListener)
|
itemView.setOnTouchListener(eventListener)
|
||||||
@@ -77,9 +75,11 @@ fun categoryAD(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
repeat(coverViews.size) { i ->
|
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)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
fallback(fallback)
|
fallback(fallback)
|
||||||
|
source(cover?.mangaSource)
|
||||||
crossfade(crossFadeDuration * (i + 1))
|
crossfade(crossFadeDuration * (i + 1))
|
||||||
error(R.drawable.ic_error_placeholder)
|
error(R.drawable.ic_error_placeholder)
|
||||||
allowRgb565(true)
|
allowRgb565(true)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
package org.koitharu.kotatsu.favourites.ui.categories.adapter
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
class CategoryListModel(
|
class CategoryListModel(
|
||||||
val mangaCount: Int,
|
val mangaCount: Int,
|
||||||
val covers: List<String>,
|
val covers: List<Cover>,
|
||||||
val category: FavouriteCategory,
|
val category: FavouriteCategory,
|
||||||
val isReorderMode: Boolean,
|
val isReorderMode: Boolean,
|
||||||
) : ListModel {
|
) : ListModel {
|
||||||
@@ -21,9 +22,7 @@ class CategoryListModel(
|
|||||||
if (covers != other.covers) return false
|
if (covers != other.covers) return false
|
||||||
if (category.id != other.category.id) return false
|
if (category.id != other.category.id) return false
|
||||||
if (category.title != other.category.title) return false
|
if (category.title != other.category.title) return false
|
||||||
if (category.order != other.category.order) return false
|
return category.order == other.category.order
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -35,4 +34,4 @@ class CategoryListModel(
|
|||||||
result = 31 * result + category.order.hashCode()
|
result = 31 * result + category.order.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,29 @@ package org.koitharu.kotatsu.favourites.ui.categories.select
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
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.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.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
import org.koitharu.kotatsu.core.util.ext.withArgs
|
import org.koitharu.kotatsu.core.util.ext.withArgs
|
||||||
import org.koitharu.kotatsu.databinding.SheetFavoriteCategoriesBinding
|
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.adapter.MangaCategoriesAdapter
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
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
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class FavouriteCategoriesBottomSheet :
|
class FavouriteCategoriesSheet :
|
||||||
BaseBottomSheet<SheetFavoriteCategoriesBinding>(),
|
BaseAdaptiveSheet<SheetFavoriteCategoriesBinding>(),
|
||||||
OnListItemClickListener<MangaCategoryItem>,
|
OnListItemClickListener<MangaCategoryItem> {
|
||||||
View.OnClickListener,
|
|
||||||
Toolbar.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
private val viewModel: MangaCategoriesViewModel by viewModels()
|
private val viewModel: MangaCategoriesViewModel by viewModels()
|
||||||
|
|
||||||
@@ -40,13 +35,13 @@ class FavouriteCategoriesBottomSheet :
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
) = SheetFavoriteCategoriesBinding.inflate(inflater, container, false)
|
) = SheetFavoriteCategoriesBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
override fun onViewBindingCreated(binding: SheetFavoriteCategoriesBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(
|
||||||
|
binding: SheetFavoriteCategoriesBinding,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
adapter = MangaCategoriesAdapter(this)
|
adapter = MangaCategoriesAdapter(this)
|
||||||
binding.recyclerViewCategories.adapter = adapter
|
binding.recyclerViewCategories.adapter = adapter
|
||||||
binding.buttonDone.setOnClickListener(this)
|
|
||||||
binding.headerBar.toolbar.setOnMenuItemClickListener(this)
|
|
||||||
|
|
||||||
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
viewModel.content.observe(viewLifecycleOwner, this::onContentChanged)
|
||||||
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
|
viewModel.onError.observeEvent(viewLifecycleOwner, ::onError)
|
||||||
}
|
}
|
||||||
@@ -56,25 +51,11 @@ class FavouriteCategoriesBottomSheet :
|
|||||||
super.onDestroyView()
|
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) {
|
override fun onItemClick(item: MangaCategoryItem, view: View) {
|
||||||
viewModel.setChecked(item.id, !item.isChecked)
|
viewModel.setChecked(item.id, !item.isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onContentChanged(categories: List<MangaCategoryItem>) {
|
private fun onContentChanged(categories: List<ListModel>) {
|
||||||
adapter?.items = categories
|
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: Manga) = Companion.show(fm, listOf(manga))
|
||||||
|
|
||||||
fun show(fm: FragmentManager, manga: Collection<Manga>) = FavouriteCategoriesBottomSheet().withArgs(1) {
|
fun show(fm: FragmentManager, manga: Collection<Manga>) =
|
||||||
putParcelableArrayList(
|
FavouriteCategoriesSheet().withArgs(1) {
|
||||||
KEY_MANGA_LIST,
|
putParcelableArrayList(
|
||||||
manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withChapters = false) },
|
KEY_MANGA_LIST,
|
||||||
)
|
manga.mapTo(ArrayList(manga.size)) {
|
||||||
}.show(fm, TAG)
|
ParcelableManga(
|
||||||
|
it,
|
||||||
|
withChapters = false,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}.show(fm, TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
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.model.parcelable.ParcelableManga
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
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.favourites.ui.categories.select.model.MangaCategoryItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -23,17 +26,21 @@ class MangaCategoriesViewModel @Inject constructor(
|
|||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val manga = requireNotNull(savedStateHandle.get<List<ParcelableManga>>(KEY_MANGA_LIST)).map { it.manga }
|
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(),
|
favouritesRepository.observeCategories(),
|
||||||
observeCategoriesIds(),
|
observeCategoriesIds(),
|
||||||
) { all, checked ->
|
) { all, checked ->
|
||||||
all.map {
|
buildList(all.size + 1) {
|
||||||
MangaCategoryItem(
|
add(header)
|
||||||
id = it.id,
|
all.mapTo(this) {
|
||||||
name = it.title,
|
MangaCategoryItem(
|
||||||
isChecked = it.id in checked,
|
id = it.id,
|
||||||
)
|
name = it.title,
|
||||||
|
isChecked = it.id in checked,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
}.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 androidx.recyclerview.widget.DiffUtil
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
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.favourites.ui.categories.select.model.MangaCategoryItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
class MangaCategoriesAdapter(
|
class MangaCategoriesAdapter(
|
||||||
clickListener: OnListItemClickListener<MangaCategoryItem>
|
clickListener: OnListItemClickListener<MangaCategoryItem>,
|
||||||
) : AsyncListDifferDelegationAdapter<MangaCategoryItem>(DiffCallback()) {
|
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
|
delegatesManager.addDelegate(mangaCategoryAD(clickListener))
|
||||||
|
.addDelegate(categoriesHeaderAD())
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<MangaCategoryItem>() {
|
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||||
override fun areItemsTheSame(
|
override fun areItemsTheSame(
|
||||||
oldItem: MangaCategoryItem,
|
oldItem: ListModel,
|
||||||
newItem: MangaCategoryItem
|
newItem: ListModel,
|
||||||
): Boolean = oldItem.id == newItem.id
|
): 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(
|
override fun areContentsTheSame(
|
||||||
oldItem: MangaCategoryItem,
|
oldItem: ListModel,
|
||||||
newItem: MangaCategoryItem
|
newItem: ListModel,
|
||||||
): Boolean = oldItem == newItem
|
): Boolean = oldItem == newItem
|
||||||
|
|
||||||
override fun getChangePayload(
|
override fun getChangePayload(
|
||||||
oldItem: MangaCategoryItem,
|
oldItem: ListModel,
|
||||||
newItem: MangaCategoryItem
|
newItem: ListModel,
|
||||||
): Any? {
|
): Any? {
|
||||||
if (oldItem.isChecked != newItem.isChecked) {
|
if (oldItem is MangaCategoryItem && newItem is MangaCategoryItem && oldItem.isChecked != newItem.isChecked) {
|
||||||
return newItem.isChecked
|
return newItem.isChecked
|
||||||
}
|
}
|
||||||
return super.getChangePayload(oldItem, newItem)
|
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.core.ui.list.OnListItemClickListener
|
||||||
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
|
import org.koitharu.kotatsu.databinding.ItemCheckableNewBinding
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
fun mangaCategoryAD(
|
fun mangaCategoryAD(
|
||||||
clickListener: OnListItemClickListener<MangaCategoryItem>
|
clickListener: OnListItemClickListener<MangaCategoryItem>,
|
||||||
) = adapterDelegateViewBinding<MangaCategoryItem, MangaCategoryItem, ItemCheckableNewBinding>(
|
) = adapterDelegateViewBinding<MangaCategoryItem, ListModel, ItemCheckableNewBinding>(
|
||||||
{ inflater, parent -> ItemCheckableNewBinding.inflate(inflater, parent, false) },
|
{ 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
|
package org.koitharu.kotatsu.favourites.ui.categories.select.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
data class MangaCategoryItem(
|
data class MangaCategoryItem(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
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 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.CoroutineStart
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -17,21 +19,40 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
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.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
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 org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
import java.util.LinkedList
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FilterCoordinator(
|
@ViewModelScoped
|
||||||
private val repository: RemoteMangaRepository,
|
class FilterCoordinator @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
dataRepository: MangaDataRepository,
|
dataRepository: MangaDataRepository,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val searchRepository: MangaSearchRepository,
|
||||||
) : OnFilterChangedListener {
|
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 val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet()))
|
||||||
private var searchQuery = MutableStateFlow("")
|
private var searchQuery = MutableStateFlow("")
|
||||||
private val localTags = SuspendLazy {
|
private val localTags = SuspendLazy {
|
||||||
@@ -39,13 +60,23 @@ class FilterCoordinator(
|
|||||||
}
|
}
|
||||||
private var availableTagsDeferred = loadTagsAsync()
|
private var availableTagsDeferred = loadTagsAsync()
|
||||||
|
|
||||||
val items: StateFlow<List<FilterItem>> = getItemsFlow()
|
override val filterItems: StateFlow<List<ListModel>> = getItemsFlow()
|
||||||
.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(FilterItem.Loading))
|
.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 {
|
init {
|
||||||
observeState()
|
observeState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun applyFilter(tags: Set<MangaTag>) {
|
||||||
|
setTags(tags)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSortItemClick(item: FilterItem.Sort) {
|
override fun onSortItemClick(item: FilterItem.Sort) {
|
||||||
currentState.update { oldValue ->
|
currentState.update { oldValue ->
|
||||||
FilterState(item.order, oldValue.tags)
|
FilterState(item.order, oldValue.tags)
|
||||||
@@ -91,6 +122,14 @@ class FilterCoordinator(
|
|||||||
searchQuery.value = query
|
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(
|
private fun getItemsFlow() = combine(
|
||||||
getTagsAsFlow(),
|
getTagsAsFlow(),
|
||||||
currentState,
|
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
|
@WorkerThread
|
||||||
private fun buildFilterList(
|
private fun buildFilterList(
|
||||||
allTags: TagsWrapper,
|
allTags: TagsWrapper,
|
||||||
state: FilterState,
|
state: FilterState,
|
||||||
query: String,
|
query: String,
|
||||||
): List<FilterItem> {
|
): List<ListModel> {
|
||||||
val sortOrders = repository.sortOrders.sortedBy { it.ordinal }
|
val sortOrders = repository.sortOrders.sortedBy { it.ordinal }
|
||||||
val tags = mergeTags(state.tags, allTags.tags).toList()
|
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 (query.isEmpty()) {
|
||||||
if (sortOrders.isNotEmpty()) {
|
if (sortOrders.isNotEmpty()) {
|
||||||
list.add(FilterItem.Header(R.string.sort_order, 0))
|
list.add(ListHeader(R.string.sort_order, 0, null))
|
||||||
sortOrders.mapTo(list) {
|
sortOrders.mapTo(list) {
|
||||||
FilterItem.Sort(it, isSelected = it == state.sortOrder)
|
FilterItem.Sort(it, isSelected = it == state.sortOrder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) {
|
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) {
|
tags.mapTo(list) {
|
||||||
FilterItem.Tag(it, isChecked = it in state.tags)
|
FilterItem.Tag(it, isChecked = it in state.tags)
|
||||||
}
|
}
|
||||||
@@ -135,7 +216,7 @@ class FilterCoordinator(
|
|||||||
if (allTags.isError) {
|
if (allTags.isError) {
|
||||||
list.add(FilterItem.Error(R.string.filter_load_error))
|
list.add(FilterItem.Error(R.string.filter_load_error))
|
||||||
} else if (allTags.isLoading) {
|
} else if (allTags.isLoading) {
|
||||||
list.add(FilterItem.Loading)
|
list.add(LoadingFooter())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tags.mapNotNullTo(list) {
|
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 {
|
interface OnFilterChangedListener {
|
||||||
|
|
||||||
fun onSortItemClick(item: FilterItem.Sort)
|
fun onSortItemClick(item: FilterItem.Sort)
|
||||||
|
|
||||||
fun onTagItemClick(item: FilterItem.Tag)
|
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.core.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
|
||||||
class ListHeader2(
|
class FilterHeaderModel(
|
||||||
val chips: Collection<ChipsView.ChipModel>,
|
val chips: Collection<ChipsView.ChipModel>,
|
||||||
val sortOrder: SortOrder?,
|
val sortOrder: SortOrder?,
|
||||||
val hasSelectedTags: Boolean,
|
val hasSelectedTags: Boolean,
|
||||||
) : ListModel {
|
) : ListModel {
|
||||||
|
|
||||||
|
val textSummary: String
|
||||||
|
get() = chips.mapNotNull { if (it.isChecked) it.title else null }.joinToString()
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
other as ListHeader2
|
other as FilterHeaderModel
|
||||||
|
|
||||||
if (chips != other.chips) return false
|
if (chips != other.chips) return false
|
||||||
return sortOrder == other.sortOrder
|
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.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
@@ -15,9 +15,7 @@ class FilterState(
|
|||||||
other as FilterState
|
other as FilterState
|
||||||
|
|
||||||
if (sortOrder != other.sortOrder) return false
|
if (sortOrder != other.sortOrder) return false
|
||||||
if (tags != other.tags) return false
|
return tags == other.tags
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -25,4 +23,4 @@ class FilterState(
|
|||||||
result = 31 * result + tags.hashCode()
|
result = 31 * result + tags.hashCode()
|
||||||
return result
|
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.R
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
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.ext.setValueRounded
|
||||||
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
|
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
|
||||||
import org.koitharu.kotatsu.databinding.DialogListModeBinding
|
import org.koitharu.kotatsu.databinding.DialogListModeBinding
|
||||||
@@ -19,7 +19,7 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ListModeBottomSheet :
|
class ListModeBottomSheet :
|
||||||
BaseBottomSheet<DialogListModeBinding>(),
|
BaseAdaptiveSheet<DialogListModeBinding>(),
|
||||||
Slider.OnChangeListener,
|
Slider.OnChangeListener,
|
||||||
MaterialButtonToggleGroup.OnButtonCheckedListener {
|
MaterialButtonToggleGroup.OnButtonCheckedListener {
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
|||||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
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
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
@@ -296,7 +296,7 @@ abstract class MangaListFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.action_favourite -> {
|
R.id.action_favourite -> {
|
||||||
FavouriteCategoriesBottomSheet.show(childFragmentManager, selectedItems)
|
FavouriteCategoriesSheet.show(childFragmentManager, selectedItems)
|
||||||
mode.finish()
|
mode.finish()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.adapter
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
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.isAnimationsEnabled
|
||||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
|
||||||
import org.koitharu.kotatsu.databinding.ItemHeader2Binding
|
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
|
||||||
|
@Deprecated("")
|
||||||
fun listHeader2AD(
|
fun listHeader2AD(
|
||||||
listener: MangaListListener,
|
listener: MangaListListener,
|
||||||
) = adapterDelegateViewBinding<ListHeader2, ListModel, ItemHeader2Binding>(
|
) = adapterDelegateViewBinding<FilterHeaderModel, ListModel, FragmentFilterHeaderBinding>(
|
||||||
{ layoutInflater, parent -> ItemHeader2Binding.inflate(layoutInflater, parent, false) },
|
{ layoutInflater, parent -> FragmentFilterHeaderBinding.inflate(layoutInflater, parent, false) },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var ignoreChecking = false
|
var ignoreChecking = false
|
||||||
binding.textViewFilter.setOnClickListener {
|
|
||||||
listener.onFilterClick(it)
|
|
||||||
}
|
|
||||||
binding.chipsTags.setOnCheckedStateChangeListener { _, _ ->
|
binding.chipsTags.setOnCheckedStateChangeListener { _, _ ->
|
||||||
if (!ignoreChecking) {
|
if (!ignoreChecking) {
|
||||||
listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java))
|
listener.onUpdateFilter(binding.chipsTags.getCheckedData(MangaTag::class.java))
|
||||||
@@ -36,6 +32,5 @@ fun listHeader2AD(
|
|||||||
ignoreChecking = true
|
ignoreChecking = true
|
||||||
binding.chipsTags.setChips(item.chips) // TODO use recyclerview
|
binding.chipsTags.setChips(item.chips) // TODO use recyclerview
|
||||||
ignoreChecking = false
|
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 com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||||
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
|
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.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
|
||||||
@@ -22,3 +23,12 @@ fun listHeaderAD(
|
|||||||
binding.buttonMore.setTextAndVisible(item.buttonTextRes)
|
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 coil.ImageLoader
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
|
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.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader2
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
||||||
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
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)
|
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(
|
ChipsView.ChipModel(
|
||||||
tint = tagHighlighter?.getTint(it) ?: 0,
|
tint = tagHighlighter?.getTint(it) ?: 0,
|
||||||
title = it.title,
|
title = it.title,
|
||||||
|
icon = 0,
|
||||||
isCheckable = false,
|
isCheckable = false,
|
||||||
isChecked = false,
|
isChecked = false,
|
||||||
data = it,
|
data = it,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
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.CompositeMutex
|
||||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.local.data.input.LocalMangaInput
|
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.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.EnumSet
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -37,11 +39,20 @@ private const val MAX_PARALLELISM = 4
|
|||||||
class LocalMangaRepository @Inject constructor(
|
class LocalMangaRepository @Inject constructor(
|
||||||
private val storageManager: LocalStorageManager,
|
private val storageManager: LocalStorageManager,
|
||||||
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
||||||
|
private val settings: AppSettings,
|
||||||
) : MangaRepository {
|
) : MangaRepository {
|
||||||
|
|
||||||
override val source = MangaSource.LOCAL
|
override val source = MangaSource.LOCAL
|
||||||
private val locks = CompositeMutex<Long>()
|
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> {
|
override suspend fun getList(offset: Int, query: String): List<Manga> {
|
||||||
if (offset > 0) {
|
if (offset > 0) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
@@ -137,8 +148,6 @@ class LocalMangaRepository @Inject constructor(
|
|||||||
}.firstOrNull()?.getManga()
|
}.firstOrNull()?.getManga()
|
||||||
}
|
}
|
||||||
|
|
||||||
override val sortOrders = setOf(SortOrder.ALPHABETICAL, SortOrder.RATING)
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage) = page.url
|
override suspend fun getPageUrl(page: MangaPage) = page.url
|
||||||
|
|
||||||
override suspend fun getTags() = emptySet<MangaTag>()
|
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) {
|
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) {
|
||||||
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
||||||
try {
|
try {
|
||||||
file.sink(append = false).buffer().use {
|
val bytes = file.sink(append = false).buffer().use {
|
||||||
it.writeAllCancellable(source)
|
it.writeAllCancellable(source)
|
||||||
}
|
}
|
||||||
|
check(bytes != 0L) { "No data has been written" }
|
||||||
lruCache.get().put(url, file)
|
lruCache.get().put(url, file)
|
||||||
} finally {
|
} finally {
|
||||||
file.delete()
|
file.delete()
|
||||||
|
|||||||
@@ -130,7 +130,6 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
}
|
}
|
||||||
val cbz = root.listFilesRecursive(CbzFilter()).firstOrNull() ?: return null
|
val cbz = root.listFilesRecursive(CbzFilter()).firstOrNull() ?: return null
|
||||||
return ZipFile(cbz).use { zip ->
|
return ZipFile(cbz).use { zip ->
|
||||||
val filter = ImageFileFilter()
|
|
||||||
zip.entries().asSequence()
|
zip.entries().asSequence()
|
||||||
.firstOrNull { x -> !x.isDirectory && filter.accept(x) }
|
.firstOrNull { x -> !x.isDirectory && filter.accept(x) }
|
||||||
?.let { entry -> zipUri(cbz, entry.name) }
|
?.let { entry -> zipUri(cbz, entry.name) }
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.viewModels
|
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.ShareHelper
|
||||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
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.databinding.FragmentListBinding
|
||||||
|
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
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>()
|
override val viewModel by viewModels<LocalListViewModel>()
|
||||||
|
|
||||||
@@ -35,11 +37,7 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onFilterClick(view: View?) {
|
override fun onFilterClick(view: View?) {
|
||||||
super.onFilterClick(view)
|
FilterSheetFragment.show(childFragmentManager)
|
||||||
val menu = PopupMenu(requireContext(), view ?: requireViewBinding().recyclerView)
|
|
||||||
menu.inflate(R.menu.popup_order)
|
|
||||||
menu.setOnMenuItemClickListener(this)
|
|
||||||
menu.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrolledToEnd() = Unit
|
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) {
|
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
|
||||||
MaterialAlertDialogBuilder(context ?: return)
|
MaterialAlertDialogBuilder(context ?: return)
|
||||||
.setTitle(R.string.delete_manga)
|
.setTitle(R.string.delete_manga)
|
||||||
@@ -96,6 +83,8 @@ class LocalListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickListener
|
|||||||
|
|
||||||
companion object {
|
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
|
package org.koitharu.kotatsu.local.ui
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.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.R
|
||||||
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
import org.koitharu.kotatsu.core.parser.MangaTagHighlighter
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
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.MutableEventFlow
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
|
||||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
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.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.data.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
|
||||||
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 javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LocalListViewModel @Inject constructor(
|
class LocalListViewModel @Inject constructor(
|
||||||
private val repository: LocalMangaRepository,
|
savedStateHandle: SavedStateHandle,
|
||||||
private val historyRepository: HistoryRepository,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val trackingRepository: TrackingRepository,
|
filter: FilterCoordinator,
|
||||||
private val settings: AppSettings,
|
tagHighlighter: MangaTagHighlighter,
|
||||||
private val tagHighlighter: MangaTagHighlighter,
|
settings: AppSettings,
|
||||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
|
||||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
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 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 {
|
init {
|
||||||
onRefresh()
|
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
localStorageChanges
|
localStorageChanges
|
||||||
.collectLatest {
|
.collect {
|
||||||
if (refreshJob?.isActive != true) {
|
loadList(filter.snapshot(), append = false).join()
|
||||||
doRefresh()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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>) {
|
fun delete(ids: Set<Long>) {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
deleteLocalMangaUseCase(ids)
|
deleteLocalMangaUseCase(ids)
|
||||||
@@ -125,60 +59,12 @@ class LocalListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doRefresh() {
|
override fun createEmptyState(canResetFilter: Boolean): EmptyState {
|
||||||
try {
|
return EmptyState(
|
||||||
listError.value = null
|
icon = R.drawable.ic_empty_local,
|
||||||
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
|
textPrimary = R.string.text_local_holder_primary,
|
||||||
} catch (e: CancellationException) {
|
textSecondary = R.string.text_local_holder_secondary,
|
||||||
throw e
|
actionStringRes = R.string._import,
|
||||||
} 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 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 =
|
navigationDelegate =
|
||||||
MainNavigationDelegate(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager)
|
MainNavigationDelegate(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager)
|
||||||
navigationDelegate.addOnFragmentChangedListener(this)
|
navigationDelegate.addOnFragmentChangedListener(this)
|
||||||
navigationDelegate.onCreate(savedInstanceState)
|
navigationDelegate.onCreate()
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(ExitCallback(this, viewBinding.container))
|
onBackPressedDispatcher.addCallback(ExitCallback(this, viewBinding.container))
|
||||||
onBackPressedDispatcher.addCallback(navigationDelegate)
|
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