Update library fragment

This commit is contained in:
Koitharu
2022-07-21 17:09:20 +03:00
parent 81df005655
commit 300d365d8b
29 changed files with 282 additions and 243 deletions

View File

@@ -9,11 +9,12 @@ insert_final_newline = true
max_line_length = 120 max_line_length = 120
tab_width = 4 tab_width = 4
# noinspection EditorConfigKeyCorrectness # noinspection EditorConfigKeyCorrectness
disabled_rules=no-wildcard-imports,no-unused-imports disabled_rules = no-wildcard-imports, no-unused-imports
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] [{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_continuation_indent_size = 4 ij_continuation_indent_size = 4
[{*.kt,*.kts}] [{*.kt,*.kts}]
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma = true
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL

View File

@@ -12,8 +12,8 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.parser.favicon.faviconUri import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding import org.koitharu.kotatsu.databinding.ItemExploreButtonsBinding
import org.koitharu.kotatsu.databinding.ItemExploreHeaderBinding
import org.koitharu.kotatsu.databinding.ItemExploreSourceBinding import org.koitharu.kotatsu.databinding.ItemExploreSourceBinding
import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
import org.koitharu.kotatsu.explore.ui.model.ExploreItem import org.koitharu.kotatsu.explore.ui.model.ExploreItem
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.disposeImageRequest
@@ -42,8 +42,8 @@ fun exploreButtonsAD(
fun exploreSourcesHeaderAD( fun exploreSourcesHeaderAD(
listener: ExploreListEventListener, listener: ExploreListEventListener,
) = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemExploreHeaderBinding>( ) = adapterDelegateViewBinding<ExploreItem.Header, ExploreItem, ItemHeaderButtonBinding>(
{ layoutInflater, parent -> ItemExploreHeaderBinding.inflate(layoutInflater, parent, false) } { layoutInflater, parent -> ItemHeaderButtonBinding.inflate(layoutInflater, parent, false) }
) { ) {
val listenerAdapter = View.OnClickListener { val listenerAdapter = View.OnClickListener {
@@ -105,4 +105,4 @@ fun exploreEmptyHintListAD(
} }
} }
fun exploreLoadingAD() = adapterDelegate<ExploreItem.Loading, ExploreItem>(R.layout.item_loading_state) {} fun exploreLoadingAD() = adapterDelegate<ExploreItem.Loading, ExploreItem>(R.layout.item_loading_state) {}

View File

@@ -19,7 +19,9 @@ abstract class FavouritesDao {
fun observeAll(order: SortOrder): Flow<List<FavouriteManga>> { fun observeAll(order: SortOrder): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order) val orderBy = getOrderBy(order)
@Language("RoomSql") val query = SimpleSQLiteQuery(
@Language("RoomSql")
val query = SimpleSQLiteQuery(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy", "WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy",
) )
@@ -29,20 +31,22 @@ abstract class FavouritesDao {
@Transaction @Transaction
@Query( @Query(
"SELECT * FROM favourites WHERE deleted_at = 0 " + "SELECT * FROM favourites WHERE deleted_at = 0 " +
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset" "GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
) )
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga> abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
@Transaction @Transaction
@Query( @Query(
"SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " + "SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " +
"GROUP BY manga_id ORDER BY created_at DESC" "GROUP BY manga_id ORDER BY created_at DESC",
) )
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga> abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
fun observeAll(categoryId: Long, order: SortOrder): Flow<List<FavouriteManga>> { fun observeAll(categoryId: Long, order: SortOrder): Flow<List<FavouriteManga>> {
val orderBy = getOrderBy(order) val orderBy = getOrderBy(order)
@Language("RoomSql") val query = SimpleSQLiteQuery(
@Language("RoomSql")
val query = SimpleSQLiteQuery(
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + "SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
"WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy", "WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy",
arrayOf<Any>(categoryId), arrayOf<Any>(categoryId),
@@ -53,19 +57,21 @@ abstract class FavouritesDao {
@Transaction @Transaction
@Query( @Query(
"SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " + "SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " +
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset" "GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
) )
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga> abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
@Query( @Query(
"SELECT * FROM manga WHERE manga_id IN " + "SELECT * FROM manga WHERE manga_id IN " +
"(SELECT manga_id FROM favourites WHERE category_id = :categoryId AND deleted_at = 0)" "(SELECT manga_id FROM favourites WHERE category_id = :categoryId AND deleted_at = 0)",
) )
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity> abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
suspend fun findCovers(categoryId: Long, order: SortOrder): List<String> { suspend fun findCovers(categoryId: Long, order: SortOrder): List<String> {
val orderBy = getOrderBy(order) val orderBy = getOrderBy(order)
@Language("RoomSql") val query = SimpleSQLiteQuery(
@Language("RoomSql")
val query = SimpleSQLiteQuery(
"SELECT m.cover_url FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " + "SELECT m.cover_url FROM favourites AS f LEFT JOIN manga AS m ON f.manga_id = m.manga_id " +
"WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy", "WHERE f.category_id = ? AND deleted_at = 0 ORDER BY $orderBy",
arrayOf<Any>(categoryId), arrayOf<Any>(categoryId),
@@ -81,6 +87,7 @@ abstract class FavouritesDao {
abstract suspend fun find(id: Long): FavouriteManga? abstract suspend fun find(id: Long): FavouriteManga?
@Transaction @Transaction
@Deprecated("Ignores order")
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id") @Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
abstract fun observe(id: Long): Flow<FavouriteManga?> abstract fun observe(id: Long): Flow<FavouriteManga?>
@@ -140,7 +147,8 @@ abstract class FavouritesDao {
private fun getOrderBy(sortOrder: SortOrder) = when (sortOrder) { private fun getOrderBy(sortOrder: SortOrder) = when (sortOrder) {
SortOrder.RATING -> "rating DESC" SortOrder.RATING -> "rating DESC"
SortOrder.NEWEST, SortOrder.NEWEST,
SortOrder.UPDATED -> "created_at DESC" SortOrder.UPDATED,
-> "created_at DESC"
SortOrder.ALPHABETICAL -> "title ASC" SortOrder.ALPHABETICAL -> "title ASC"
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported") else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
} }

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.favourites.domain package org.koitharu.kotatsu.favourites.domain
import android.util.ArrayMap
import androidx.room.withTransaction import androidx.room.withTransaction
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.base.domain.ReversibleHandle
@@ -55,7 +54,7 @@ class FavouritesRepository(
return db.favouriteCategoriesDao.observeAll() return db.favouriteCategoriesDao.observeAll()
.map { .map {
db.withTransaction { db.withTransaction {
val res = ArrayMap<FavouriteCategory, List<String>>() val res = LinkedHashMap<FavouriteCategory, List<String>>()
for (entity in it) { for (entity in it) {
val cat = entity.toFavouriteCategory() val cat = entity.toFavouriteCategory()
res[cat] = db.favouritesDao.findCovers( res[cat] = db.favouritesDao.findCovers(

View File

@@ -6,12 +6,9 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import com.google.android.material.snackbar.Snackbar
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.core.ui.titleRes import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
@@ -32,7 +29,6 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() } viewModel.sortOrder.observe(viewLifecycleOwner) { activity?.invalidateOptionsMenu() }
viewModel.onItemsRemoved.observe(viewLifecycleOwner, ::onItemsRemoved)
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
@@ -75,15 +71,6 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
} }
} }
private fun onItemsRemoved(reversibleHandle: ReversibleHandle) {
val message = viewModel.categoryName?.let {
getString(R.string.removed_from_s, it)
} ?: getString(R.string.removed_from_favourites)
Snackbar.make(binding.recyclerView, message, Snackbar.LENGTH_LONG)
.setAction(R.string.undo) { reversibleHandle.reverseAsync() }
.show()
}
companion object { companion object {
const val NO_ID = 0L const val NO_ID = 0L

View File

@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
@@ -23,7 +23,6 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class FavouritesListViewModel( class FavouritesListViewModel(
@@ -72,8 +71,6 @@ class FavouritesListViewModel(
emit(listOf(it.toErrorState(canRetry = false))) emit(listOf(it.toErrorState(canRetry = false)))
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
val onItemsRemoved = SingleLiveEvent<ReversibleHandle>()
init { init {
if (categoryId != NO_ID) { if (categoryId != NO_ID) {
launchJob { launchJob {
@@ -100,7 +97,7 @@ class FavouritesListViewModel(
} else { } else {
repository.removeFromCategory(categoryId, ids) repository.removeFromCategory(categoryId, ids)
} }
onItemsRemoved.postCall(handle) onActionDone.postCall(ReversibleAction(R.string.removed_from_favourites, handle))
} }
} }

View File

@@ -5,12 +5,9 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.addMenuProvider import org.koitharu.kotatsu.utils.ext.addMenuProvider
@@ -26,7 +23,6 @@ class HistoryListFragment : MangaListFragment() {
viewModel.isGroupingEnabled.observe(viewLifecycleOwner) { viewModel.isGroupingEnabled.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
} }
viewModel.onItemsRemoved.observe(viewLifecycleOwner, ::onItemsRemoved)
} }
override fun onScrolledToEnd() = Unit override fun onScrolledToEnd() = Unit
@@ -56,14 +52,8 @@ class HistoryListFragment : MangaListFragment() {
override fun onCreateAdapter() = HistoryListAdapter(get(), viewLifecycleOwner, this) override fun onCreateAdapter() = HistoryListAdapter(get(), viewLifecycleOwner, this)
private fun onItemsRemoved(reversibleHandle: ReversibleHandle) {
Snackbar.make(binding.recyclerView, R.string.removed_from_history, Snackbar.LENGTH_LONG)
.setAction(R.string.undo) { reversibleHandle.reverseAsync() }
.show()
}
companion object { companion object {
fun newInstance() = HistoryListFragment() fun newInstance() = HistoryListFragment()
} }
} }

View File

@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
@@ -19,7 +19,6 @@ import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.onFirst import org.koitharu.kotatsu.utils.ext.onFirst
@@ -33,7 +32,6 @@ class HistoryListViewModel(
) : MangaListViewModel(settings) { ) : MangaListViewModel(settings) {
val isGroupingEnabled = MutableLiveData<Boolean>() val isGroupingEnabled = MutableLiveData<Boolean>()
val onItemsRemoved = SingleLiveEvent<ReversibleHandle>()
private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled } private val historyGrouping = settings.observeAsFlow(AppSettings.KEY_HISTORY_GROUPING) { isHistoryGroupingEnabled }
.onEach { isGroupingEnabled.postValue(it) } .onEach { isGroupingEnabled.postValue(it) }
@@ -78,7 +76,7 @@ class HistoryListViewModel(
} }
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val handle = repository.delete(ids) val handle = repository.delete(ids)
onItemsRemoved.postCall(handle) onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle))
} }
} }

View File

@@ -1,40 +1,30 @@
package org.koitharu.kotatsu.library.domain package org.koitharu.kotatsu.library.domain
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.favourites.data.FavouriteManga
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
class LibraryRepository( class LibraryRepository(
private val db: MangaDatabase, private val db: MangaDatabase,
) { ) {
fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
fun observeFavourites(order: SortOrder): Flow<Map<FavouriteCategory, List<Manga>>> { return db.favouriteCategoriesDao.observeAll()
return db.favouritesDao.observeAll(order) .flatMapLatest { categories ->
.map { list -> groupByCategory(list) } combine(
} categories.map { cat ->
val category = cat.toFavouriteCategory()
private fun groupByCategory(list: List<FavouriteManga>): Map<FavouriteCategory, List<Manga>> { db.favouritesDao.observeAll(category.id, category.order)
val map = HashMap<FavouriteCategory, MutableList<Manga>>() .map { category to it.map { x -> x.manga.toManga(x.tags.toMangaTags()) } }
for (item in list) { },
val manga = item.manga.toManga(item.tags.toMangaTags()) ) { array -> array.toMap() }
for (category in item.categories) {
if (!category.isVisibleInLibrary) {
continue
}
map.getOrPut(category.toFavouriteCategory()) { ArrayList() }
.add(manga)
} }
}
return map
} }
} }

View File

@@ -33,7 +33,9 @@ import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations import org.koitharu.kotatsu.utils.ext.invalidateNestedItemDecorations
class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEventListener, class LibraryFragment :
BaseFragment<FragmentLibraryBinding>(),
LibraryListEventListener,
SectionedSelectionController.Callback<LibrarySectionModel> { SectionedSelectionController.Callback<LibrarySectionModel> {
private val viewModel by viewModel<LibraryViewModel>() private val viewModel by viewModel<LibraryViewModel>()
@@ -109,7 +111,7 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
} }
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_remote, menu) mode.menuInflater.inflate(R.menu.mode_library, menu)
return true return true
} }
@@ -172,7 +174,7 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
Snackbar.make( Snackbar.make(
binding.recyclerView, binding.recyclerView,
e.getDisplayMessage(resources), e.getDisplayMessage(resources),
Snackbar.LENGTH_SHORT Snackbar.LENGTH_SHORT,
).show() ).show()
} }

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.library.ui
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@@ -21,17 +22,15 @@ import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
import java.util.*
private const val HISTORY_MAX_SEGMENTS = 2 private const val HISTORY_MAX_SEGMENTS = 2
class LibraryViewModel( class LibraryViewModel(
private val repository: LibraryRepository, repository: LibraryRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val settings: AppSettings, private val settings: AppSettings,
@@ -41,7 +40,7 @@ class LibraryViewModel(
val content: LiveData<List<ListModel>> = combine( val content: LiveData<List<ListModel>> = combine(
historyRepository.observeAllWithHistory(), historyRepository.observeAllWithHistory(),
repository.observeFavourites(SortOrder.NEWEST), repository.observeFavourites(),
) { history, favourites -> ) { history, favourites ->
mapList(history, favourites) mapList(history, favourites)
}.catch { e -> }.catch { e ->
@@ -60,25 +59,6 @@ class LibraryViewModel(
} }
} }
fun getManga(ids: Set<Long>): Set<Manga> {
val snapshot = content.value ?: return emptySet()
val result = ArraySet<Manga>(ids.size)
for (section in snapshot) {
if (section !is LibrarySectionModel) {
continue
}
for (item in section.items) {
if (item.id in ids) {
result.add(item.manga)
if (result.size == ids.size) {
return result
}
}
}
}
return result
}
fun removeFromHistory(ids: Set<Long>) { fun removeFromHistory(ids: Set<Long>) {
if (ids.isEmpty()) { if (ids.isEmpty()) {
return return
@@ -102,16 +82,35 @@ class LibraryViewModel(
} }
} }
fun getManga(ids: Set<Long>): Set<Manga> {
val snapshot = content.value ?: return emptySet()
val result = ArraySet<Manga>(ids.size)
for (section in snapshot) {
if (section !is LibrarySectionModel) {
continue
}
for (item in section.items) {
if (item.id in ids) {
result.add(item.manga)
if (result.size == ids.size) {
return result
}
}
}
}
return result
}
private suspend fun mapList( private suspend fun mapList(
history: List<MangaWithHistory>, history: List<MangaWithHistory>,
favourites: Map<FavouriteCategory, List<Manga>>, favourites: Map<FavouriteCategory, List<Manga>>,
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(favourites.keys.size + 1) val result = ArrayList<ListModel>(favourites.keys.size + 1)
if (history.isNotEmpty()) { if (history.isNotEmpty()) {
result += mapHistory(history) mapHistory(result, history)
} }
for ((category, list) in favourites) { if (favourites.isNotEmpty()) {
result += LibrarySectionModel.Favourites(list.toUi(ListMode.GRID, this), category, R.string.show_all) mapFavourites(result, favourites)
} }
if (result.isEmpty()) { if (result.isEmpty()) {
result += EmptyState( result += EmptyState(
@@ -121,40 +120,45 @@ class LibraryViewModel(
actionStringRes = 0, actionStringRes = 0,
) )
} }
result.trimToSize()
return result return result
} }
private suspend fun mapHistory(list: List<MangaWithHistory>): List<LibrarySectionModel.History> { private suspend fun mapHistory(
destination: MutableList<in LibrarySectionModel.History>,
list: List<MangaWithHistory>,
) {
val showPercent = settings.isReadingIndicatorsEnabled val showPercent = settings.isReadingIndicatorsEnabled
val groups = ArrayList<DateTimeAgo>() val groups = list.groupByTo(LinkedHashMap()) { timeAgo(it.history.updatedAt) }
val map = HashMap<DateTimeAgo, ArrayList<MangaItemModel>>() while (groups.size > HISTORY_MAX_SEGMENTS) {
for ((manga, history) in list) { val lastKey = groups.keys.last()
val date = timeAgo(history.updatedAt) val subList = groups.remove(lastKey) ?: continue
val counter = trackingRepository.getNewChaptersCount(manga.id) groups[groups.keys.last()]?.addAll(subList)
val percent = if (showPercent) history.percent else PROGRESS_NONE
if (groups.lastOrNull() != date) {
groups.add(date)
}
map.getOrPut(date) { ArrayList() }.add(manga.toGridModel(counter, percent))
} }
val result = ArrayList<LibrarySectionModel.History>(HISTORY_MAX_SEGMENTS) for ((timeAgo, subList) in groups) {
repeat(minOf(HISTORY_MAX_SEGMENTS - 1, groups.size - 1)) { i -> destination += LibrarySectionModel.History(
val key = groups[i] items = subList.map { (manga, history) ->
val values = map.remove(key) val counter = trackingRepository.getNewChaptersCount(manga.id)
if (!values.isNullOrEmpty()) { val percent = if (showPercent) history.percent else PROGRESS_NONE
result.add(LibrarySectionModel.History(values, key, 0)) manga.toGridModel(counter, percent)
} },
timeAgo = timeAgo,
showAllButtonText = R.string.show_all,
)
} }
val values = map.values.flatten() }
if (values.isNotEmpty()) {
val key = if (result.isEmpty()) { private suspend fun mapFavourites(
map.keys.singleOrNull()?.takeUnless { it == DateTimeAgo.LongAgo } destination: MutableList<in LibrarySectionModel.Favourites>,
} else { favourites: Map<FavouriteCategory, List<Manga>>,
map.keys.singleOrNull() ?: DateTimeAgo.LongAgo ) {
} for ((category, list) in favourites) {
result.add(LibrarySectionModel.History(values, key, R.string.show_all)) destination += LibrarySectionModel.Favourites(
items = list.toUi(ListMode.GRID, this),
category = category,
showAllButtonText = R.string.show_all,
)
} }
return result
} }
private fun timeAgo(date: Date): DateTimeAgo { private fun timeAgo(date: Date): DateTimeAgo {

View File

@@ -1,11 +1,14 @@
package org.koitharu.kotatsu.library.ui.adapter package org.koitharu.kotatsu.library.ui.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlin.jvm.internal.Intrinsics
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel import org.koitharu.kotatsu.library.ui.model.LibrarySectionModel
import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
@@ -13,7 +16,6 @@ import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.jvm.internal.Intrinsics
class LibraryAdapter( class LibraryAdapter(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
@@ -21,7 +23,7 @@ class LibraryAdapter(
listener: LibraryListEventListener, listener: LibraryListEventListener,
sizeResolver: ItemSizeResolver, sizeResolver: ItemSizeResolver,
selectionController: SectionedSelectionController<LibrarySectionModel>, selectionController: SectionedSelectionController<LibrarySectionModel>,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()), FastScroller.SectionIndexer {
init { init {
val pool = RecyclerView.RecycledViewPool() val pool = RecyclerView.RecycledViewPool()
@@ -34,7 +36,7 @@ class LibraryAdapter(
sizeResolver = sizeResolver, sizeResolver = sizeResolver,
selectionController = selectionController, selectionController = selectionController,
listener = listener, listener = listener,
) ),
) )
.addDelegate(loadingStateAD()) .addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD()) .addDelegate(loadingFooterAD())
@@ -42,6 +44,11 @@ class LibraryAdapter(
.addDelegate(errorStateListAD(listener)) .addDelegate(errorStateListAD(listener))
} }
override fun getSectionText(context: Context, position: Int): CharSequence {
val item = items.getOrNull(position) as? LibrarySectionModel
return item?.getTitle(context.resources) ?: ""
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() { private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
@@ -64,4 +71,4 @@ class LibraryAdapter(
} }
} }
} }
} }

View File

@@ -91,4 +91,4 @@ sealed class LibrarySectionModel(
return "fav_${category.id}" return "fav_${category.id}"
} }
} }
} }

View File

@@ -16,6 +16,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager import org.koitharu.kotatsu.base.ui.list.FitHeightGridLayoutManager
import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager import org.koitharu.kotatsu.base.ui.list.FitHeightLinearLayoutManager
@@ -24,6 +25,7 @@ import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
@@ -35,6 +37,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesB
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.main.ui.AppBarOwner import org.koitharu.kotatsu.main.ui.AppBarOwner
@@ -49,7 +52,8 @@ abstract class MangaListFragment :
PaginationScrollListener.Callback, PaginationScrollListener.Callback,
MangaListListener, MangaListListener,
SwipeRefreshLayout.OnRefreshListener, SwipeRefreshLayout.OnRefreshListener,
ListSelectionController.Callback, FastScroller.FastScrollListener { ListSelectionController.Callback,
FastScroller.FastScrollListener {
private var listAdapter: MangaListAdapter? = null private var listAdapter: MangaListAdapter? = null
private var paginationListener: PaginationScrollListener? = null private var paginationListener: PaginationScrollListener? = null
@@ -71,7 +75,7 @@ abstract class MangaListFragment :
override fun onInflateView( override fun onInflateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?,
) = FragmentListBinding.inflate(inflater, container, false) ) = FragmentListBinding.inflate(inflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -104,6 +108,7 @@ abstract class MangaListFragment :
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -141,11 +146,21 @@ abstract class MangaListFragment :
Snackbar.make( Snackbar.make(
binding.recyclerView, binding.recyclerView,
e.getDisplayMessage(resources), e.getDisplayMessage(resources),
Snackbar.LENGTH_SHORT Snackbar.LENGTH_SHORT,
).show() ).show()
} }
} }
private fun onActionDone(action: ReversibleAction) {
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length)
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}
snackbar.show()
}
private fun resolveException(e: Throwable) { private fun resolveException(e: Throwable) {
if (ExceptionResolver.canResolve(e)) { if (ExceptionResolver.canResolve(e)) {
viewLifecycleScope.launch { viewLifecycleScope.launch {
@@ -201,6 +216,8 @@ abstract class MangaListFragment :
override fun onEmptyActionClick() = Unit override fun onEmptyActionClick() = Unit
override fun onListHeaderClick(item: ListHeader, view: View) = Unit
override fun onRetryClick(error: Throwable) { override fun onRetryClick(error: Throwable) {
resolveException(error) resolveException(error)
} }
@@ -225,7 +242,7 @@ abstract class MangaListFragment :
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing) val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
val decoration = TypedSpacingItemDecoration( val decoration = TypedSpacingItemDecoration(
MangaListAdapter.ITEM_TYPE_MANGA_LIST to 0, MangaListAdapter.ITEM_TYPE_MANGA_LIST to 0,
fallbackSpacing = spacing fallbackSpacing = spacing,
) )
addItemDecoration(decoration) addItemDecoration(decoration)
} }
@@ -332,4 +349,4 @@ abstract class MangaListFragment :
invalidateSpanIndexCache() invalidateSpanIndexCache()
} }
} }
} }

