Remove shelf
This commit is contained in:
@@ -29,7 +29,6 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
|
||||
import java.io.File
|
||||
import java.net.Proxy
|
||||
import java.util.Collections
|
||||
@@ -54,22 +53,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val remoteMangaSources: Set<MangaSource>
|
||||
get() = Collections.unmodifiableSet(remoteSources)
|
||||
|
||||
var shelfSections: List<ShelfSection>
|
||||
get() {
|
||||
val raw = prefs.getString(KEY_SHELF_SECTIONS, null)
|
||||
val values = enumValues<ShelfSection>()
|
||||
if (raw.isNullOrEmpty()) {
|
||||
return values.toList()
|
||||
}
|
||||
return raw.split('|')
|
||||
.mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) }
|
||||
.distinct()
|
||||
}
|
||||
set(value) {
|
||||
val raw = value.joinToString("|") { it.ordinal.toString() }
|
||||
prefs.edit { putString(KEY_SHELF_SECTIONS, raw) }
|
||||
}
|
||||
|
||||
var listMode: ListMode
|
||||
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
|
||||
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
|
||||
@@ -498,7 +481,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
|
||||
const val KEY_LOCAL_LIST_ORDER = "local_order"
|
||||
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
|
||||
const val KEY_SHELF_SECTIONS = "shelf_sections_2"
|
||||
const val KEY_PREFETCH_CONTENT = "prefetch_content"
|
||||
const val KEY_APP_LOCALE = "app_locale"
|
||||
const val KEY_LOGGING_ENABLED = "logging"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.koitharu.kotatsu.favourites.data
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@@ -15,6 +19,9 @@ abstract class FavouriteCategoriesDao {
|
||||
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 ORDER BY sort_key")
|
||||
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key")
|
||||
abstract fun observeAllForLibrary(): Flow<List<FavouriteCategoryEntity>>
|
||||
|
||||
@Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0")
|
||||
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity?>
|
||||
|
||||
|
||||
@@ -67,6 +67,12 @@ class FavouritesRepository @Inject constructor(
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeCategoriesForLibrary(): Flow<List<FavouriteCategory>> {
|
||||
return db.favouriteCategoriesDao.observeAllForLibrary().mapItems {
|
||||
it.toFavouriteCategory()
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<Cover>>> {
|
||||
return db.favouriteCategoriesDao.observeAll()
|
||||
.map {
|
||||
|
||||
@@ -14,10 +14,10 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class FavouritesContainerViewModel @Inject constructor(
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
favouritesRepository: FavouritesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val categories = favouritesRepository.observeCategories()
|
||||
val categories = favouritesRepository.observeCategoriesForLibrary()
|
||||
.mapItems { FavouriteTabModel(it.id, it.title) }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.domain
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
||||
import org.koitharu.kotatsu.favourites.data.toMangaList
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.shelf.domain.model.ShelfContent
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
class ShelfContentObserveUseCase @Inject constructor(
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val suggestionRepository: SuggestionRepository,
|
||||
private val db: MangaDatabase,
|
||||
private val settings: AppSettings,
|
||||
@LocalStorageChanges private val localStorageChanges: SharedFlow<LocalManga?>,
|
||||
) {
|
||||
|
||||
operator fun invoke(): Flow<ShelfContent> = combine(
|
||||
historyRepository.observeAll(20),
|
||||
observeLocalManga(SortOrder.UPDATED, 20),
|
||||
observeFavourites(),
|
||||
trackingRepository.observeUpdatedManga(),
|
||||
suggestionRepository.observeAll(20),
|
||||
) { history, local, favorites, updated, suggestions ->
|
||||
ShelfContent(history, favorites, updated, local, suggestions)
|
||||
}
|
||||
|
||||
private fun observeLocalManga(sortOrder: SortOrder, limit: Int): Flow<List<Manga>> {
|
||||
return combine<LocalManga?, String, Any?>(
|
||||
localStorageChanges,
|
||||
settings.observe().filter { it == AppSettings.KEY_LOCAL_MANGA_DIRS }.onStart { emit("") }
|
||||
) { a, b -> a to b }
|
||||
.onStart { emit(null) }
|
||||
.mapLatest {
|
||||
localMangaRepository.getList(0, null, sortOrder).take(limit)
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun observeFavourites(): Flow<Map<FavouriteCategory, List<Manga>>> {
|
||||
return db.favouriteCategoriesDao.observeAll()
|
||||
.flatMapLatest { categories ->
|
||||
val cats = categories.filter { it.isVisibleInLibrary }
|
||||
if (cats.isEmpty()) {
|
||||
flowOf(emptyMap())
|
||||
} else {
|
||||
observeCategoriesContent(cats)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeCategoriesContent(
|
||||
categories: List<FavouriteCategoryEntity>,
|
||||
) = combine<Pair<FavouriteCategory, List<Manga>>, Map<FavouriteCategory, List<Manga>>>(
|
||||
categories.map { cat ->
|
||||
val category = cat.toFavouriteCategory()
|
||||
db.favouritesDao.observeAll(category.id, category.order)
|
||||
.map { category to it.toMangaList() }
|
||||
},
|
||||
) { array -> array.toMap() }
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.domain.model
|
||||
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
class ShelfContent(
|
||||
val history: List<Manga>,
|
||||
val favourites: Map<FavouriteCategory, List<Manga>>,
|
||||
val updated: List<Manga>,
|
||||
val local: List<Manga>,
|
||||
val suggestions: List<Manga>,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ShelfContent
|
||||
|
||||
if (history != other.history) return false
|
||||
if (favourites != other.favourites) return false
|
||||
if (updated != other.updated) return false
|
||||
if (local != other.local) return false
|
||||
return suggestions == other.suggestions
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = history.hashCode()
|
||||
result = 31 * result + favourites.hashCode()
|
||||
result = 31 * result + updated.hashCode()
|
||||
result = 31 * result + local.hashCode()
|
||||
result = 31 * result + suggestions.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.domain.model
|
||||
|
||||
enum class ShelfSection {
|
||||
|
||||
HISTORY, LOCAL, UPDATED, FAVORITES, SUGGESTIONS;
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.list.NestedScrollStateHandle
|
||||
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.FragmentShelfBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
|
||||
import org.koitharu.kotatsu.history.ui.HistoryActivity
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.search.ui.MangaListActivity
|
||||
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfAdapter
|
||||
import org.koitharu.kotatsu.shelf.ui.adapter.ShelfListEventListener
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
|
||||
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShelfFragment :
|
||||
BaseFragment<FragmentShelfBinding>(),
|
||||
RecyclerViewOwner,
|
||||
ShelfListEventListener {
|
||||
|
||||
@Inject
|
||||
lateinit var coil: ImageLoader
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
private var nestedScrollStateHandle: NestedScrollStateHandle? = null
|
||||
private val viewModel by viewModels<ShelfViewModel>()
|
||||
private var adapter: ShelfAdapter? = null
|
||||
private var selectionController: SectionedSelectionController<ShelfSectionModel>? = null
|
||||
|
||||
override val recyclerView: RecyclerView
|
||||
get() = requireViewBinding().recyclerView
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentShelfBinding {
|
||||
return FragmentShelfBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentShelfBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
nestedScrollStateHandle = NestedScrollStateHandle(savedInstanceState, KEY_NESTED_SCROLL)
|
||||
val sizeResolver = DynamicItemSizeResolver(resources, settings)
|
||||
selectionController = SectionedSelectionController(
|
||||
activity = requireActivity(),
|
||||
owner = this,
|
||||
callback = ShelfSelectionCallback(binding.recyclerView, childFragmentManager, viewModel),
|
||||
)
|
||||
adapter = ShelfAdapter(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
coil = coil,
|
||||
listener = this,
|
||||
sizeResolver = sizeResolver,
|
||||
selectionController = checkNotNull(selectionController),
|
||||
nestedScrollStateHandle = checkNotNull(nestedScrollStateHandle),
|
||||
)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
addMenuProvider(ShelfMenuProvider(binding.root.context, childFragmentManager, viewModel))
|
||||
|
||||
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
|
||||
viewModel.onDownloadStarted.observeEvent(viewLifecycleOwner, DownloadStartedObserver(binding.recyclerView))
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
nestedScrollStateHandle?.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
adapter = null
|
||||
selectionController = null
|
||||
nestedScrollStateHandle = null
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Manga, section: ShelfSectionModel, view: View) {
|
||||
if (selectionController?.onItemClick(section, item.id) != true) {
|
||||
val intent = DetailsActivity.newIntent(view.context, item)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Manga, section: ShelfSectionModel, view: View): Boolean {
|
||||
return selectionController?.onItemLongClick(section, item.id) ?: false
|
||||
}
|
||||
|
||||
override fun onSectionClick(section: ShelfSectionModel, view: View) {
|
||||
selectionController?.clear()
|
||||
val intent = when (section) {
|
||||
is ShelfSectionModel.History -> HistoryActivity.newIntent(view.context)
|
||||
is ShelfSectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category)
|
||||
is ShelfSectionModel.Updated -> UpdatesActivity.newIntent(view.context)
|
||||
is ShelfSectionModel.Local -> MangaListActivity.newIntent(view.context, MangaSource.LOCAL)
|
||||
is ShelfSectionModel.Suggestions -> SuggestionsActivity.newIntent(view.context)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
|
||||
override fun onEmptyActionClick() {
|
||||
val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Settings.Panel.ACTION_INTERNET_CONNECTIVITY
|
||||
} else {
|
||||
Settings.ACTION_WIRELESS_SETTINGS
|
||||
}
|
||||
startActivity(Intent(action))
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
requireViewBinding().recyclerView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onListChanged(list: List<ListModel>) {
|
||||
adapter?.items = list
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_NESTED_SCROLL = "nested_scroll"
|
||||
|
||||
fun newInstance() = ShelfFragment()
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
|
||||
import org.koitharu.kotatsu.core.util.ext.startOfDay
|
||||
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
||||
import org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity
|
||||
import org.koitharu.kotatsu.shelf.ui.config.size.ShelfSizeBottomSheet
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class ShelfMenuProvider(
|
||||
private val context: Context,
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val viewModel: ShelfViewModel,
|
||||
) : MenuProvider {
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_shelf, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return when (menuItem.itemId) {
|
||||
R.id.action_clear_history -> {
|
||||
showClearHistoryDialog()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_grid_size -> {
|
||||
ShelfSizeBottomSheet.show(fragmentManager)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_import -> {
|
||||
ImportDialogFragment.show(fragmentManager)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_categories -> {
|
||||
context.startActivity(ShelfSettingsActivity.newIntent(context))
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun showClearHistoryDialog() {
|
||||
val selectionListener = RememberSelectionDialogListener(2)
|
||||
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
|
||||
.setTitle(R.string.clear_history)
|
||||
.setSingleChoiceItems(
|
||||
arrayOf(
|
||||
context.getString(R.string.last_2_hours),
|
||||
context.getString(R.string.today),
|
||||
context.getString(R.string.clear_all_history),
|
||||
),
|
||||
selectionListener.selection,
|
||||
selectionListener,
|
||||
)
|
||||
.setIcon(R.drawable.ic_delete)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
val minDate = when (selectionListener.selection) {
|
||||
0 -> System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2)
|
||||
1 -> Date().startOfDay()
|
||||
2 -> 0L
|
||||
else -> return@setPositiveButton
|
||||
}
|
||||
viewModel.clearHistory(minDate)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
|
||||
import org.koitharu.kotatsu.core.util.ShareHelper
|
||||
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesSheet
|
||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.flattenTo
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
|
||||
class ShelfSelectionCallback(
|
||||
private val recyclerView: RecyclerView,
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val viewModel: ShelfViewModel,
|
||||
) : SectionedSelectionController.Callback<ShelfSectionModel> {
|
||||
|
||||
private val context: Context
|
||||
get() = recyclerView.context
|
||||
|
||||
override fun onCreateActionMode(
|
||||
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||
mode: ActionMode,
|
||||
menu: Menu,
|
||||
): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.mode_shelf, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(
|
||||
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||
mode: ActionMode,
|
||||
menu: Menu,
|
||||
): Boolean {
|
||||
val checkedIds = controller.peekCheckedIds().entries
|
||||
val singleKey = checkedIds.singleOrNull { (_, ids) -> ids.isNotEmpty() }?.key
|
||||
menu.findItem(R.id.action_remove)?.isVisible = singleKey != null &&
|
||||
singleKey !is ShelfSectionModel.Updated &&
|
||||
singleKey !is ShelfSectionModel.Suggestions
|
||||
menu.findItem(R.id.action_save)?.isVisible = singleKey !is ShelfSectionModel.Local
|
||||
return super.onPrepareActionMode(controller, mode, menu)
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(
|
||||
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||
mode: ActionMode,
|
||||
item: MenuItem,
|
||||
): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_share -> {
|
||||
ShareHelper(context).shareMangaLinks(collectSelectedItems(controller))
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_favourite -> {
|
||||
FavouriteCategoriesSheet.show(fragmentManager, collectSelectedItems(controller))
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_save -> {
|
||||
viewModel.download(collectSelectedItems(controller))
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_remove -> {
|
||||
val (group, ids) = controller.snapshot().entries.singleOrNull { it.value.isNotEmpty() } ?: return false
|
||||
when (group) {
|
||||
is ShelfSectionModel.Favourites -> viewModel.removeFromFavourites(group.category, ids)
|
||||
is ShelfSectionModel.History -> viewModel.removeFromHistory(ids)
|
||||
is ShelfSectionModel.Updated -> return false
|
||||
is ShelfSectionModel.Local -> {
|
||||
showDeletionConfirm(ids, mode)
|
||||
return true
|
||||
}
|
||||
|
||||
is ShelfSectionModel.Suggestions -> return false
|
||||
}
|
||||
mode.finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(controller: SectionedSelectionController<ShelfSectionModel>, count: Int) {
|
||||
recyclerView.invalidateNestedItemDecorations()
|
||||
}
|
||||
|
||||
override fun onCreateItemDecoration(
|
||||
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||
section: ShelfSectionModel,
|
||||
): AbstractSelectionItemDecoration = MangaSelectionDecoration(context)
|
||||
|
||||
private fun collectSelectedItemsMap(
|
||||
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||
): Map<ShelfSectionModel, Set<Manga>> {
|
||||
val snapshot = controller.peekCheckedIds()
|
||||
if (snapshot.isEmpty()) {
|
||||
return emptyMap()
|
||||
}
|
||||
return snapshot.mapValues { (_, ids) -> viewModel.getManga(ids) }
|
||||
}
|
||||
|
||||
private fun collectSelectedItems(
|
||||
controller: SectionedSelectionController<ShelfSectionModel>,
|
||||
): Set<Manga> {
|
||||
val snapshot = controller.peekCheckedIds()
|
||||
if (snapshot.isEmpty()) {
|
||||
return emptySet()
|
||||
}
|
||||
return viewModel.getManga(snapshot.values.flattenTo(HashSet()))
|
||||
}
|
||||
|
||||
private fun showDeletionConfirm(ids: Set<Long>, mode: ActionMode) {
|
||||
if (ids.isEmpty()) {
|
||||
return
|
||||
}
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.delete_manga)
|
||||
.setMessage(context.getString(R.string.text_delete_local_manga_batch))
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
viewModel.deleteLocal(ids)
|
||||
mode.finish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.os.NetworkState
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.shelf.domain.ShelfContentObserveUseCase
|
||||
import org.koitharu.kotatsu.shelf.domain.model.ShelfContent
|
||||
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ShelfViewModel @Inject constructor(
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val settings: AppSettings,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val deleteLocalMangaUseCase: DeleteLocalMangaUseCase,
|
||||
private val listExtraProvider: ListExtraProvider,
|
||||
shelfContentObserveUseCase: ShelfContentObserveUseCase,
|
||||
syncController: SyncController,
|
||||
networkState: NetworkState,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
|
||||
val content: StateFlow<List<ListModel>> = combine(
|
||||
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
|
||||
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
|
||||
settings.observeAsFlow(AppSettings.KEY_SUGGESTIONS) { isSuggestionsEnabled },
|
||||
networkState,
|
||||
shelfContentObserveUseCase(),
|
||||
) { sections, isTrackerEnabled, isSuggestionsEnabled, isConnected, content ->
|
||||
mapList(content, isTrackerEnabled, isSuggestionsEnabled, sections, isConnected)
|
||||
}.catch { e ->
|
||||
emit(listOf(e.toErrorState(canRetry = false)))
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
|
||||
|
||||
init {
|
||||
launchJob(Dispatchers.Default) {
|
||||
syncController.requestFullSync()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromFavourites(category: FavouriteCategory, ids: Set<Long>) {
|
||||
if (ids.isEmpty()) {
|
||||
return
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = favouritesRepository.removeFromCategory(category.id, ids)
|
||||
onActionDone.call(ReversibleAction(R.string.removed_from_favourites, handle))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFromHistory(ids: Set<Long>) {
|
||||
if (ids.isEmpty()) {
|
||||
return
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = historyRepository.delete(ids)
|
||||
onActionDone.call(ReversibleAction(R.string.removed_from_history, handle))
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteLocal(ids: Set<Long>) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
deleteLocalMangaUseCase(ids)
|
||||
onActionDone.call(ReversibleAction(R.string.removal_completed, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearHistory(minDate: Long) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
val stringRes = if (minDate <= 0) {
|
||||
historyRepository.clear()
|
||||
R.string.history_cleared
|
||||
} else {
|
||||
historyRepository.deleteAfter(minDate)
|
||||
R.string.removed_from_history
|
||||
}
|
||||
onActionDone.call(ReversibleAction(stringRes, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun getManga(ids: Set<Long>): Set<Manga> {
|
||||
val snapshot = content.value
|
||||
val result = ArraySet<Manga>(ids.size)
|
||||
for (section in snapshot) {
|
||||
if (section !is ShelfSectionModel) {
|
||||
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 download(items: Set<Manga>) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
downloadScheduler.schedule(items)
|
||||
onDownloadStarted.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun mapList(
|
||||
content: ShelfContent,
|
||||
isTrackerEnabled: Boolean,
|
||||
isSuggestionsEnabled: Boolean,
|
||||
sections: List<ShelfSection>,
|
||||
isNetworkAvailable: Boolean,
|
||||
): List<ListModel> {
|
||||
val result = ArrayList<ListModel>(content.favourites.keys.size + sections.size)
|
||||
if (isNetworkAvailable) {
|
||||
for (section in sections) {
|
||||
when (section) {
|
||||
ShelfSection.HISTORY -> mapHistory(result, content.history)
|
||||
ShelfSection.LOCAL -> mapLocal(result, content.local)
|
||||
ShelfSection.UPDATED -> if (isTrackerEnabled) {
|
||||
mapUpdated(result, content.updated)
|
||||
}
|
||||
|
||||
ShelfSection.FAVORITES -> mapFavourites(result, content.favourites)
|
||||
ShelfSection.SUGGESTIONS -> if (isSuggestionsEnabled) {
|
||||
mapSuggestions(result, content.suggestions)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result += EmptyHint(
|
||||
icon = R.drawable.ic_empty_common,
|
||||
textPrimary = R.string.network_unavailable,
|
||||
textSecondary = R.string.network_unavailable_hint,
|
||||
actionStringRes = R.string.manage,
|
||||
)
|
||||
for (section in sections) {
|
||||
when (section) {
|
||||
ShelfSection.HISTORY -> mapHistory(
|
||||
result,
|
||||
content.history.filter { it.source == MangaSource.LOCAL },
|
||||
)
|
||||
|
||||
ShelfSection.LOCAL -> mapLocal(result, content.local)
|
||||
ShelfSection.UPDATED -> Unit
|
||||
ShelfSection.FAVORITES -> Unit
|
||||
ShelfSection.SUGGESTIONS -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
result += EmptyState(
|
||||
icon = R.drawable.ic_empty_history,
|
||||
textPrimary = R.string.text_shelf_holder_primary,
|
||||
textSecondary = R.string.text_shelf_holder_secondary,
|
||||
actionStringRes = 0,
|
||||
)
|
||||
} else {
|
||||
val one = result.singleOrNull()
|
||||
if (one is EmptyHint) {
|
||||
result[0] = one.toState()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun mapHistory(
|
||||
destination: MutableList<in ShelfSectionModel.History>,
|
||||
list: List<Manga>,
|
||||
) {
|
||||
if (list.isEmpty()) {
|
||||
return
|
||||
}
|
||||
destination += ShelfSectionModel.History(
|
||||
items = list.map { manga ->
|
||||
manga.toGridModel(listExtraProvider)
|
||||
},
|
||||
showAllButtonText = R.string.show_all,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun mapUpdated(
|
||||
destination: MutableList<in ShelfSectionModel.Updated>,
|
||||
updated: List<Manga>,
|
||||
) {
|
||||
if (updated.isEmpty()) {
|
||||
return
|
||||
}
|
||||
settings.isReadingIndicatorsEnabled
|
||||
destination += ShelfSectionModel.Updated(
|
||||
items = updated.map { manga ->
|
||||
manga.toGridModel(listExtraProvider)
|
||||
},
|
||||
showAllButtonText = R.string.show_all,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun mapLocal(
|
||||
destination: MutableList<in ShelfSectionModel.Local>,
|
||||
local: List<Manga>,
|
||||
) {
|
||||
if (local.isEmpty()) {
|
||||
return
|
||||
}
|
||||
destination += ShelfSectionModel.Local(
|
||||
items = local.toUi(ListMode.GRID, listExtraProvider),
|
||||
showAllButtonText = R.string.show_all,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun mapSuggestions(
|
||||
destination: MutableList<in ShelfSectionModel.Suggestions>,
|
||||
suggestions: List<Manga>,
|
||||
) {
|
||||
if (suggestions.isEmpty()) {
|
||||
return
|
||||
}
|
||||
destination += ShelfSectionModel.Suggestions(
|
||||
items = suggestions.toUi(ListMode.GRID, listExtraProvider),
|
||||
showAllButtonText = R.string.show_all,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun mapFavourites(
|
||||
destination: MutableList<in ShelfSectionModel.Favourites>,
|
||||
favourites: Map<FavouriteCategory, List<Manga>>,
|
||||
) {
|
||||
if (favourites.isEmpty()) {
|
||||
return
|
||||
}
|
||||
for ((category, list) in favourites) {
|
||||
if (list.isNotEmpty()) {
|
||||
destination += ShelfSectionModel.Favourites(
|
||||
items = list.toUi(ListMode.GRID, listExtraProvider),
|
||||
category = category,
|
||||
showAllButtonText = R.string.show_all,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class ScrollKeepObserver(
|
||||
private val recyclerView: RecyclerView,
|
||||
) : RecyclerView.AdapterDataObserver() {
|
||||
|
||||
private val layoutManager: LinearLayoutManager
|
||||
get() = recyclerView.layoutManager as LinearLayoutManager
|
||||
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
|
||||
val position = minOf(toPosition, fromPosition) // if items are swapping positions may be swapped too
|
||||
if (firstVisiblePosition != RecyclerView.NO_POSITION && (position == 0 || position < firstVisiblePosition)) {
|
||||
postScroll(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition()
|
||||
if (firstVisiblePosition != RecyclerView.NO_POSITION && (positionStart == 0 || positionStart < firstVisiblePosition)) {
|
||||
postScroll(positionStart)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postScroll(targetPosition: Int) {
|
||||
recyclerView.post {
|
||||
layoutManager.scrollToPositionWithOffset(targetPosition, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.list.NestedScrollStateHandle
|
||||
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
|
||||
class ShelfAdapter(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
coil: ImageLoader,
|
||||
listener: ShelfListEventListener,
|
||||
sizeResolver: ItemSizeResolver,
|
||||
selectionController: SectionedSelectionController<ShelfSectionModel>,
|
||||
nestedScrollStateHandle: NestedScrollStateHandle,
|
||||
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
|
||||
|
||||
init {
|
||||
val pool = RecyclerView.RecycledViewPool()
|
||||
delegatesManager.addDelegate(
|
||||
shelfGroupAD(
|
||||
sharedPool = pool,
|
||||
lifecycleOwner = lifecycleOwner,
|
||||
coil = coil,
|
||||
sizeResolver = sizeResolver,
|
||||
selectionController = selectionController,
|
||||
listener = listener,
|
||||
nestedScrollStateHandle = nestedScrollStateHandle,
|
||||
),
|
||||
).addDelegate(loadingStateAD()).addDelegate(loadingFooterAD())
|
||||
.addDelegate(emptyHintAD(coil, lifecycleOwner, listener))
|
||||
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener)).addDelegate(errorStateListAD(listener))
|
||||
}
|
||||
|
||||
override fun getSectionText(context: Context, position: Int): CharSequence? {
|
||||
val item = items.getOrNull(position) as? ShelfSectionModel ?: return null
|
||||
return item.getTitle(context.resources)
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.list.NestedScrollStateHandle
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
|
||||
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.core.util.ext.removeItemDecoration
|
||||
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
|
||||
import org.koitharu.kotatsu.databinding.ItemListGroupBinding
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
|
||||
fun shelfGroupAD(
|
||||
sharedPool: RecyclerView.RecycledViewPool,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
coil: ImageLoader,
|
||||
sizeResolver: ItemSizeResolver,
|
||||
selectionController: SectionedSelectionController<ShelfSectionModel>,
|
||||
listener: ShelfListEventListener,
|
||||
nestedScrollStateHandle: NestedScrollStateHandle,
|
||||
) = adapterDelegateViewBinding<ShelfSectionModel, ListModel, ItemListGroupBinding>(
|
||||
{ layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
val listenerAdapter = object : OnListItemClickListener<Manga>, View.OnClickListener {
|
||||
override fun onItemClick(item: Manga, view: View) {
|
||||
listener.onItemClick(item, this@adapterDelegateViewBinding.item, view)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: Manga, view: View): Boolean {
|
||||
return listener.onItemLongClick(item, this@adapterDelegateViewBinding.item, view)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
listener.onSectionClick(item, itemView)
|
||||
}
|
||||
}
|
||||
|
||||
val adapter = AsyncListDifferDelegationAdapter(
|
||||
ListModelDiffCallback,
|
||||
mangaGridItemAD(coil, lifecycleOwner, sizeResolver, listenerAdapter),
|
||||
)
|
||||
adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
|
||||
adapter.registerAdapterDataObserver(ScrollKeepObserver(binding.recyclerView))
|
||||
binding.recyclerView.setRecycledViewPool(sharedPool)
|
||||
binding.recyclerView.adapter = adapter
|
||||
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
|
||||
binding.recyclerView.addItemDecoration(spacingDecoration)
|
||||
binding.buttonMore.setOnClickListener(listenerAdapter)
|
||||
val stateController = nestedScrollStateHandle.attach(binding.recyclerView)
|
||||
|
||||
bind {
|
||||
selectionController.attachToRecyclerView(item, binding.recyclerView)
|
||||
binding.textViewTitle.text = item.getTitle(context.resources)
|
||||
binding.buttonMore.setTextAndVisible(item.showAllButtonText)
|
||||
adapter.items = item.items
|
||||
stateController.onBind(bindingAdapterPosition)
|
||||
}
|
||||
|
||||
onViewRecycled {
|
||||
stateController.onRecycled()
|
||||
adapter.items = emptyList()
|
||||
binding.recyclerView.removeItemDecoration(AbstractSelectionItemDecoration::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.adapter
|
||||
|
||||
import android.view.View
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
|
||||
interface ShelfListEventListener : ListStateHolderListener {
|
||||
|
||||
fun onItemClick(item: Manga, section: ShelfSectionModel, view: View)
|
||||
|
||||
fun onItemLongClick(item: Manga, section: ShelfSectionModel, view: View): Boolean
|
||||
|
||||
fun onSectionClick(section: ShelfSectionModel, view: View)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.config
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.ActivityShelfSettingsBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShelfSettingsActivity :
|
||||
BaseActivity<ActivityShelfSettingsBinding>(),
|
||||
View.OnClickListener, ShelfSettingsListener {
|
||||
|
||||
private val viewModel by viewModels<ShelfSettingsViewModel>()
|
||||
private lateinit var reorderHelper: ItemTouchHelper
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(ActivityShelfSettingsBinding.inflate(layoutInflater))
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
|
||||
}
|
||||
viewBinding.buttonDone.setOnClickListener(this)
|
||||
val settingsAdapter = ShelfSettingsAdapter(this)
|
||||
with(viewBinding.recyclerView) {
|
||||
setHasFixedSize(true)
|
||||
adapter = settingsAdapter
|
||||
reorderHelper = ItemTouchHelper(SectionsReorderCallback()).also {
|
||||
it.attachToRecyclerView(this)
|
||||
}
|
||||
}
|
||||
viewModel.content.observe(this) { settingsAdapter.items = it }
|
||||
}
|
||||
|
||||
override fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean) {
|
||||
viewModel.setItemChecked(item, isChecked)
|
||||
}
|
||||
|
||||
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) {
|
||||
reorderHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
finishAfterTransition()
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
viewBinding.root.updatePadding(
|
||||
left = insets.left,
|
||||
right = insets.right,
|
||||
)
|
||||
viewBinding.recyclerView.updatePadding(
|
||||
bottom = insets.bottom,
|
||||
)
|
||||
viewBinding.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.top
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SectionsReorderCallback : ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
|
||||
0,
|
||||
) {
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder,
|
||||
): Boolean = viewHolder.itemViewType == target.itemViewType && viewModel.reorderSections(
|
||||
viewHolder.bindingAdapterPosition,
|
||||
target.bindingAdapterPosition,
|
||||
)
|
||||
|
||||
override fun canDropOver(
|
||||
recyclerView: RecyclerView,
|
||||
current: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder,
|
||||
): Boolean = current.itemViewType == target.itemViewType
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
|
||||
|
||||
override fun isLongPressDragEnabled() = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context) = Intent(context, ShelfSettingsActivity::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.config
|
||||
|
||||
import org.koitharu.kotatsu.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
class ShelfSettingsAdapter(
|
||||
listener: ShelfSettingsListener,
|
||||
) : BaseListAdapter<ListModel>() {
|
||||
|
||||
init {
|
||||
delegatesManager.addDelegate(shelfCategoryAD(listener))
|
||||
.addDelegate(shelfSectionAD(listener))
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.config
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import androidx.core.view.updatePaddingRelative
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.setChecked
|
||||
import org.koitharu.kotatsu.databinding.ItemCategoryCheckableMultipleBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemShelfSectionDraggableBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun shelfSectionAD(
|
||||
listener: ShelfSettingsListener,
|
||||
) =
|
||||
adapterDelegateViewBinding<ShelfSettingsItemModel.Section, ListModel, ItemShelfSectionDraggableBinding>(
|
||||
{ layoutInflater, parent -> ItemShelfSectionDraggableBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
|
||||
val eventListener = object :
|
||||
View.OnTouchListener,
|
||||
CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
override fun onTouch(v: View?, event: MotionEvent): Boolean {
|
||||
return if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
listener.onDragHandleTouch(this@adapterDelegateViewBinding)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
|
||||
listener.onItemCheckedChanged(item, isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
binding.switchToggle.setOnCheckedChangeListener(eventListener)
|
||||
binding.imageViewHandle.setOnTouchListener(eventListener)
|
||||
|
||||
bind { payloads ->
|
||||
binding.textViewTitle.setText(item.section.titleResId)
|
||||
binding.switchToggle.setChecked(item.isChecked, payloads.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun shelfCategoryAD(
|
||||
listener: ShelfSettingsListener,
|
||||
) =
|
||||
adapterDelegateViewBinding<ShelfSettingsItemModel.FavouriteCategory, ListModel, ItemCategoryCheckableMultipleBinding>(
|
||||
{ layoutInflater, parent -> ItemCategoryCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
|
||||
) {
|
||||
itemView.setOnClickListener {
|
||||
listener.onItemCheckedChanged(item, !item.isChecked)
|
||||
}
|
||||
binding.root.updatePaddingRelative(
|
||||
start = binding.root.paddingStart * 2,
|
||||
end = binding.root.paddingStart,
|
||||
)
|
||||
|
||||
bind { payloads ->
|
||||
binding.root.text = item.title
|
||||
binding.root.setChecked(item.isChecked, payloads.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private val ShelfSection.titleResId: Int
|
||||
get() = when (this) {
|
||||
ShelfSection.HISTORY -> R.string.history
|
||||
ShelfSection.LOCAL -> R.string.local_storage
|
||||
ShelfSection.UPDATED -> R.string.updated
|
||||
ShelfSection.FAVORITES -> R.string.favourites
|
||||
ShelfSection.SUGGESTIONS -> R.string.suggestions
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.config
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
|
||||
|
||||
sealed interface ShelfSettingsItemModel : ListModel {
|
||||
|
||||
val isChecked: Boolean
|
||||
|
||||
class Section(
|
||||
val section: ShelfSection,
|
||||
override val isChecked: Boolean,
|
||||
) : ShelfSettingsItemModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is Section && section == other.section
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return if (previousState is Section && previousState.isChecked != isChecked) {
|
||||
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
|
||||
} else {
|
||||
super.getChangePayload(previousState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Section
|
||||
|
||||
if (section != other.section) return false
|
||||
return isChecked == other.isChecked
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = section.hashCode()
|
||||
result = 31 * result + isChecked.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class FavouriteCategory(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
override val isChecked: Boolean,
|
||||
) : ShelfSettingsItemModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is FavouriteCategory && other.id == id
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FavouriteCategory
|
||||
|
||||
if (id != other.id) return false
|
||||
if (title != other.title) return false
|
||||
return isChecked == other.isChecked
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + isChecked.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.config
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
interface ShelfSettingsListener {
|
||||
|
||||
fun onItemCheckedChanged(item: ShelfSettingsItemModel, isChecked: Boolean)
|
||||
|
||||
fun onDragHandleTouch(holder: RecyclerView.ViewHolder)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.config
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.parsers.util.move
|
||||
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ShelfSettingsViewModel @Inject constructor(
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val content = combine(
|
||||
settings.observeAsFlow(AppSettings.KEY_SHELF_SECTIONS) { shelfSections },
|
||||
settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled },
|
||||
favouritesRepository.observeCategories(),
|
||||
) { sections, isTrackerEnabled, categories ->
|
||||
buildList(sections, isTrackerEnabled, categories)
|
||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
private var updateJob: Job? = null
|
||||
|
||||
fun setItemChecked(item: ShelfSettingsItemModel, isChecked: Boolean) {
|
||||
val prevJob = updateJob
|
||||
updateJob = launchJob(Dispatchers.Default) {
|
||||
prevJob?.join()
|
||||
when (item) {
|
||||
is ShelfSettingsItemModel.FavouriteCategory -> {
|
||||
favouritesRepository.updateCategory(item.id, isChecked)
|
||||
}
|
||||
|
||||
is ShelfSettingsItemModel.Section -> {
|
||||
val sections = settings.shelfSections
|
||||
settings.shelfSections = if (isChecked) {
|
||||
sections + item.section
|
||||
} else {
|
||||
if (sections.size > 1) {
|
||||
sections - item.section
|
||||
} else {
|
||||
return@launchJob
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reorderSections(oldPos: Int, newPos: Int): Boolean {
|
||||
val snapshot = content.value.toMutableList()
|
||||
snapshot.move(oldPos, newPos)
|
||||
settings.shelfSections = snapshot.sections()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun buildList(
|
||||
sections: List<ShelfSection>,
|
||||
isTrackerEnabled: Boolean,
|
||||
categories: List<FavouriteCategory>
|
||||
): List<ShelfSettingsItemModel> {
|
||||
val result = ArrayList<ShelfSettingsItemModel>()
|
||||
val sectionsList = ShelfSection.values().toMutableList()
|
||||
if (!isTrackerEnabled) {
|
||||
sectionsList.remove(ShelfSection.UPDATED)
|
||||
}
|
||||
for (section in sections) {
|
||||
if (sectionsList.remove(section)) {
|
||||
result.addSection(section, true, categories)
|
||||
}
|
||||
}
|
||||
for (section in sectionsList) {
|
||||
result.addSection(section, false, categories)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun MutableList<in ShelfSettingsItemModel>.addSection(
|
||||
section: ShelfSection,
|
||||
isEnabled: Boolean,
|
||||
favouriteCategories: List<FavouriteCategory>,
|
||||
) {
|
||||
add(ShelfSettingsItemModel.Section(section, isEnabled))
|
||||
if (isEnabled && section == ShelfSection.FAVORITES) {
|
||||
favouriteCategories.mapTo(this) {
|
||||
ShelfSettingsItemModel.FavouriteCategory(
|
||||
id = it.id,
|
||||
title = it.title,
|
||||
isChecked = it.isVisibleInLibrary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<ShelfSettingsItemModel>.sections(): List<ShelfSection> {
|
||||
return mapNotNull { (it as? ShelfSettingsItemModel.Section)?.takeIf { x -> x.isChecked }?.section }
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.config.size
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.google.android.material.slider.LabelFormatter
|
||||
import com.google.android.material.slider.Slider
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseBottomSheet
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
|
||||
import org.koitharu.kotatsu.databinding.SheetShelfSizeBinding
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ShelfSizeBottomSheet :
|
||||
BaseBottomSheet<SheetShelfSizeBinding>(),
|
||||
Slider.OnChangeListener,
|
||||
View.OnClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
private var labelFormatter: LabelFormatter? = null
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetShelfSizeBinding {
|
||||
return SheetShelfSizeBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetShelfSizeBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
labelFormatter = IntPercentLabelFormatter(binding.root.context)
|
||||
binding.sliderGrid.addOnChangeListener(this)
|
||||
binding.buttonSmall.setOnClickListener(this)
|
||||
binding.buttonLarge.setOnClickListener(this)
|
||||
|
||||
binding.sliderGrid.setValueRounded(settings.gridSize.toFloat())
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
labelFormatter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
settings.gridSize = value.toInt()
|
||||
requireViewBinding().textViewLabel.text = labelFormatter?.getFormattedValue(value)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val slider = requireViewBinding().sliderGrid
|
||||
when (v.id) {
|
||||
R.id.button_small -> slider.setValueRounded(slider.value - slider.stepSize)
|
||||
R.id.button_large -> slider.setValueRounded(slider.value + slider.stepSize)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "ShelfSizeBottomSheet"
|
||||
|
||||
fun show(fm: FragmentManager) = ShelfSizeBottomSheet().show(fm, TAG)
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
package org.koitharu.kotatsu.shelf.ui.model
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
|
||||
|
||||
sealed interface ShelfSectionModel : ListModel {
|
||||
|
||||
val items: List<MangaItemModel>
|
||||
|
||||
@get:StringRes
|
||||
val showAllButtonText: Int
|
||||
|
||||
val key: String
|
||||
fun getTitle(resources: Resources): CharSequence
|
||||
|
||||
override fun toString(): String
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is ShelfSectionModel && key == other.key
|
||||
}
|
||||
|
||||
override fun getChangePayload(previousState: ListModel): Any? {
|
||||
return if (previousState is ShelfSectionModel) {
|
||||
Unit
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
class History(
|
||||
override val items: List<MangaItemModel>,
|
||||
override val showAllButtonText: Int,
|
||||
) : ShelfSectionModel {
|
||||
|
||||
override val key = "history"
|
||||
|
||||
override fun getTitle(resources: Resources) = resources.getString(R.string.history)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as History
|
||||
|
||||
if (showAllButtonText != other.showAllButtonText) return false
|
||||
return items == other.items
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = items.hashCode()
|
||||
result = 31 * result + showAllButtonText.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String = key
|
||||
}
|
||||
|
||||
class Favourites(
|
||||
override val items: List<MangaItemModel>,
|
||||
val category: FavouriteCategory,
|
||||
override val showAllButtonText: Int,
|
||||
) : ShelfSectionModel {
|
||||
|
||||
override val key = "fav_${category.id}"
|
||||
|
||||
override fun getTitle(resources: Resources) = category.title
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Favourites
|
||||
|
||||
if (category != other.category) return false
|
||||
if (showAllButtonText != other.showAllButtonText) return false
|
||||
return items == other.items
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = items.hashCode()
|
||||
result = 31 * result + category.hashCode()
|
||||
result = 31 * result + showAllButtonText.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String = key
|
||||
}
|
||||
|
||||
class Updated(
|
||||
override val items: List<MangaItemModel>,
|
||||
override val showAllButtonText: Int,
|
||||
) : ShelfSectionModel {
|
||||
|
||||
override val key = "upd"
|
||||
|
||||
override fun getTitle(resources: Resources) = resources.getString(R.string.updated)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Updated
|
||||
|
||||
if (items != other.items) return false
|
||||
return showAllButtonText == other.showAllButtonText
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = items.hashCode()
|
||||
result = 31 * result + showAllButtonText
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String = key
|
||||
}
|
||||
|
||||
class Local(
|
||||
override val items: List<MangaItemModel>,
|
||||
override val showAllButtonText: Int,
|
||||
) : ShelfSectionModel {
|
||||
|
||||
override val key = "local"
|
||||
|
||||
override fun getTitle(resources: Resources) = resources.getString(R.string.local_storage)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Local
|
||||
|
||||
if (items != other.items) return false
|
||||
return showAllButtonText == other.showAllButtonText
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = items.hashCode()
|
||||
result = 31 * result + showAllButtonText
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String = key
|
||||
}
|
||||
|
||||
class Suggestions(
|
||||
override val items: List<MangaItemModel>,
|
||||
override val showAllButtonText: Int,
|
||||
) : ShelfSectionModel {
|
||||
|
||||
override val key = "suggestions"
|
||||
|
||||
override fun getTitle(resources: Resources) = resources.getString(R.string.suggestions)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Suggestions
|
||||
|
||||
if (items != other.items) return false
|
||||
return showAllButtonText == other.showAllButtonText
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = items.hashCode()
|
||||
result = 31 * result + showAllButtonText
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String = key
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodyLarge"
|
||||
app:layout_constraintBottom_toTopOf="@id/textView_subtitle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/imageView_handle"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
@@ -88,7 +88,7 @@
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/imageView_handle"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover3"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView_title"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
@@ -105,6 +105,7 @@
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="4dp"
|
||||
tools:ignore="RtlSymmetry">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.SegmentedBarView
|
||||
|
||||
@@ -74,13 +74,11 @@
|
||||
</style>
|
||||
|
||||
<style name="Widget.Kotatsu.Tabs" parent="@style/Widget.Material3.TabLayout">
|
||||
<item name="android:background">?colorOutline</item>
|
||||
<item name="tabBackground">@drawable/tabs_background</item>
|
||||
<item name="android:background">@drawable/tabs_background</item>
|
||||
<item name="tabGravity">center</item>
|
||||
<item name="tabInlineLabel">true</item>
|
||||
<item name="tabMinWidth">75dp</item>
|
||||
<item name="tabMode">scrollable</item>
|
||||
<item name="tabRippleColor">@color/ripple_toolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Kotatsu.SearchView" parent="@style/Widget.AppCompat.SearchView">
|
||||
|
||||
Reference in New Issue
Block a user