Migrate to AdapterDelegates

This commit is contained in:
Koitharu
2020-11-19 20:43:36 +02:00
parent 7d24286c55
commit 7e76e10591
44 changed files with 613 additions and 300 deletions

View File

@@ -70,6 +70,7 @@ dependencies {
implementation 'androidx.activity:activity-ktx:1.2.0-beta01' implementation 'androidx.activity:activity-ktx:1.2.0-beta01'
implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01' implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06'
@@ -88,6 +89,9 @@ dependencies {
implementation 'com.squareup.okio:okio:2.9.0' implementation 'com.squareup.okio:okio:2.9.0'
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:4.3.0'
implementation 'org.koin:koin-android:2.2.0' implementation 'org.koin:koin-android:2.2.0'
implementation 'org.koin:koin-android-viewmodel:2.2.0' implementation 'org.koin:koin-android-viewmodel:2.2.0'
implementation 'io.coil-kt:coil-base:1.0.0' implementation 'io.coil-kt:coil-base:1.0.0'
@@ -97,6 +101,6 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
testImplementation 'junit:junit:4.13.1' testImplementation 'junit:junit:4.13.1'
testImplementation 'org.json:json:20200518' testImplementation 'org.json:json:20201115'
testImplementation 'org.koin:koin-test:2.2.0-rc-2' testImplementation 'org.koin:koin-test:2.2.0-rc-2'
} }

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.base.ui.list
import android.view.View
interface OnListItemClickListener<I> {
fun onItemClick(item: I, view: View)
fun onItemLongClick(item: I, view: View) = false
}

View File

