diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index c19ea05fc..59297d4e9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -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 get() = Collections.unmodifiableSet(remoteSources) - var shelfSections: List - get() { - val raw = prefs.getString(KEY_SHELF_SECTIONS, null) - val values = enumValues() - 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" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt index d387a46ff..9cac803fc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouriteCategoriesDao.kt @@ -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> + @Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key") + abstract fun observeAllForLibrary(): Flow> + @Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0") abstract fun observe(id: Long): Flow diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index a21923ed5..198972bcb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -67,6 +67,12 @@ class FavouritesRepository @Inject constructor( }.distinctUntilChanged() } + fun observeCategoriesForLibrary(): Flow> { + return db.favouriteCategoriesDao.observeAllForLibrary().mapItems { + it.toFavouriteCategory() + }.distinctUntilChanged() + } + fun observeCategoriesWithCovers(): Flow>> { return db.favouriteCategoriesDao.observeAll() .map { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerViewModel.kt index 00d9644e1..b4b07eea2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/container/FavouritesContainerViewModel.kt @@ -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()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/ShelfContentObserveUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/ShelfContentObserveUseCase.kt deleted file mode 100644 index 6bdaf89ac..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/ShelfContentObserveUseCase.kt +++ /dev/null @@ -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, -) { - - operator fun invoke(): Flow = 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> { - return combine( - 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>> { - 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, - ) = combine>, Map>>( - categories.map { cat -> - val category = cat.toFavouriteCategory() - db.favouritesDao.observeAll(category.id, category.order) - .map { category to it.toMangaList() } - }, - ) { array -> array.toMap() } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfContent.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfContent.kt deleted file mode 100644 index 39c88010f..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfContent.kt +++ /dev/null @@ -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, - val favourites: Map>, - val updated: List, - val local: List, - val suggestions: List, -) { - - 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 - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt deleted file mode 100644 index ecdd14b7d..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/domain/model/ShelfSection.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.koitharu.kotatsu.shelf.domain.model - -enum class ShelfSection { - - HISTORY, LOCAL, UPDATED, FAVORITES, SUGGESTIONS; -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt deleted file mode 100644 index 6a53b6a49..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfFragment.kt +++ /dev/null @@ -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(), - RecyclerViewOwner, - ShelfListEventListener { - - @Inject - lateinit var coil: ImageLoader - - @Inject - lateinit var settings: AppSettings - - private var nestedScrollStateHandle: NestedScrollStateHandle? = null - private val viewModel by viewModels() - private var adapter: ShelfAdapter? = null - private var selectionController: SectionedSelectionController? = 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) { - adapter?.items = list - } - - companion object { - - private const val KEY_NESTED_SCROLL = "nested_scroll" - - fun newInstance() = ShelfFragment() - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt deleted file mode 100644 index 842699dfc..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfMenuProvider.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt deleted file mode 100644 index 204dd7599..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfSelectionCallback.kt +++ /dev/null @@ -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 { - - private val context: Context - get() = recyclerView.context - - override fun onCreateActionMode( - controller: SectionedSelectionController, - mode: ActionMode, - menu: Menu, - ): Boolean { - mode.menuInflater.inflate(R.menu.mode_shelf, menu) - return true - } - - override fun onPrepareActionMode( - controller: SectionedSelectionController, - 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, - 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, count: Int) { - recyclerView.invalidateNestedItemDecorations() - } - - override fun onCreateItemDecoration( - controller: SectionedSelectionController, - section: ShelfSectionModel, - ): AbstractSelectionItemDecoration = MangaSelectionDecoration(context) - - private fun collectSelectedItemsMap( - controller: SectionedSelectionController, - ): Map> { - val snapshot = controller.peekCheckedIds() - if (snapshot.isEmpty()) { - return emptyMap() - } - return snapshot.mapValues { (_, ids) -> viewModel.getManga(ids) } - } - - private fun collectSelectedItems( - controller: SectionedSelectionController, - ): Set { - val snapshot = controller.peekCheckedIds() - if (snapshot.isEmpty()) { - return emptySet() - } - return viewModel.getManga(snapshot.values.flattenTo(HashSet())) - } - - private fun showDeletionConfirm(ids: Set, 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() - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt deleted file mode 100644 index 16ea86002..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt +++ /dev/null @@ -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() - val onDownloadStarted = MutableEventFlow() - - val content: StateFlow> = 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) { - 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) { - 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) { - 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): Set { - val snapshot = content.value - val result = ArraySet(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) { - launchJob(Dispatchers.Default) { - downloadScheduler.schedule(items) - onDownloadStarted.call(Unit) - } - } - - private suspend fun mapList( - content: ShelfContent, - isTrackerEnabled: Boolean, - isSuggestionsEnabled: Boolean, - sections: List, - isNetworkAvailable: Boolean, - ): List { - val result = ArrayList(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, - list: List, - ) { - 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, - updated: List, - ) { - 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, - local: List, - ) { - if (local.isEmpty()) { - return - } - destination += ShelfSectionModel.Local( - items = local.toUi(ListMode.GRID, listExtraProvider), - showAllButtonText = R.string.show_all, - ) - } - - private suspend fun mapSuggestions( - destination: MutableList, - suggestions: List, - ) { - if (suggestions.isEmpty()) { - return - } - destination += ShelfSectionModel.Suggestions( - items = suggestions.toUi(ListMode.GRID, listExtraProvider), - showAllButtonText = R.string.show_all, - ) - } - - private suspend fun mapFavourites( - destination: MutableList, - favourites: Map>, - ) { - 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, - ) - } - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt deleted file mode 100644 index 322210205..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ScrollKeepObserver.kt +++ /dev/null @@ -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) - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt deleted file mode 100644 index e27568d14..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfAdapter.kt +++ /dev/null @@ -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, - nestedScrollStateHandle: NestedScrollStateHandle, -) : BaseListAdapter(), 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) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt deleted file mode 100644 index 544920a8d..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfGroupAD.kt +++ /dev/null @@ -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, - listener: ShelfListEventListener, - nestedScrollStateHandle: NestedScrollStateHandle, -) = adapterDelegateViewBinding( - { layoutInflater, parent -> ItemListGroupBinding.inflate(layoutInflater, parent, false) }, -) { - val listenerAdapter = object : OnListItemClickListener, 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) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfListEventListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfListEventListener.kt deleted file mode 100644 index af34933d2..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/adapter/ShelfListEventListener.kt +++ /dev/null @@ -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) -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt deleted file mode 100644 index e8c8f346f..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsActivity.kt +++ /dev/null @@ -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(), - View.OnClickListener, ShelfSettingsListener { - - private val viewModel by viewModels() - 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 { - 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) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt deleted file mode 100644 index 8a9f49ea4..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapter.kt +++ /dev/null @@ -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() { - - init { - delegatesManager.addDelegate(shelfCategoryAD(listener)) - .addDelegate(shelfSectionAD(listener)) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt deleted file mode 100644 index 4f0a6ef17..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsAdapterDelegates.kt +++ /dev/null @@ -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( - { 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( - { 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 - } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt deleted file mode 100644 index 87606f97c..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsItemModel.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt deleted file mode 100644 index b8fa749a1..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsListener.kt +++ /dev/null @@ -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) -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt deleted file mode 100644 index 4df55a1d1..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/ShelfSettingsViewModel.kt +++ /dev/null @@ -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, - isTrackerEnabled: Boolean, - categories: List - ): List { - val result = ArrayList() - 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.addSection( - section: ShelfSection, - isEnabled: Boolean, - favouriteCategories: List, - ) { - 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.sections(): List { - return mapNotNull { (it as? ShelfSettingsItemModel.Section)?.takeIf { x -> x.isChecked }?.section } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt deleted file mode 100644 index 26f74d59b..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/config/size/ShelfSizeBottomSheet.kt +++ /dev/null @@ -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(), - 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) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt deleted file mode 100644 index ff0cc7160..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/shelf/ui/model/ShelfSectionModel.kt +++ /dev/null @@ -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 - - @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, - 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, - 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, - 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, - 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, - 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 - } -} diff --git a/app/src/main/res/layout/item_category.xml b/app/src/main/res/layout/item_category.xml index 3f1e61d54..ae3269968 100644 --- a/app/src/main/res/layout/item_category.xml +++ b/app/src/main/res/layout/item_category.xml @@ -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" /> diff --git a/app/src/main/res/layout/preference_memory_usage.xml b/app/src/main/res/layout/preference_memory_usage.xml index e054367da..e6d920fde 100644 --- a/app/src/main/res/layout/preference_memory_usage.xml +++ b/app/src/main/res/layout/preference_memory_usage.xml @@ -6,6 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" + android:paddingBottom="4dp" tools:ignore="RtlSymmetry">