Update library fragment
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,4 +91,4 @@ sealed class LibrarySectionModel(
|
|||||||
return "fav_${category.id}"
|
return "fav_${category.id}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
30
app/src/main/res/menu/mode_library.xml
Normal file
30
app/src/main/res/menu/mode_library.xml
Normal 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>
|
||||||
Reference in New Issue
Block a user