@@ -7,6 +7,9 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.sendBlocking
import kotlinx.coroutines.flow.callbackFlow
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.delegates.prefs.* import org.koitharu.kotatsu.utils.delegates.prefs.*
@@ -117,6 +120,16 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
prefs.unregisterOnSharedPreferenceChangeListener(listener) prefs.unregisterOnSharedPreferenceChangeListener(listener)
} }
fun observe() = callbackFlow<String> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
sendBlocking(key)
}
prefs.registerOnSharedPreferenceChangeListener(listener)
awaitClose {
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
}
companion object { companion object {
const val PAGE_SWITCH_TAPS = "taps" const val PAGE_SWITCH_TAPS = "taps"

View File

@@ -7,5 +7,5 @@ import org.koitharu.kotatsu.details.ui.DetailsViewModel
val detailsModule val detailsModule
get() = module { get() = module {
viewModel { DetailsViewModel(get(), get(), get(), get(), get(), get()) } viewModel { DetailsViewModel(get(), get(), get(), get(), get(), get(), get()) }
} }

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.domain.OnFavouritesChangeListener import org.koitharu.kotatsu.favourites.domain.OnFavouritesChangeListener
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
@@ -28,8 +29,11 @@ class DetailsViewModel(
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val searchRepository: MangaSearchRepository, private val searchRepository: MangaSearchRepository,
private val mangaDataRepository: MangaDataRepository private val mangaDataRepository: MangaDataRepository,
) : MangaListViewModel(), OnHistoryChangeListener, OnFavouritesChangeListener { settings: AppSettings
) : MangaListViewModel(settings), OnHistoryChangeListener, OnFavouritesChangeListener {
override val content = MutableLiveData<List<Any>>()
val mangaData = MutableLiveData<Manga>() val mangaData = MutableLiveData<Manga>()
val newChapters = MutableLiveData<Int>(0) val newChapters = MutableLiveData<Int>(0)

View File

@@ -12,7 +12,7 @@ val favouritesModule
single { FavouritesRepository(get()) } single { FavouritesRepository(get()) }
viewModel { (categoryId: Long) -> viewModel { (categoryId: Long) ->
FavouritesListViewModel(categoryId, get()) FavouritesListViewModel(categoryId, get(), get())
} }
viewModel { FavouritesCategoriesViewModel(get()) } viewModel { FavouritesCategoriesViewModel(get()) }
} }

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.favourites.data package org.koitharu.kotatsu.favourites.data
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao @Dao
@@ -10,6 +11,10 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at") @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
abstract suspend fun findAll(): List<FavouriteManga> abstract suspend fun findAll(): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
abstract fun observeAll(): Flow<List<FavouriteManga>>
@Transaction @Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset") @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga> abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
@@ -18,6 +23,10 @@ abstract class FavouritesDao {
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at") @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga> abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
abstract fun observeAll(categoryId: Long): Flow<List<FavouriteManga>>
@Transaction @Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset") @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga> abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.favourites.domain
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@@ -11,6 +12,7 @@ import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
class FavouritesRepository(private val db: MangaDatabase) { class FavouritesRepository(private val db: MangaDatabase) {
@@ -20,6 +22,11 @@ class FavouritesRepository(private val db: MangaDatabase) {
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
} }
fun observeAll(): Flow<List<Manga>> {
return db.favouritesDao.observeAll()
.mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
suspend fun getAllManga(offset: Int): List<Manga> { suspend fun getAllManga(offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(offset, 20) val entities = db.favouritesDao.findAll(offset, 20)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
@@ -30,6 +37,11 @@ class FavouritesRepository(private val db: MangaDatabase) {
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
} }
fun observeAll(categoryId: Long): Flow<List<Manga>> {
return db.favouritesDao.observeAll(categoryId)
.mapItems { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
}
suspend fun getManga(categoryId: Long, offset: Int): List<Manga> { suspend fun getManga(categoryId: Long, offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId, offset, 20) val entities = db.favouritesDao.findAll(categoryId, offset, 20)
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }

View File

@@ -20,9 +20,7 @@ class FavouritesListFragment : MangaListFragment() {
private val categoryId: Long private val categoryId: Long
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L
override fun onRequestMoreItems(offset: Int) { override fun onRequestMoreItems(offset: Int) = Unit
viewModel.loadList(offset)
}
override fun setUpEmptyListHolder() { override fun setUpEmptyListHolder() {
textView_holder.setText( textView_holder.setText(

View File

@@ -1,28 +1,34 @@
package org.koitharu.kotatsu.favourites.ui.list package org.koitharu.kotatsu.favourites.ui.list
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
import org.koitharu.kotatsu.list.ui.model.toListModel
class FavouritesListViewModel( class FavouritesListViewModel(
private val categoryId: Long, private val categoryId: Long,
private val repository: FavouritesRepository private val repository: FavouritesRepository,
) : MangaListViewModel() { settings: AppSettings
) : MangaListViewModel(settings) {
fun loadList(offset: Int) { override val content = combine(
launchLoadingJob { if (categoryId == 0L) repository.observeAll() else repository.observeAll(categoryId),
val list = if (categoryId == 0L) { createListModeFlow()
repository.getAllManga(offset = offset) ) { list, mode ->
} else { when (mode) {
repository.getManga(categoryId = categoryId, offset = offset) ListMode.LIST -> list.map { it.toListModel() }
} ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
if (offset == 0) { ListMode.GRID -> list.map { it.toGridModel() }
content.value = list
} else {
content.value = content.value.orEmpty() + list
}
} }
} }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
fun removeFromFavourites(manga: Manga) { fun removeFromFavourites(manga: Manga) {
launchJob { launchJob {
@@ -31,7 +37,6 @@ class FavouritesListViewModel(
} else { } else {
repository.removeFromCategory(manga, categoryId) repository.removeFromCategory(manga, categoryId)
} }
content.value = content.value?.filterNot { it.id == manga.id }
} }
} }
} }

View File

@@ -10,5 +10,5 @@ val historyModule
get() = module { get() = module {
single { HistoryRepository(get()) } single { HistoryRepository(get()) }
viewModel { HistoryListViewModel(get(), androidContext()) } viewModel { HistoryListViewModel(get(), androidContext(), get()) }
} }

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.history.data package org.koitharu.kotatsu.history.data
import androidx.room.* import androidx.room.*
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@@ -14,6 +15,10 @@ abstract class HistoryDao {
@Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset") @Query("SELECT * FROM history ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga> abstract suspend fun findAll(offset: Int, limit: Int): List<HistoryWithManga>
@Transaction
@Query("SELECT * FROM history ORDER BY updated_at DESC")
abstract fun observeAll(): Flow<List<HistoryWithManga>>
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)") @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM history)")
abstract suspend fun findAllManga(): List<MangaEntity> abstract suspend fun findAllManga(): List<MangaEntity>

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.history.domain
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@@ -13,6 +14,7 @@ import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.mapItems
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
class HistoryRepository(private val db: MangaDatabase) : KoinComponent { class HistoryRepository(private val db: MangaDatabase) : KoinComponent {
@@ -24,6 +26,12 @@ class HistoryRepository(private val db: MangaDatabase) : KoinComponent {
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) } return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
} }
fun observeAll(): Flow<List<Manga>> {
return db.historyDao.observeAll().mapItems {
it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag))
}
}
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) { suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction { db.withTransaction {

View File

@@ -18,14 +18,16 @@ class HistoryListFragment : MangaListFragment() {
override val viewModel by viewModel<HistoryListViewModel>() override val viewModel by viewModel<HistoryListViewModel>()
init {
isSwipeRefreshEnabled = false
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved) viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
} }
override fun onRequestMoreItems(offset: Int) { override fun onRequestMoreItems(offset: Int) = Unit
viewModel.loadList(offset)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.opt_history, menu) inflater.inflate(R.menu.opt_history, menu)

View File

@@ -2,34 +2,43 @@ package org.koitharu.kotatsu.history.ui
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
class HistoryListViewModel( class HistoryListViewModel(
private val repository: HistoryRepository, private val repository: HistoryRepository,
private val context: Context //todo create ShortcutRepository private val context: Context //todo create ShortcutRepository
) : MangaListViewModel() { , settings: AppSettings
) : MangaListViewModel(settings) {
val onItemRemoved = SingleLiveEvent<Manga>() val onItemRemoved = SingleLiveEvent<Manga>()
fun loadList(offset: Int) { override val content = combine(
launchLoadingJob { repository.observeAll(),
val list = repository.getList(offset = offset) createListModeFlow()
if (offset == 0) { ) { list, mode ->
content.value = list when(mode) {
} else { ListMode.LIST -> list.map { it.toListModel() }
content.value = content.value.orEmpty() + list ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
} ListMode.GRID -> list.map { it.toGridModel() }
} }
} }.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
fun clearHistory() { fun clearHistory() {
launchLoadingJob { launchLoadingJob {
repository.clear() repository.clear()
content.value = emptyList()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
MangaShortcut.clearAppShortcuts(context) MangaShortcut.clearAppShortcuts(context)
} }
@@ -39,7 +48,6 @@ class HistoryListViewModel(
fun removeFromHistory(manga: Manga) { fun removeFromHistory(manga: Manga) {
launchJob { launchJob {
repository.delete(manga) repository.delete(manga)
content.value = content.value?.filterNot { it.id == manga.id }
onItemRemoved.call(manga) onItemRemoved.call(manga)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
MangaShortcut(manga).removeAppShortcut(context) MangaShortcut(manga).removeAppShortcut(context)

View File

@@ -1,35 +0,0 @@
package org.koitharu.kotatsu.list.ui
import android.view.ViewGroup
import coil.ImageLoader
import coil.request.Disposable
import kotlinx.android.synthetic.main.item_manga_grid.*
import org.koin.core.component.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
class MangaGridHolder(parent: ViewGroup) :
BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_grid) {
private val coil by inject<ImageLoader>()
private var imageRequest: Disposable? = null
override fun onBind(data: Manga, extra: MangaHistory?) {
textView_title.text = data.title
imageRequest?.dispose()
imageRequest = imageView_cover.newImageRequest(data.coverUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.enqueueWith(coil)
}
override fun onRecycled() {
imageRequest?.dispose()
imageView_cover.setImageDrawable(null)
}
}

View File

@@ -1,26 +0,0 @@
package org.koitharu.kotatsu.list.ui
import android.view.ViewGroup
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.prefs.ListMode
class MangaListAdapter(onItemClickListener: OnRecyclerItemClickListener<Manga>) :
BaseRecyclerAdapter<Manga, MangaHistory?>(onItemClickListener) {
var listMode: ListMode = ListMode.LIST
override fun onCreateViewHolder(parent: ViewGroup) = when (listMode) {
ListMode.LIST -> MangaListHolder(parent)
ListMode.DETAILED_LIST -> MangaListDetailsHolder(
parent
)
ListMode.GRID -> MangaGridHolder(parent)
}
override fun onGetItemId(item: Manga) = item.id
override fun getExtra(item: Manga, position: Int): MangaHistory? = null
}

View File

@@ -1,51 +0,0 @@
package org.koitharu.kotatsu.list.ui
import android.annotation.SuppressLint
import android.view.ViewGroup
import androidx.core.view.isVisible
import coil.ImageLoader
import coil.request.Disposable
import kotlinx.android.synthetic.main.item_manga_list_details.*
import org.koin.core.component.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
import kotlin.math.roundToInt
class MangaListDetailsHolder(
parent: ViewGroup
) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_list_details) {
private val coil by inject<ImageLoader>()
private var imageRequest: Disposable? = null
@SuppressLint("SetTextI18n")
override fun onBind(data: Manga, extra: MangaHistory?) {
imageRequest?.dispose()
textView_title.text = data.title
textView_subtitle.textAndVisible = data.altTitle
imageRequest = imageView_cover.newImageRequest(data.coverUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.enqueueWith(coil)
if (data.rating == Manga.NO_RATING) {
textView_rating.isVisible = false
} else {
textView_rating.text = "${(data.rating * 10).roundToInt()}/10"
textView_rating.isVisible = true
}
textView_tags.text = data.tags.joinToString(", ") {
it.title
}
}
override fun onRecycled() {
imageRequest?.dispose()
imageView_cover.setImageDrawable(null)
}
}

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.list.ui package org.koitharu.kotatsu.list.ui
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
@@ -9,16 +8,18 @@ import androidx.core.view.GravityCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.ProgressBarAdapter
import org.koitharu.kotatsu.base.ui.list.decor.ItemTypeDividerDecoration import org.koitharu.kotatsu.base.ui.list.decor.ItemTypeDividerDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SectionItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SectionItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
@@ -26,28 +27,21 @@ import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaFilter
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.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.filter.FilterAdapter import org.koitharu.kotatsu.list.ui.filter.FilterAdapter
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.UiUtils
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
abstract class MangaListFragment : BaseFragment(R.layout.fragment_list), abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>, PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener,
SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener,
SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener { SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener {
private val settings by inject<AppSettings>()
private val adapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true)
.setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build()
private var adapter: MangaListAdapter? = null private var adapter: MangaListAdapter? = null
private var progressAdapter: ProgressBarAdapter? = null
private var paginationListener: PaginationScrollListener? = null private var paginationListener: PaginationScrollListener? = null
private val spanResolver: MangaListSpanResolver? = null
protected var isSwipeRefreshEnabled = true protected var isSwipeRefreshEnabled = true
protected abstract val viewModel: MangaListViewModel protected abstract val viewModel: MangaListViewModel
@@ -60,31 +54,27 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
adapter = MangaListAdapter(this) adapter = MangaListAdapter(get(), this)
progressAdapter = ProgressBarAdapter()
paginationListener = PaginationScrollListener(4, this) paginationListener = PaginationScrollListener(4, this)
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
initListMode(settings.listMode) recyclerView.adapter = adapter
recyclerView.addOnScrollListener(paginationListener!!) recyclerView.addOnScrollListener(paginationListener!!)
swipeRefreshLayout.setOnRefreshListener(this) swipeRefreshLayout.setOnRefreshListener(this)
recyclerView_filter.setHasFixedSize(true) recyclerView_filter.setHasFixedSize(true)
recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context)) recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context))
recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this)) recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this))
settings.subscribe(this)
if (savedInstanceState == null) {
onRequestMoreItems(0)
}
viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.filter.observe(viewLifecycleOwner, ::onInitFilter) viewModel.filter.observe(viewLifecycleOwner, ::onInitFilter)
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
} }
override fun onDestroyView() { override fun onDestroyView() {
adapter = null adapter = null
progressAdapter = null
paginationListener = null paginationListener = null
settings.unsubscribe(this)
super.onDestroyView() super.onDestroyView()
} }
@@ -111,11 +101,11 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
} }
override fun onItemClick(item: Manga, position: Int, view: View) { override fun onItemClick(item: Manga, view: View) {
startActivity(DetailsActivity.newIntent(context ?: return, item)) startActivity(DetailsActivity.newIntent(context ?: return, item))
} }
override fun onItemLongClick(item: Manga, position: Int, view: View): Boolean { override fun onItemLongClick(item: Manga, view: View): Boolean {
val menu = PopupMenu(context ?: return false, view) val menu = PopupMenu(context ?: return false, view)
onCreatePopupMenu(menu.menuInflater, menu.menu, item) onCreatePopupMenu(menu.menuInflater, menu.menu, item)
return if (menu.menu.hasVisibleItems()) { return if (menu.menu.hasVisibleItems()) {
@@ -135,16 +125,14 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
onRequestMoreItems(0) onRequestMoreItems(0)
} }
private fun onListChanged(list: List<Manga>) { private fun onListChanged(list: List<Any>) {
paginationListener?.reset() adapter?.items = list
adapter?.replaceData(list)
if (list.isEmpty()) { if (list.isEmpty()) {
setUpEmptyListHolder() setUpEmptyListHolder()
layout_holder.isVisible = true layout_holder.isVisible = true
} else { } else {
layout_holder.isVisible = false layout_holder.isVisible = false
} }
progressAdapter?.isProgressVisible = list.isNotEmpty()
recyclerView.callOnScrollListeners() recyclerView.callOnScrollListeners()
} }
@@ -179,17 +167,6 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
} }
} }
@CallSuper
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if (context == null) {
return
}
when (key) {
AppSettings.KEY_LIST_MODE -> initListMode(settings.listMode)
AppSettings.KEY_GRID_SIZE -> UiUtils.SpanCountResolver.update(recyclerView)
}
}
protected fun onInitFilter(config: MangaFilterConfig) { protected fun onInitFilter(config: MangaFilterConfig) {
recyclerView_filter.adapter = FilterAdapter( recyclerView_filter.adapter = FilterAdapter(
sortOrders = config.sortOrders, sortOrders = config.sortOrders,
@@ -220,14 +197,43 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
textView_holder.setText(R.string.nothing_found) textView_holder.setText(R.string.nothing_found)
} }
private fun onGridScaleChanged(scale: Float) {
UiUtils.SpanCountResolver.update(recyclerView)
}
private fun onListModeChanged(mode: ListMode) {
with(recyclerView) {
clearItemDecorations()
when (mode) {
ListMode.LIST -> {
layoutManager = LinearLayoutManager(context)
addItemDecoration(
DividerItemDecoration(
context,
RecyclerView.VERTICAL
)
)
}
ListMode.DETAILED_LIST -> {
layoutManager = LinearLayoutManager(context)
}
ListMode.GRID -> {
layoutManager = GridLayoutManager(context, 3)
addItemDecoration(
SpacingItemDecoration(
resources.getDimensionPixelOffset(R.dimen.grid_spacing)
)
)
}
}
}
}
private fun initListMode(mode: ListMode) { private fun initListMode(mode: ListMode) {
val ctx = context ?: return val ctx = context ?: return
val position = recyclerView.firstItem
recyclerView.adapter = null
recyclerView.layoutManager = null recyclerView.layoutManager = null
recyclerView.clearItemDecorations() recyclerView.clearItemDecorations()
recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver)
adapter?.listMode = mode
recyclerView.layoutManager = when (mode) { recyclerView.layoutManager = when (mode) {
ListMode.GRID -> { ListMode.GRID -> {
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply { GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
@@ -239,8 +245,6 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
} }
else -> LinearLayoutManager(ctx) else -> LinearLayoutManager(ctx)
} }
recyclerView.recycledViewPool.clear()
recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.addItemDecoration( recyclerView.addItemDecoration(
when (mode) { when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)
@@ -254,7 +258,6 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver) recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver)
} }
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
recyclerView.firstItem = position
} }
override fun getItemsCount() = adapter?.itemCount ?: 0 override fun getItemsCount() = adapter?.itemCount ?: 0

View File

@@ -1,38 +0,0 @@
package org.koitharu.kotatsu.list.ui
import android.view.ViewGroup
import coil.ImageLoader
import coil.request.Disposable
import kotlinx.android.synthetic.main.item_manga_list.*
import org.koin.core.component.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
class MangaListHolder(
parent: ViewGroup
) : BaseViewHolder<Manga, MangaHistory?>(parent, R.layout.item_manga_list) {
private val coil by inject<ImageLoader>()
private var imageRequest: Disposable? = null
override fun onBind(data: Manga, extra: MangaHistory?) {
imageRequest?.dispose()
textView_title.text = data.title
textView_subtitle.textAndVisible = data.tags.joinToString(", ") { it.title }
imageRequest = imageView_cover.newImageRequest(data.coverUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.enqueueWith(coil)
}
override fun onRecycled() {
imageRequest?.dispose()
imageView_cover.setImageDrawable(null)
}
}

View File

@@ -6,44 +6,42 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.sheet_list.* import kotlinx.android.synthetic.main.sheet_list.*
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.ProgressBarAdapter
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
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.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.utils.UiUtils import org.koitharu.kotatsu.utils.UiUtils
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list), abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>, PaginationScrollListener.Callback, OnListItemClickListener<Manga>,
SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener { SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener {
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
private val adapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true)
.setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build()
private var adapter: MangaListAdapter? = null private var adapter: MangaListAdapter? = null
private var progressAdapter: ProgressBarAdapter? = null
protected abstract val viewModel: MangaListViewModel protected abstract val viewModel: MangaListViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
adapter = MangaListAdapter(this) adapter = MangaListAdapter(get(), this)
progressAdapter = ProgressBarAdapter()
initListMode(settings.listMode) initListMode(settings.listMode)
recyclerView.adapter = adapter recyclerView.adapter = adapter
recyclerView.addOnScrollListener(PaginationScrollListener(4, this)) recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
@@ -64,12 +62,12 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.listMode.observe(viewLifecycleOwner, ::initListMode)
} }
override fun onDestroyView() { override fun onDestroyView() {
settings.unsubscribe(this) settings.unsubscribe(this)
adapter = null adapter = null
progressAdapter = null
super.onDestroyView() super.onDestroyView()
} }
@@ -120,14 +118,13 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
} }
} }
override fun onItemClick(item: Manga, position: Int, view: View) { override fun onItemClick(item: Manga, view: View) {
startActivity(DetailsActivity.newIntent(context ?: return, item)) startActivity(DetailsActivity.newIntent(context ?: return, item))
} }
private fun onListChanged(list: List<Manga>) { private fun onListChanged(list: List<Any>) {
adapter?.replaceData(list) adapter?.items = list
textView_holder.isVisible = list.isEmpty() textView_holder.isVisible = list.isEmpty()
progressAdapter?.isProgressVisible = list.isNotEmpty()
recyclerView.callOnScrollListeners() recyclerView.callOnScrollListeners()
} }
@@ -147,11 +144,9 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
private fun initListMode(mode: ListMode) { private fun initListMode(mode: ListMode) {
val ctx = context ?: return val ctx = context ?: return
val position = recyclerView.firstItem val position = recyclerView.firstItem
recyclerView.adapter = null
recyclerView.layoutManager = null recyclerView.layoutManager = null
recyclerView.clearItemDecorations() recyclerView.clearItemDecorations()
recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver) recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver)
adapter?.listMode = mode
recyclerView.layoutManager = when (mode) { recyclerView.layoutManager = when (mode) {
ListMode.GRID -> { ListMode.GRID -> {
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply { GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
@@ -163,7 +158,6 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
} }
else -> LinearLayoutManager(ctx) else -> LinearLayoutManager(ctx)
} }
recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.addItemDecoration( recyclerView.addItemDecoration(
when (mode) { when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)

View File

@@ -0,0 +1,55 @@
package org.koitharu.kotatsu.list.ui
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import kotlin.math.abs
import kotlin.math.roundToInt
class MangaListSpanResolver(
context: Context,
private val adapter: MangaListAdapter
) : GridLayoutManager.SpanSizeLookup(), View.OnLayoutChangeListener {
private val gridWidth = context.resources.getDimension(R.dimen.preferred_grid_width)
private var cellWidth = -1f
override fun getSpanSize(position: Int) = when(adapter.getItemViewType(position)) {
else -> 1
}
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
if (cellWidth <= 0f) {
return
}
val rv = v as? RecyclerView ?: return
val width = abs(right - left)
if (width == 0) {
return
}
(rv.layoutManager as? GridLayoutManager)?.spanCount = resolveGridSpanCount(width)
}
fun setGridSize(gridSize: Int) {
val scaleFactor = gridSize / 100f
cellWidth = gridWidth * scaleFactor
}
private fun resolveGridSpanCount(width: Int): Int {
val estimatedCount = (width / cellWidth).roundToInt()
return estimatedCount.coerceAtLeast(2)
}
}

View File

@@ -1,11 +1,31 @@
package org.koitharu.kotatsu.list.ui package org.koitharu.kotatsu.list.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
abstract class MangaListViewModel : BaseViewModel() { abstract class MangaListViewModel(
private val settings: AppSettings
) : BaseViewModel() {
val content = MutableLiveData<List<Manga>>() abstract val content: LiveData<List<Any>>
val filter = MutableLiveData<MangaFilterConfig>() val filter = MutableLiveData<MangaFilterConfig>()
val listMode = MutableLiveData<ListMode>()
val gridScale = settings.observe()
.filter { it == AppSettings.KEY_GRID_SIZE }
.map { settings.gridSize / 100f }
.asLiveData(viewModelScope.coroutineContext + Dispatchers.IO)
protected fun createListModeFlow() = settings.observe()
.filter { it == AppSettings.KEY_LIST_MODE }
.map { settings.listMode }
.onStart { emit(settings.listMode) }
.distinctUntilChanged()
.onEach { listMode.postValue(it) }
} }

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.list.ui.adapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
fun indeterminateProgressAD() = adapterDelegate<IndeterminateProgress, Any>(R.layout.item_progress) {
}

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.list.ui.adapter
import coil.ImageLoader
import coil.request.Disposable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_manga_list.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
fun mangaGridItemAD(
coil: ImageLoader,
clickListener: OnListItemClickListener<Manga>
) = adapterDelegateLayoutContainer<MangaGridModel, Any>(R.layout.item_manga_grid) {
var imageRequest: Disposable? = null
itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it)
}
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it)
}
bind {
textView_title.text = item.title
imageRequest?.dispose()
imageRequest = imageView_cover.newImageRequest(item.coverUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.enqueueWith(coil)
}
onViewRecycled {
imageRequest?.dispose()
imageView_cover.setImageDrawable(null)
}
}

