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.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'
}

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.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"

View File

@@ -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()) }
}

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.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)

View File

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

View File

@@ -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>

View File

@@ -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)) }

View File

@@ -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(

View File

@@ -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 }
}
}
}

View File

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

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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)

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
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

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 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)

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
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) }
}

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.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()) }
}

View File

@@ -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
}
}
}

View File

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

View File

@@ -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)
}

View File

@@ -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()) }
}

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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,

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"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:visibility="invisible"
android:layout_height="wrap_content"
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
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