View File

@@ -6,12 +6,14 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsLiveData import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.SingleLiveEvent
abstract class MangaListViewModel( abstract class MangaListViewModel(
private val settings: AppSettings, private val settings: AppSettings,
@@ -19,6 +21,7 @@ abstract class MangaListViewModel(
abstract val content: LiveData<List<ListModel>> abstract val content: LiveData<List<ListModel>>
val listMode = MutableLiveData<ListMode>() val listMode = MutableLiveData<ListMode>()
val onActionDone = SingleLiveEvent<ReversibleAction>()
val gridScale = settings.observeAsLiveData( val gridScale = settings.observeAsLiveData(
context = viewModelScope.coroutineContext + Dispatchers.Default, context = viewModelScope.coroutineContext + Dispatchers.Default,
key = AppSettings.KEY_GRID_SIZE, key = AppSettings.KEY_GRID_SIZE,
@@ -37,4 +40,4 @@ abstract class MangaListViewModel(
abstract fun onRefresh() abstract fun onRefresh()
abstract fun onRetry() abstract fun onRetry()
} }

View File

@@ -1,46 +1,22 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import android.widget.TextView
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.databinding.ItemHeaderButtonBinding
import org.koitharu.kotatsu.core.ui.titleRes
import org.koitharu.kotatsu.databinding.ItemHeaderWithFilterBinding
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun listHeaderAD() = adapterDelegate<ListHeader, ListModel>( fun listHeaderAD(
layout = R.layout.item_header, listener: ListHeaderClickListener,
on = { item, _, _ -> item is ListHeader && item.sortOrder == null }, ) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderButtonBinding>(
{ inflater, parent -> ItemHeaderButtonBinding.inflate(inflater, parent, false) },
) { ) {
binding.buttonMore.setOnClickListener {
listener.onListHeaderClick(item, it)
}
bind { bind {
val textView = (itemView as TextView) binding.textViewTitle.text = item.getText(context)
if (item.text != null) { binding.buttonMore.setTextAndVisible(item.buttonTextRes)
textView.text = item.text
} else {
textView.setText(item.textRes)
}
} }
} }
fun listHeaderWithFilterAD(
listener: MangaListListener,
) = adapterDelegateViewBinding<ListHeader, ListModel, ItemHeaderWithFilterBinding>(
viewBinding = { inflater, parent -> ItemHeaderWithFilterBinding.inflate(inflater, parent, false) },
on = { item, _, _ -> item is ListHeader && item.sortOrder != null },
) {
binding.textViewFilter.setOnClickListener {
listener.onFilterClick(it)
}
bind {
if (item.text != null) {
binding.textViewTitle.text = item.text
} else {
binding.textViewTitle.setText(item.textRes)
}
binding.textViewFilter.setText(requireNotNull(item.sortOrder).titleRes)
}
}

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
import org.koitharu.kotatsu.list.ui.model.ListHeader
interface ListHeaderClickListener {
fun onListHeaderClick(item: ListHeader, view: View)
}

View File

@@ -25,9 +25,8 @@ open class MangaListAdapter(
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener)) .addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener)) .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener)) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD()) .addDelegate(ITEM_TYPE_HEADER, listHeaderAD(listener))
.addDelegate(ITEM_TYPE_HEADER_2, listHeader2AD(listener)) .addDelegate(ITEM_TYPE_HEADER_2, listHeader2AD(listener))
.addDelegate(ITEM_TYPE_HEADER_FILTER, listHeaderWithFilterAD(listener))
} }
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() { private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
@@ -45,6 +44,11 @@ open class MangaListAdapter(
oldItem is DateTimeAgo && newItem is DateTimeAgo -> { oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
oldItem == newItem oldItem == newItem
} }
oldItem is ListHeader && newItem is ListHeader -> {
oldItem.textRes == newItem.textRes &&
oldItem.text == newItem.text &&
oldItem.dateTimeAgo == newItem.dateTimeAgo
}
else -> oldItem.javaClass == newItem.javaClass else -> oldItem.javaClass == newItem.javaClass
} }
@@ -59,7 +63,6 @@ open class MangaListAdapter(
if (oldItem.progress != newItem.progress) { if (oldItem.progress != newItem.progress) {
PAYLOAD_PROGRESS PAYLOAD_PROGRESS
} else { } else {
Unit
} }
} }
is ListHeader2 -> Unit is ListHeader2 -> Unit
@@ -81,8 +84,7 @@ open class MangaListAdapter(
const val ITEM_TYPE_EMPTY = 8 const val ITEM_TYPE_EMPTY = 8
const val ITEM_TYPE_HEADER = 9 const val ITEM_TYPE_HEADER = 9
const val ITEM_TYPE_HEADER_2 = 10 const val ITEM_TYPE_HEADER_2 = 10
const val ITEM_TYPE_HEADER_FILTER = 11
val PAYLOAD_PROGRESS = Any() val PAYLOAD_PROGRESS = Any()
} }
} }