View File

@@ -0,0 +1,49 @@
package org.koitharu.kotatsu.list.ui.adapter
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import kotlin.jvm.internal.Intrinsics
class MangaListAdapter(
coil: ImageLoader,
clickListener: OnListItemClickListener<Manga>
) : AsyncListDifferDelegationAdapter<Any>(DiffCallback) {
init {
delegatesManager.addDelegate(mangaListItemAD(coil, clickListener))
.addDelegate(mangaListDetailedItemAD(coil, clickListener))
.addDelegate(mangaGridItemAD(coil, clickListener))
.addDelegate(indeterminateProgressAD())
}
private companion object DiffCallback : DiffUtil.ItemCallback<Any>() {
override fun areItemsTheSame(oldItem: Any, newItem: Any) = when {
oldItem is MangaListModel && newItem is MangaListModel -> {
oldItem.id == newItem.id
}
oldItem is MangaListDetailedModel && newItem is MangaListDetailedModel -> {
oldItem.id == newItem.id
}
oldItem is MangaGridModel && newItem is MangaGridModel -> {
oldItem.id == newItem.id
}
oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> {
true
}
else -> false
}
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
}
}

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.list.ui.adapter
import coil.ImageLoader
import coil.request.Disposable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_manga_list_details.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun mangaListDetailedItemAD(
coil: ImageLoader,
clickListener: OnListItemClickListener<Manga>
) = adapterDelegateLayoutContainer<MangaListDetailedModel, Any>(R.layout.item_manga_list_details) {
var imageRequest: Disposable? = null
itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it)
}
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it)
}
bind {
imageRequest?.dispose()
textView_title.text = item.title
textView_subtitle.textAndVisible = item.subtitle
imageRequest = imageView_cover.newImageRequest(item.coverUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.enqueueWith(coil)
textView_rating.textAndVisible = item.rating
textView_tags.text = item.tags
}
onViewRecycled {
imageRequest?.dispose()
imageView_cover.setImageDrawable(null)
}
}

