Migrate to AdapterDelegates
This commit is contained in:
@@ -70,6 +70,7 @@ dependencies {
|
||||
implementation 'androidx.activity:activity-ktx:1.2.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-livedata-ktx:2.3.0-beta01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha06'
|
||||
@@ -88,6 +89,9 @@ dependencies {
|
||||
implementation 'com.squareup.okio:okio:2.9.0'
|
||||
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-viewmodel:2.2.0'
|
||||
implementation 'io.coil-kt:coil-base:1.0.0'
|
||||
@@ -97,6 +101,6 @@ dependencies {
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5'
|
||||
|
||||
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'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.arraySetOf
|
||||
import androidx.core.content.edit
|
||||
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.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.utils.delegates.prefs.*
|
||||
@@ -117,6 +120,16 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
|
||||
fun observe() = callbackFlow<String> {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
sendBlocking(key)
|
||||
}
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
awaitClose {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val PAGE_SWITCH_TAPS = "taps"
|
||||
|
||||
@@ -7,5 +7,5 @@ import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
val detailsModule
|
||||
get() = module {
|
||||
|
||||
viewModel { DetailsViewModel(get(), get(), get(), get(), get(), get()) }
|
||||
viewModel { DetailsViewModel(get(), get(), get(), get(), get(), get(), get()) }
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
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.OnFavouritesChangeListener
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
@@ -28,8 +29,11 @@ class DetailsViewModel(
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
private val mangaDataRepository: MangaDataRepository
|
||||
) : MangaListViewModel(), OnHistoryChangeListener, OnFavouritesChangeListener {
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings), OnHistoryChangeListener, OnFavouritesChangeListener {
|
||||
|
||||
override val content = MutableLiveData<List<Any>>()
|
||||
|
||||
val mangaData = MutableLiveData<Manga>()
|
||||
val newChapters = MutableLiveData<Int>(0)
|
||||
|
||||
@@ -12,7 +12,7 @@ val favouritesModule
|
||||
single { FavouritesRepository(get()) }
|
||||
|
||||
viewModel { (categoryId: Long) ->
|
||||
FavouritesListViewModel(categoryId, get())
|
||||
FavouritesListViewModel(categoryId, get(), get())
|
||||
}
|
||||
viewModel { FavouritesCategoriesViewModel(get()) }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Dao
|
||||
@@ -10,6 +11,10 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
|
||||
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
|
||||
@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>
|
||||
@@ -18,6 +23,10 @@ abstract class FavouritesDao {
|
||||
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
|
||||
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
|
||||
@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>
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.favourites.domain
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
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.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
|
||||
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)) }
|
||||
}
|
||||
|
||||
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> {
|
||||
val entities = db.favouritesDao.findAll(offset, 20)
|
||||
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)) }
|
||||
}
|
||||
|
||||
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> {
|
||||
val entities = db.favouritesDao.findAll(categoryId, offset, 20)
|
||||
return entities.map { it.manga.toManga(it.tags.mapToSet(TagEntity::toMangaTag)) }
|
||||
|
||||
@@ -20,9 +20,7 @@ class FavouritesListFragment : MangaListFragment() {
|
||||
private val categoryId: Long
|
||||
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L
|
||||
|
||||
override fun onRequestMoreItems(offset: Int) {
|
||||
viewModel.loadList(offset)
|
||||
}
|
||||
override fun onRequestMoreItems(offset: Int) = Unit
|
||||
|
||||
override fun setUpEmptyListHolder() {
|
||||
textView_holder.setText(
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
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(
|
||||
private val categoryId: Long,
|
||||
private val repository: FavouritesRepository
|
||||
) : MangaListViewModel() {
|
||||
private val repository: FavouritesRepository,
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
fun loadList(offset: Int) {
|
||||
launchLoadingJob {
|
||||
val list = if (categoryId == 0L) {
|
||||
repository.getAllManga(offset = offset)
|
||||
} else {
|
||||
repository.getManga(categoryId = categoryId, offset = offset)
|
||||
}
|
||||
if (offset == 0) {
|
||||
content.value = list
|
||||
} else {
|
||||
content.value = content.value.orEmpty() + list
|
||||
}
|
||||
override val content = combine(
|
||||
if (categoryId == 0L) repository.observeAll() else repository.observeAll(categoryId),
|
||||
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)
|
||||
|
||||
fun removeFromFavourites(manga: Manga) {
|
||||
launchJob {
|
||||
@@ -31,7 +37,6 @@ class FavouritesListViewModel(
|
||||
} else {
|
||||
repository.removeFromCategory(manga, categoryId)
|
||||
}
|
||||
content.value = content.value?.filterNot { it.id == manga.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,5 @@ val historyModule
|
||||
get() = module {
|
||||
|
||||
single { HistoryRepository(get()) }
|
||||
viewModel { HistoryListViewModel(get(), androidContext()) }
|
||||
viewModel { HistoryListViewModel(get(), androidContext(), get()) }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.history.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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")
|
||||
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)")
|
||||
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.history.domain
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.room.withTransaction
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
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.history.data.HistoryEntity
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.ext.mapItems
|
||||
import org.koitharu.kotatsu.utils.ext.mapToSet
|
||||
|
||||
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)) }
|
||||
}
|
||||
|
||||
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) {
|
||||
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
|
||||
db.withTransaction {
|
||||
|
||||
@@ -18,14 +18,16 @@ class HistoryListFragment : MangaListFragment() {
|
||||
|
||||
override val viewModel by viewModel<HistoryListViewModel>()
|
||||
|
||||
init {
|
||||
isSwipeRefreshEnabled = false
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.onItemRemoved.observe(viewLifecycleOwner, ::onItemRemoved)
|
||||
}
|
||||
|
||||
override fun onRequestMoreItems(offset: Int) {
|
||||
viewModel.loadList(offset)
|
||||
}
|
||||
override fun onRequestMoreItems(offset: Int) = Unit
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.opt_history, menu)
|
||||
|
||||
@@ -2,34 +2,43 @@ package org.koitharu.kotatsu.history.ui
|
||||
|
||||
import android.content.Context
|
||||
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.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
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.SingleLiveEvent
|
||||
|
||||
class HistoryListViewModel(
|
||||
private val repository: HistoryRepository,
|
||||
private val context: Context //todo create ShortcutRepository
|
||||
) : MangaListViewModel() {
|
||||
, settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
val onItemRemoved = SingleLiveEvent<Manga>()
|
||||
|
||||
fun loadList(offset: Int) {
|
||||
launchLoadingJob {
|
||||
val list = repository.getList(offset = offset)
|
||||
if (offset == 0) {
|
||||
content.value = list
|
||||
} else {
|
||||
content.value = content.value.orEmpty() + list
|
||||
}
|
||||
override val content = combine(
|
||||
repository.observeAll(),
|
||||
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)
|
||||
|
||||
fun clearHistory() {
|
||||
launchLoadingJob {
|
||||
repository.clear()
|
||||
content.value = emptyList()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
MangaShortcut.clearAppShortcuts(context)
|
||||
}
|
||||
@@ -39,7 +48,6 @@ class HistoryListViewModel(
|
||||
fun removeFromHistory(manga: Manga) {
|
||||
launchJob {
|
||||
repository.delete(manga)
|
||||
content.value = content.value?.filterNot { it.id == manga.id }
|
||||
onItemRemoved.call(manga)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
MangaShortcut(manga).removeAppShortcut(context)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.annotation.CallSuper
|
||||
@@ -9,16 +8,18 @@ import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
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 com.google.android.material.snackbar.Snackbar
|
||||
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.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.ProgressBarAdapter
|
||||
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.SpacingItemDecoration
|
||||
@@ -26,28 +27,21 @@ import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
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.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.OnFilterChangedListener
|
||||
import org.koitharu.kotatsu.utils.UiUtils
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener, OnFilterChangedListener,
|
||||
PaginationScrollListener.Callback, OnListItemClickListener<Manga>, OnFilterChangedListener,
|
||||
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 progressAdapter: ProgressBarAdapter? = null
|
||||
private var paginationListener: PaginationScrollListener? = null
|
||||
private val spanResolver: MangaListSpanResolver? = null
|
||||
protected var isSwipeRefreshEnabled = true
|
||||
|
||||
protected abstract val viewModel: MangaListViewModel
|
||||
@@ -60,31 +54,27 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
drawer?.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||
adapter = MangaListAdapter(this)
|
||||
progressAdapter = ProgressBarAdapter()
|
||||
adapter = MangaListAdapter(get(), this)
|
||||
paginationListener = PaginationScrollListener(4, this)
|
||||
recyclerView.setHasFixedSize(true)
|
||||
initListMode(settings.listMode)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(paginationListener!!)
|
||||
swipeRefreshLayout.setOnRefreshListener(this)
|
||||
recyclerView_filter.setHasFixedSize(true)
|
||||
recyclerView_filter.addItemDecoration(ItemTypeDividerDecoration(view.context))
|
||||
recyclerView_filter.addItemDecoration(SectionItemDecoration(false, this))
|
||||
settings.subscribe(this)
|
||||
if (savedInstanceState == null) {
|
||||
onRequestMoreItems(0)
|
||||
}
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||
viewModel.filter.observe(viewLifecycleOwner, ::onInitFilter)
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.listMode.observe(viewLifecycleOwner, ::onListModeChanged)
|
||||
viewModel.gridScale.observe(viewLifecycleOwner, ::onGridScaleChanged)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
adapter = null
|
||||
progressAdapter = null
|
||||
paginationListener = null
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@@ -111,11 +101,11 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
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))
|
||||
}
|
||||
|
||||
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)
|
||||
onCreatePopupMenu(menu.menuInflater, menu.menu, item)
|
||||
return if (menu.menu.hasVisibleItems()) {
|
||||
@@ -135,16 +125,14 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
onRequestMoreItems(0)
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<Manga>) {
|
||||
paginationListener?.reset()
|
||||
adapter?.replaceData(list)
|
||||
private fun onListChanged(list: List<Any>) {
|
||||
adapter?.items = list
|
||||
if (list.isEmpty()) {
|
||||
setUpEmptyListHolder()
|
||||
layout_holder.isVisible = true
|
||||
} else {
|
||||
layout_holder.isVisible = false
|
||||
}
|
||||
progressAdapter?.isProgressVisible = list.isNotEmpty()
|
||||
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) {
|
||||
recyclerView_filter.adapter = FilterAdapter(
|
||||
sortOrders = config.sortOrders,
|
||||
@@ -220,14 +197,43 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
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) {
|
||||
val ctx = context ?: return
|
||||
val position = recyclerView.firstItem
|
||||
recyclerView.adapter = null
|
||||
recyclerView.layoutManager = null
|
||||
recyclerView.clearItemDecorations()
|
||||
recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver)
|
||||
adapter?.listMode = mode
|
||||
recyclerView.layoutManager = when (mode) {
|
||||
ListMode.GRID -> {
|
||||
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
|
||||
@@ -239,8 +245,6 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
}
|
||||
else -> LinearLayoutManager(ctx)
|
||||
}
|
||||
recyclerView.recycledViewPool.clear()
|
||||
recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
|
||||
recyclerView.addItemDecoration(
|
||||
when (mode) {
|
||||
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)
|
||||
@@ -254,7 +258,6 @@ abstract class MangaListFragment : BaseFragment(R.layout.fragment_list),
|
||||
recyclerView.addOnLayoutChangeListener(UiUtils.SpanCountResolver)
|
||||
}
|
||||
adapter?.notifyDataSetChanged()
|
||||
recyclerView.firstItem = position
|
||||
}
|
||||
|
||||
override fun getItemsCount() = adapter?.itemCount ?: 0
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -6,44 +6,42 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
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.BottomSheetDialog
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.sheet_list.*
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
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.ProgressBarAdapter
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
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.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||
import org.koitharu.kotatsu.utils.UiUtils
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
|
||||
abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
|
||||
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga>,
|
||||
PaginationScrollListener.Callback, OnListItemClickListener<Manga>,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener {
|
||||
|
||||
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 progressAdapter: ProgressBarAdapter? = null
|
||||
|
||||
protected abstract val viewModel: MangaListViewModel
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = MangaListAdapter(this)
|
||||
progressAdapter = ProgressBarAdapter()
|
||||
adapter = MangaListAdapter(get(), this)
|
||||
initListMode(settings.listMode)
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
|
||||
@@ -64,12 +62,12 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
|
||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||
viewModel.onError.observe(viewLifecycleOwner, ::onError)
|
||||
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
|
||||
viewModel.listMode.observe(viewLifecycleOwner, ::initListMode)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
adapter = null
|
||||
progressAdapter = null
|
||||
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))
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<Manga>) {
|
||||
adapter?.replaceData(list)
|
||||
private fun onListChanged(list: List<Any>) {
|
||||
adapter?.items = list
|
||||
textView_holder.isVisible = list.isEmpty()
|
||||
progressAdapter?.isProgressVisible = list.isNotEmpty()
|
||||
recyclerView.callOnScrollListeners()
|
||||
}
|
||||
|
||||
@@ -147,11 +144,9 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
|
||||
private fun initListMode(mode: ListMode) {
|
||||
val ctx = context ?: return
|
||||
val position = recyclerView.firstItem
|
||||
recyclerView.adapter = null
|
||||
recyclerView.layoutManager = null
|
||||
recyclerView.clearItemDecorations()
|
||||
recyclerView.removeOnLayoutChangeListener(UiUtils.SpanCountResolver)
|
||||
adapter?.listMode = mode
|
||||
recyclerView.layoutManager = when (mode) {
|
||||
ListMode.GRID -> {
|
||||
GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
|
||||
@@ -163,7 +158,6 @@ abstract class MangaListSheet : BaseBottomSheet(R.layout.sheet_list),
|
||||
}
|
||||
else -> LinearLayoutManager(ctx)
|
||||
}
|
||||
recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
|
||||
recyclerView.addItemDecoration(
|
||||
when (mode) {
|
||||
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,31 @@
|
||||
package org.koitharu.kotatsu.list.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
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.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 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) }
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
object IndeterminateProgress
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
@@ -2,16 +2,17 @@ package org.koitharu.kotatsu.local
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
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.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.ui.LocalListViewModel
|
||||
|
||||
val localModule
|
||||
get() = module {
|
||||
|
||||
single { LocalMangaRepository(androidContext()) } bind MangaRepository::class
|
||||
single { LocalMangaRepository(androidContext()) }
|
||||
factory(named(MangaSource.LOCAL)) { get<LocalMangaRepository>() }
|
||||
|
||||
viewModel { LocalListViewModel(get(), get(), get(), androidContext()) }
|
||||
}
|
||||
@@ -3,13 +3,21 @@ package org.koitharu.kotatsu.local.ui
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
|
||||
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.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.utils.MangaShortcut
|
||||
import org.koitharu.kotatsu.utils.MediaStoreCompat
|
||||
@@ -23,9 +31,18 @@ class LocalListViewModel(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val settings: AppSettings,
|
||||
private val context: Context
|
||||
) : MangaListViewModel() {
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
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 {
|
||||
loadList()
|
||||
@@ -51,9 +68,8 @@ class LocalListViewModel(
|
||||
source.copyTo(output)
|
||||
}
|
||||
} ?: throw IOException("Cannot open input stream: $uri")
|
||||
repository.getList(0)
|
||||
}
|
||||
content.value = list
|
||||
loadList()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,10 +91,9 @@ class LocalListViewModel(
|
||||
|
||||
private fun loadList() {
|
||||
launchLoadingJob {
|
||||
val list = withContext(Dispatchers.Default) {
|
||||
repository.getList(0)
|
||||
withContext(Dispatchers.Default) {
|
||||
mangaList.value = repository.getList(0)
|
||||
}
|
||||
content.value = list
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,6 @@ val remoteListModule
|
||||
get() = module {
|
||||
|
||||
viewModel { (source: MangaSource) ->
|
||||
RemoteListViewModel(get(named(source)))
|
||||
RemoteListViewModel(get(named(source)), get())
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,70 @@
|
||||
package org.koitharu.kotatsu.remotelist.ui
|
||||
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.MangaFilter
|
||||
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.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(
|
||||
private val repository: MangaRepository
|
||||
) : MangaListViewModel() {
|
||||
private val repository: MangaRepository,
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
private val mangaList = MutableStateFlow<List<Manga>>(emptyList())
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
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 {
|
||||
loadList(0)
|
||||
loadFilter()
|
||||
}
|
||||
|
||||
fun loadList(offset: Int) {
|
||||
launchLoadingJob {
|
||||
val list = withContext(Dispatchers.Default) {
|
||||
repository.getList(
|
||||
withContext(Dispatchers.Default) {
|
||||
val list = repository.getList(
|
||||
offset = offset,
|
||||
sortOrder = appliedFilter?.sortOrder,
|
||||
tag = appliedFilter?.tag
|
||||
)
|
||||
}
|
||||
if (offset == 0) {
|
||||
content.value = list
|
||||
} else {
|
||||
content.value = content.value.orEmpty() + list
|
||||
if (offset == 0) {
|
||||
mangaList.value = list
|
||||
} else if (list.isNotEmpty()) {
|
||||
mangaList.value += list
|
||||
}
|
||||
hasNextPage.value = list.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyFilter(newFilter: MangaFilter) {
|
||||
appliedFilter = newFilter
|
||||
content.value = emptyList()
|
||||
mangaList.value = emptyList()
|
||||
hasNextPage.value = false
|
||||
loadList(0)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,6 @@ val searchModule
|
||||
|
||||
single { MangaSearchRepository() }
|
||||
|
||||
viewModel { (source: MangaSource) -> SearchViewModel(get(named(source))) }
|
||||
viewModel { GlobalSearchViewModel(get()) }
|
||||
viewModel { (source: MangaSource) -> SearchViewModel(get(named(source)), get()) }
|
||||
viewModel { GlobalSearchViewModel(get(), get()) }
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package org.koitharu.kotatsu.search.ui
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
|
||||
class SearchViewModel(
|
||||
private val repository: MangaRepository
|
||||
) : MangaListViewModel() {
|
||||
private val repository: MangaRepository,
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
override val content = MutableLiveData<List<Any>>()
|
||||
|
||||
fun loadList(query: String, offset: Int) {
|
||||
launchLoadingJob {
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package org.koitharu.kotatsu.search.ui.global
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
||||
import java.io.IOException
|
||||
|
||||
class GlobalSearchViewModel(
|
||||
private val repository: MangaSearchRepository
|
||||
) : MangaListViewModel() {
|
||||
private val repository: MangaSearchRepository,
|
||||
settings: AppSettings
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
override val content = MutableLiveData<List<Any>>()
|
||||
private var searchJob: Job? = null
|
||||
|
||||
fun startSearch(query: String) {
|
||||
|
||||
@@ -3,8 +3,6 @@ package org.koitharu.kotatsu.utils.ext
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
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(
|
||||
job: Job?,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
|
||||
19
app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt
Normal file
19
app/src/main/java/org/koitharu/kotatsu/utils/ext/FlowExt.kt
Normal 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) }
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
android:id="@+id/progressBar"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:visibility="invisible"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
|
||||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sun Nov 15 14:08:38 EET 2020
|
||||
#Thu Nov 19 06:56:48 EET 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user