View File

@@ -5,9 +5,9 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
interface MangaListListener : OnListItemClickListener<Manga>, ListStateHolderListener { interface MangaListListener : OnListItemClickListener<Manga>, ListStateHolderListener, ListHeaderClickListener {
fun onUpdateFilter(tags: Set<MangaTag>) fun onUpdateFilter(tags: Set<MangaTag>)
fun onFilterClick(view: View?) fun onFilterClick(view: View?)
} }

View File

@@ -1,11 +1,62 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.core.ui.DateTimeAgo
@Deprecated("") class ListHeader private constructor(
data class ListHeader(
val text: CharSequence?, val text: CharSequence?,
@StringRes val textRes: Int, @StringRes val textRes: Int,
val sortOrder: SortOrder?, val dateTimeAgo: DateTimeAgo?,
) : ListModel @StringRes val buttonTextRes: Int,
val payload: Any?,
) : ListModel {
constructor(
text: CharSequence,
@StringRes buttonTextRes: Int,
payload: Any?,
) : this(text, 0, null, buttonTextRes, payload)
constructor(
@StringRes textRes: Int,
@StringRes buttonTextRes: Int,
payload: Any?,
) : this(null, textRes, null, buttonTextRes, payload)
constructor(
dateTimeAgo: DateTimeAgo,
@StringRes buttonTextRes: Int,
payload: Any?,
) : this(null, 0, dateTimeAgo, buttonTextRes, payload)
fun getText(context: Context): CharSequence? = when {
text != null -> text
textRes != 0 -> context.getString(textRes)
else -> dateTimeAgo?.format(context.resources)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ListHeader
if (text != other.text) return false
if (textRes != other.textRes) return false
if (dateTimeAgo != other.dateTimeAgo) return false
if (buttonTextRes != other.buttonTextRes) return false
if (payload != other.payload) return false
return true
}
override fun hashCode(): Int {
var result = text?.hashCode() ?: 0
result = 31 * result + textRes
result = 31 * result + (dateTimeAgo?.hashCode() ?: 0)
result = 31 * result + buttonTextRes
result = 31 * result + (payload?.hashCode() ?: 0)
return result
}
}