View File

@@ -0,0 +1,44 @@
package org.koitharu.kotatsu.list.ui.adapter
import coil.ImageLoader
import coil.request.Disposable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_manga_list.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun mangaListItemAD(
coil: ImageLoader,
clickListener: OnListItemClickListener<Manga>
) = adapterDelegateLayoutContainer<MangaListModel, Any>(R.layout.item_manga_list) {
var imageRequest: Disposable? = null
itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it)
}
itemView.setOnLongClickListener {
clickListener.onItemLongClick(item.manga, it)
}
bind {
imageRequest?.dispose()
textView_title.text = item.title
textView_subtitle.textAndVisible = item.subtitle
imageRequest = imageView_cover.newImageRequest(item.coverUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.enqueueWith(coil)
}
onViewRecycled {
imageRequest?.dispose()
imageView_cover.setImageDrawable(null)
}
}

View File

@@ -0,0 +1,3 @@
package org.koitharu.kotatsu.list.ui.model
object IndeterminateProgress

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.model.Manga
import kotlin.math.roundToInt
fun Manga.toListModel() = MangaListModel(
id = id,
title = title,
subtitle = tags.joinToString(", ") { it.title },
coverUrl = coverUrl,
manga = this
)
fun Manga.toListDetailedModel() = MangaListDetailedModel(
id = id,
title = title,
subtitle = altTitle,
rating = "${(rating * 10).roundToInt()}/10",
tags = tags.joinToString(", ") { it.title },
coverUrl = coverUrl,
manga = this
)
fun Manga.toGridModel() = MangaGridModel(
id = id,
title = title,
coverUrl = coverUrl,
manga = this
)

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.model.Manga
data class MangaGridModel(
val id: Long,
val title: String,
val coverUrl: String,
val manga: Manga
)

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.model.Manga
data class MangaListDetailedModel(
val id: Long,
val title: String,
val subtitle: String?,
val tags: String,
val coverUrl: String,
val rating: String?,
val manga: Manga
)

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.model.Manga
data class MangaListModel(
val id: Long,
val title: String,
val subtitle: String,
val coverUrl: String,
val manga: Manga
)

View File

@@ -2,16 +2,17 @@ package org.koitharu.kotatsu.local
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.viewmodel.dsl.viewModel import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.bind import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.ui.LocalListViewModel import org.koitharu.kotatsu.local.ui.LocalListViewModel
val localModule val localModule
get() = module { get() = module {
single { LocalMangaRepository(androidContext()) } bind MangaRepository::class single { LocalMangaRepository(androidContext()) }
factory(named(MangaSource.LOCAL)) { get<LocalMangaRepository>() }
viewModel { LocalListViewModel(get(), get(), get(), androidContext()) } viewModel { LocalListViewModel(get(), get(), get(), androidContext()) }
} }

View File

@@ -3,13 +3,21 @@ package org.koitharu.kotatsu.local.ui
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
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.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
import org.koitharu.kotatsu.list.ui.model.toListModel
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.MediaStoreCompat
@@ -23,9 +31,18 @@ class LocalListViewModel(
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val context: Context private val context: Context
) : MangaListViewModel() { ) : MangaListViewModel(settings) {
val onMangaRemoved = SingleLiveEvent<Manga>() val onMangaRemoved = SingleLiveEvent<Manga>()
private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
override val content = combine(mangaList, createListModeFlow()) { list, mode ->
when(mode) {
ListMode.LIST -> list.map { it.toListModel() }
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
ListMode.GRID -> list.map { it.toGridModel() }
}
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
init { init {
loadList() loadList()
@@ -51,9 +68,8 @@ class LocalListViewModel(
source.copyTo(output) source.copyTo(output)
} }
} ?: throw IOException("Cannot open input stream: $uri") } ?: throw IOException("Cannot open input stream: $uri")
repository.getList(0)
} }
content.value = list loadList()
} }
} }
@@ -75,10 +91,9 @@ class LocalListViewModel(
private fun loadList() { private fun loadList() {
launchLoadingJob { launchLoadingJob {
val list = withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
repository.getList(0) mangaList.value = repository.getList(0)
} }
content.value = list
} }
} }
} }