View File

@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesB
import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.SearchActivity
@@ -109,6 +110,8 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
override fun onEmptyActionClick() = Unit override fun onEmptyActionClick() = Unit
override fun onListHeaderClick(item: ListHeader, view: View) = Unit
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_remote, menu) mode.menuInflater.inflate(R.menu.mode_remote, menu)
return true return true
@@ -156,4 +159,4 @@ class MultiSearchActivity : BaseActivity<ActivitySearchMultiBinding>(), MangaLis
Intent(context, MultiSearchActivity::class.java) Intent(context, MultiSearchActivity::class.java)
.putExtra(EXTRA_QUERY, query) .putExtra(EXTRA_QUERY, query)
} }
} }

View File

@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.base.ui.list.decor.TypedSpacingItemDecoration
import org.koitharu.kotatsu.databinding.FragmentFeedBinding import org.koitharu.kotatsu.databinding.FragmentFeedBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -93,6 +94,8 @@ class FeedFragment :
override fun onEmptyActionClick() = Unit override fun onEmptyActionClick() = Unit
override fun onListHeaderClick(item: ListHeader, view: View) = Unit
private fun onListChanged(list: List<ListModel>) { private fun onListChanged(list: List<ListModel>) {
feedAdapter?.items = list feedAdapter?.items = list
} }
@@ -129,4 +132,4 @@ class FeedFragment :
fun newInstance() = FeedFragment() fun newInstance() = FeedFragment()
} }
} }