View File

@@ -10,6 +10,6 @@ val remoteListModule
get() = module { get() = module {
viewModel { (source: MangaSource) -> viewModel { (source: MangaSource) ->
RemoteListViewModel(get(named(source))) RemoteListViewModel(get(named(source)), get())
} }
} }

View File

@@ -1,43 +1,70 @@
package org.koitharu.kotatsu.remotelist.ui package org.koitharu.kotatsu.remotelist.ui
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaFilter import org.koitharu.kotatsu.core.model.MangaFilter
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.prefs.ListMode
import org.koitharu.kotatsu.list.ui.MangaFilterConfig import org.koitharu.kotatsu.list.ui.MangaFilterConfig
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
import org.koitharu.kotatsu.list.ui.model.toGridModel
import org.koitharu.kotatsu.list.ui.model.toListDetailedModel
import org.koitharu.kotatsu.list.ui.model.toListModel
class RemoteListViewModel( class RemoteListViewModel(
private val repository: MangaRepository private val repository: MangaRepository,
) : MangaListViewModel() { settings: AppSettings
) : MangaListViewModel(settings) {
private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
private val hasNextPage = MutableStateFlow(false)
private var appliedFilter: MangaFilter? = null private var appliedFilter: MangaFilter? = null
override val content = combine(mangaList, createListModeFlow()) { list, mode ->
when(mode) {
ListMode.LIST -> list.map { it.toListModel() }
ListMode.DETAILED_LIST -> list.map { it.toListDetailedModel() }
ListMode.GRID -> list.map { it.toGridModel() }
}
}.combine(hasNextPage) { list, isHasNextPage ->
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
init { init {
loadList(0)
loadFilter() loadFilter()
} }
fun loadList(offset: Int) { fun loadList(offset: Int) {
launchLoadingJob { launchLoadingJob {
val list = withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
repository.getList( val list = repository.getList(
offset = offset, offset = offset,
sortOrder = appliedFilter?.sortOrder, sortOrder = appliedFilter?.sortOrder,
tag = appliedFilter?.tag tag = appliedFilter?.tag
) )
} if (offset == 0) {
if (offset == 0) { mangaList.value = list
content.value = list } else if (list.isNotEmpty()) {
} else { mangaList.value += list
content.value = content.value.orEmpty() + list }
hasNextPage.value = list.isNotEmpty()
} }
} }
} }
fun applyFilter(newFilter: MangaFilter) { fun applyFilter(newFilter: MangaFilter) {
appliedFilter = newFilter appliedFilter = newFilter
content.value = emptyList() mangaList.value = emptyList()
hasNextPage.value = false
loadList(0) loadList(0)
} }

View File

@@ -13,6 +13,6 @@ val searchModule
single { MangaSearchRepository() } single { MangaSearchRepository() }
viewModel { (source: MangaSource) -> SearchViewModel(get(named(source))) } viewModel { (source: MangaSource) -> SearchViewModel(get(named(source)), get()) }
viewModel { GlobalSearchViewModel(get()) } viewModel { GlobalSearchViewModel(get(), get()) }
} }