View File

@@ -1,8 +1,6 @@
package org.koitharu.kotatsu.tracker.ui package org.koitharu.kotatsu.tracker.ui
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import java.util.*
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
@@ -12,13 +10,18 @@ import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem import org.koitharu.kotatsu.tracker.domain.model.TrackingLogItem
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
import java.util.*
import java.util.concurrent.TimeUnit
class FeedViewModel( class FeedViewModel(
private val repository: TrackingRepository private val repository: TrackingRepository
@@ -27,7 +30,6 @@ class FeedViewModel(
private val logList = MutableStateFlow<List<TrackingLogItem>?>(null) private val logList = MutableStateFlow<List<TrackingLogItem>?>(null)
private val hasNextPage = MutableStateFlow(false) private val hasNextPage = MutableStateFlow(false)
private var loadingJob: Job? = null private var loadingJob: Job? = null
private val header = ListHeader(null, R.string.updates, null)
val onFeedCleared = SingleLiveEvent<Unit>() val onFeedCleared = SingleLiveEvent<Unit>()
val content = combine( val content = combine(
@@ -36,7 +38,6 @@ class FeedViewModel(
) { list, isHasNextPage -> ) { list, isHasNextPage ->
buildList(list.size + 2) { buildList(list.size + 2) {
if (list.isEmpty()) { if (list.isEmpty()) {
add(header)
add( add(
EmptyState( EmptyState(
icon = R.drawable.ic_empty_feed, icon = R.drawable.ic_empty_feed,
@@ -52,10 +53,7 @@ class FeedViewModel(
} }
} }
} }
}.asLiveDataDistinct( }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
viewModelScope.coroutineContext + Dispatchers.Default,
listOf(header, LoadingState)
)
init { init {
loadList(append = false) loadList(append = false)
@@ -114,4 +112,4 @@ class FeedViewModel(
else -> DateTimeAgo.Absolute(date) else -> DateTimeAgo.Absolute(date)
} }
} }
} }

View File

@@ -24,7 +24,6 @@ class FeedAdapter(
.addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener)) .addDelegate(ITEM_TYPE_ERROR_FOOTER, errorFooterAD(listener))
.addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener)) .addDelegate(ITEM_TYPE_ERROR_STATE, errorStateListAD(listener))
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener)) .addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD(listener))
.addDelegate(ITEM_TYPE_HEADER, listHeaderAD())
.addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD()) .addDelegate(ITEM_TYPE_DATE_HEADER, relatedDateItemAD())
} }
@@ -56,4 +55,4 @@ class FeedAdapter(
const val ITEM_TYPE_HEADER = 6 const val ITEM_TYPE_HEADER = 6
const val ITEM_TYPE_DATE_HEADER = 7 const val ITEM_TYPE_DATE_HEADER = 7
} }
} }

View File

@@ -4,16 +4,16 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:clipToPadding="false"
android:paddingHorizontal="@dimen/list_spacing">
<androidx.recyclerview.widget.RecyclerView <org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
app:bubbleSize="small"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_feed" /> tools:listitem="@layout/item_feed" />
</FrameLayout> </FrameLayout>

View File

@@ -15,6 +15,7 @@
android:id="@+id/imageView_icon" android:id="@+id/imageView_icon"
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:background="?colorControlHighlight"
android:labelFor="@id/textView_title" android:labelFor="@id/textView_title"
android:scaleType="fitCenter" android:scaleType="fitCenter"
app:shapeAppearance="?shapeAppearanceCornerSmall" app:shapeAppearance="?shapeAppearanceCornerSmall"
@@ -30,4 +31,4 @@
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAppearance="?attr/textAppearanceBodyMedium"
tools:text="@tools:sample/lorem[2]" /> tools:text="@tools:sample/lorem[2]" />
</LinearLayout> </LinearLayout>

View File

@@ -9,8 +9,8 @@
<TextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical|start" android:gravity="center_vertical|start"
android:padding="@dimen/grid_spacing" android:padding="@dimen/grid_spacing"
android:singleLine="true" android:singleLine="true"
@@ -24,4 +24,4 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/manage" /> android:text="@string/manage" />
</LinearLayout> </LinearLayout>

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/textView_filter"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
tools:text="@tools:sample/lorem[21]" />
<TextView
android:id="@+id/textView_filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@drawable/list_selector"
android:gravity="center_vertical"
android:paddingStart="6dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
app:drawableEndCompat="@drawable/ic_expand_more"
app:drawableTint="?android:attr/textColorSecondary"
tools:ignore="RtlSymmetry"
tools:text="@string/popular" />
</RelativeLayout>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:icon="?actionModeShareDrawable"
android:title="@string/share"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_remove"
android:icon="@drawable/ic_delete"
android:title="@string/remove"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_favourite"
android:icon="@drawable/ic_heart_outline"
android:title="@string/add_to_favourites"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_save"
android:title="@string/save"
app:showAsAction="ifRoom|withText" />
</menu>