View File

@@ -1,13 +1,18 @@
package org.koitharu.kotatsu.search.ui package org.koitharu.kotatsu.search.ui
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
class SearchViewModel( class SearchViewModel(
private val repository: MangaRepository private val repository: MangaRepository,
) : MangaListViewModel() { settings: AppSettings
) : MangaListViewModel(settings) {
override val content = MutableLiveData<List<Any>>()
fun loadList(query: String, offset: Int) { fun loadList(query: String, offset: Int) {
launchLoadingJob { launchLoadingJob {

View File

@@ -1,18 +1,22 @@
package org.koitharu.kotatsu.search.ui.global package org.koitharu.kotatsu.search.ui.global
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.utils.ext.onFirst import org.koitharu.kotatsu.utils.ext.onFirst
import java.io.IOException import java.io.IOException
class GlobalSearchViewModel( class GlobalSearchViewModel(
private val repository: MangaSearchRepository private val repository: MangaSearchRepository,
) : MangaListViewModel() { settings: AppSettings
) : MangaListViewModel(settings) {
override val content = MutableLiveData<List<Any>>()
private var searchJob: Job? = null private var searchJob: Job? = null
fun startSearch(query: String) { fun startSearch(query: String) {

View File

@@ -3,8 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Response import okhttp3.Response
@@ -34,16 +32,6 @@ suspend fun Call.await() = suspendCancellableCoroutine<Response> { cont ->
} }
} }
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
var isFirstCall = true
return onEach {
if (isFirstCall) {
action(it)
isFirstCall = false
}
}
}
fun CoroutineScope.launchAfter( fun CoroutineScope.launchAfter(
job: Job?, job: Job?,
context: CoroutineContext = EmptyCoroutineContext, context: CoroutineContext = EmptyCoroutineContext,

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
var isFirstCall = true
return onEach {
if (isFirstCall) {
action(it)
isFirstCall = false
}
}
}
inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> {
return map { list -> list.map(transform) }
}

View File

@@ -9,7 +9,6 @@
android:id="@+id/progressBar" android:id="@+id/progressBar"
style="@style/Widget.AppCompat.ProgressBar" style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:visibility="invisible"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" /> android:layout_gravity="center" />

View File

@@ -1,6 +1,6 @@
#Sun Nov 15 14:08:38 EET 2020 #Thu Nov 19 06:56:48 EET 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip