Improve library list

This commit is contained in:
Koitharu
2022-07-04 16:39:26 +03:00
parent e4a2897731
commit e0d93b0630
9 changed files with 175 additions and 19 deletions

View File

@@ -57,14 +57,19 @@
<activity <activity
android:name="org.koitharu.kotatsu.search.ui.MangaListActivity" android:name="org.koitharu.kotatsu.search.ui.MangaListActivity"
android:label="@string/search_manga" /> android:label="@string/search_manga" />
<activity
android:name=".history.ui.HistoryActivity"
android:label="@string/history" />
<activity <activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity" android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:exported="true" android:exported="true"
android:label="@string/settings"> android:label="@string/settings">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="kotatsu" /> <data android:scheme="kotatsu" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@@ -0,0 +1,49 @@
package org.koitharu.kotatsu.history.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
class HistoryActivity : BaseActivity<ActivityContainerBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
val fragment = HistoryListFragment.newInstance()
replace(R.id.container, fragment)
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
with(binding.toolbar) {
updatePadding(
left = insets.left,
right = insets.right
)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
binding.container.updatePadding(
bottom = insets.bottom
)
}
companion object {
fun newIntent(context: Context) = Intent(context, HistoryActivity::class.java)
}
}

View File

@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.databinding.FragmentLibraryBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.history.ui.HistoryListFragment import org.koitharu.kotatsu.history.ui.HistoryActivity
import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter
import org.koitharu.kotatsu.library.ui.model.LibraryGroupModel import org.koitharu.kotatsu.library.ui.model.LibraryGroupModel
import org.koitharu.kotatsu.list.ui.ItemSizeResolver import org.koitharu.kotatsu.list.ui.ItemSizeResolver
@@ -46,7 +46,7 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), MangaListListene
val sizeResolver = ItemSizeResolver(resources, get()) val sizeResolver = ItemSizeResolver(resources, get())
val itemCLickListener = object : OnListItemClickListener<LibraryGroupModel> { val itemCLickListener = object : OnListItemClickListener<LibraryGroupModel> {
override fun onItemClick(item: LibraryGroupModel, view: View) { override fun onItemClick(item: LibraryGroupModel, view: View) {
onGroupClick(item, view)
} }
} }
selectionDecoration = MangaSelectionDecoration(view.context) selectionDecoration = MangaSelectionDecoration(view.context)
@@ -153,6 +153,14 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), MangaListListene
actionMode = null actionMode = null
} }
private fun onGroupClick(item: LibraryGroupModel, view: View) {
val intent = when (item) {
is LibraryGroupModel.History -> HistoryActivity.newIntent(view.context)
is LibraryGroupModel.Favourites -> TODO()
}
startActivity(intent)
}
private fun collectSelectedItems(): Set<Manga> { private fun collectSelectedItems(): Set<Manga> {
val ids = selectionDecoration?.checkedItemsIds val ids = selectionDecoration?.checkedItemsIds
if (ids.isNullOrEmpty()) { if (ids.isNullOrEmpty()) {

View File

@@ -5,11 +5,13 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.history.domain.MangaWithHistory import org.koitharu.kotatsu.history.domain.MangaWithHistory
@@ -21,6 +23,10 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff
import java.util.*
private const val HISTORY_MAX_SEGMENTS = 2
class LibraryViewModel( class LibraryViewModel(
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
@@ -57,22 +63,54 @@ class LibraryViewModel(
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(favourites.keys.size + 1) val result = ArrayList<ListModel>(favourites.keys.size + 1)
if (history.isNotEmpty()) { if (history.isNotEmpty()) {
result += LibraryGroupModel.History(mapHistory(history), null) result += mapHistory(history)
} }
for ((category, list) in favourites) { for ((category, list) in favourites) {
result += LibraryGroupModel.Favourites(list.toUi(ListMode.GRID, this), category) result += LibraryGroupModel.Favourites(list.toUi(ListMode.GRID, this), category, R.string.show_all)
} }
return result return result
} }
private suspend fun mapHistory(list: List<MangaWithHistory>): List<MangaItemModel> { private suspend fun mapHistory(list: List<MangaWithHistory>): List<LibraryGroupModel.History> {
val showPercent = settings.isReadingIndicatorsEnabled val showPercent = settings.isReadingIndicatorsEnabled
val result = ArrayList<MangaItemModel>(list.size) val groups = ArrayList<DateTimeAgo>()
val map = HashMap<DateTimeAgo, ArrayList<MangaItemModel>>()
for ((manga, history) in list) { for ((manga, history) in list) {
val date = timeAgo(history.updatedAt)
val counter = trackingRepository.getNewChaptersCount(manga.id) val counter = trackingRepository.getNewChaptersCount(manga.id)
val percent = if (showPercent) history.percent else PROGRESS_NONE val percent = if (showPercent) history.percent else PROGRESS_NONE
result += manga.toGridModel(counter, percent) if (groups.lastOrNull() != date) {
groups.add(date)
}
map.getOrPut(date) { ArrayList() }.add(manga.toGridModel(counter, percent))
}
val result = ArrayList<LibraryGroupModel.History>(HISTORY_MAX_SEGMENTS)
repeat(minOf(HISTORY_MAX_SEGMENTS - 1, groups.size - 1)) { i ->
val key = groups[i]
val values = map.remove(key)
if (!values.isNullOrEmpty()) {
result.add(LibraryGroupModel.History(values, key, 0))
}
}
val values = map.values.flatten()
if (values.isNotEmpty()) {
val key = if (result.isEmpty()) {
map.keys.singleOrNull()?.takeUnless { it == DateTimeAgo.LongAgo }
} else {
map.keys.singleOrNull() ?: DateTimeAgo.LongAgo
}
result.add(LibraryGroupModel.History(values, key, R.string.show_all))
} }
return result return result
} }
private fun timeAgo(date: Date): DateTimeAgo {
val diffDays = -date.daysDiff(System.currentTimeMillis())
return when {
diffDays < 1 -> DateTimeAgo.Today
diffDays == 1 -> DateTimeAgo.Yesterday
diffDays <= 3 -> DateTimeAgo.DaysAgo(diffDays)
else -> DateTimeAgo.LongAgo
}
}
} }

View File

@@ -56,5 +56,12 @@ class LibraryAdapter(
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem) return Intrinsics.areEqual(oldItem, newItem)
} }
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when {
oldItem is LibraryGroupModel && newItem is LibraryGroupModel -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}
} }
} }

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.library.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.base.ui.list.AdapterDelegateClickListenerAdapter
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
fun libraryGroupAD( fun libraryGroupAD(
sharedPool: RecyclerView.RecycledViewPool, sharedPool: RecyclerView.RecycledViewPool,
@@ -30,7 +31,8 @@ fun libraryGroupAD(
) { ) {
binding.recyclerView.setRecycledViewPool(sharedPool) binding.recyclerView.setRecycledViewPool(sharedPool)
val adapter = ListDelegationAdapter( val adapter = AsyncListDifferDelegationAdapter<ListModel>(
MangaItemDiffCallback(),
mangaGridItemAD(coil, lifecycleOwner, listener, sizeResolver) mangaGridItemAD(coil, lifecycleOwner, listener, sizeResolver)
) )
binding.recyclerView.addItemDecoration(selectionDecoration) binding.recyclerView.addItemDecoration(selectionDecoration)
@@ -38,11 +40,11 @@ fun libraryGroupAD(
val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing) val spacing = context.resources.getDimensionPixelOffset(R.dimen.grid_spacing)
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing)) binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener) val eventListener = AdapterDelegateClickListenerAdapter(this, itemClickListener)
itemView.setOnClickListener(eventListener) binding.buttonMore.setOnClickListener(eventListener)
bind { bind {
binding.textViewTitle.text = item.getTitle(context.resources) binding.textViewTitle.text = item.getTitle(context.resources)
binding.buttonMore.setTextAndVisible(item.showAllButtonText)
adapter.items = item.items adapter.items = item.items
adapter.notifyDataSetChanged()
} }
} }

View File

@@ -0,0 +1,30 @@
package org.koitharu.kotatsu.library.ui.adapter
import androidx.recyclerview.widget.DiffUtil
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import kotlin.jvm.internal.Intrinsics
class MangaItemDiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
oldItem as MangaItemModel
newItem as MangaItemModel
return oldItem.javaClass == newItem.javaClass && oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
oldItem as MangaItemModel
newItem as MangaItemModel
return when {
oldItem.progress != newItem.progress -> MangaListAdapter.PAYLOAD_PROGRESS
oldItem.counter != newItem.counter -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.library.ui.model package org.koitharu.kotatsu.library.ui.model
import android.content.res.Resources import android.content.res.Resources
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.core.ui.DateTimeAgo
@@ -8,7 +9,8 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel import org.koitharu.kotatsu.list.ui.model.MangaItemModel
sealed class LibraryGroupModel( sealed class LibraryGroupModel(
val items: List<MangaItemModel> val items: List<MangaItemModel>,
@StringRes val showAllButtonText: Int,
) : ListModel { ) : ListModel {
abstract val key: Any abstract val key: Any
@@ -17,7 +19,8 @@ sealed class LibraryGroupModel(
class History( class History(
items: List<MangaItemModel>, items: List<MangaItemModel>,
val timeAgo: DateTimeAgo?, val timeAgo: DateTimeAgo?,
) : LibraryGroupModel(items) { showAllButtonText: Int,
) : LibraryGroupModel(items, showAllButtonText) {
override val key: Any override val key: Any
get() = timeAgo?.javaClass ?: this::class.java get() = timeAgo?.javaClass ?: this::class.java
@@ -32,8 +35,9 @@ sealed class LibraryGroupModel(
other as History other as History
if (items != other.items) return false
if (timeAgo != other.timeAgo) return false if (timeAgo != other.timeAgo) return false
if (showAllButtonText != other.showAllButtonText) return false
if (items != other.items) return false
return true return true
} }
@@ -41,6 +45,7 @@ sealed class LibraryGroupModel(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = items.hashCode() var result = items.hashCode()
result = 31 * result + (timeAgo?.hashCode() ?: 0) result = 31 * result + (timeAgo?.hashCode() ?: 0)
result = 31 * result + showAllButtonText.hashCode()
return result return result
} }
} }
@@ -48,7 +53,8 @@ sealed class LibraryGroupModel(
class Favourites( class Favourites(
items: List<MangaItemModel>, items: List<MangaItemModel>,
val category: FavouriteCategory, val category: FavouriteCategory,
) : LibraryGroupModel(items) { showAllButtonText: Int,
) : LibraryGroupModel(items, showAllButtonText) {
override val key: Any override val key: Any
get() = category.id get() = category.id
@@ -63,8 +69,9 @@ sealed class LibraryGroupModel(
other as Favourites other as Favourites
if (items != other.items) return false
if (category != other.category) return false if (category != other.category) return false
if (showAllButtonText != other.showAllButtonText) return false
if (items != other.items) return false
return true return true
} }
@@ -72,6 +79,7 @@ sealed class LibraryGroupModel(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = items.hashCode() var result = items.hashCode()
result = 31 * result + category.hashCode() result = 31 * result + category.hashCode()
result = 31 * result + showAllButtonText.hashCode()
return result return result
} }
} }

View File

@@ -109,13 +109,15 @@ class MainActivity :
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState) super.onRestoreInstanceState(savedInstanceState)
if (isSearchOpened()) { val isSearchOpened = isSearchOpened()
if (isSearchOpened) {
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> { binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = SCROLL_FLAG_NO_SCROLL scrollFlags = SCROLL_FLAG_NO_SCROLL
} }
binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant)) binding.appbar.setBackgroundColor(getThemeColor(materialR.attr.colorSurfaceVariant))
binding.appbar.updatePadding(left = 0, right = 0) binding.appbar.updatePadding(left = 0, right = 0)
} }
adjustFabVisibility(isSearchOpened = isSearchOpened)
} }
override fun onBackPressed() { override fun onBackPressed() {
@@ -127,7 +129,7 @@ class MainActivity :
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
runOnCommit { onSearchClosed() } runOnCommit { onSearchClosed() }
} }
else -> binding.searchView.requestFocus() else -> super.onBackPressed()
} }
} }
@@ -218,11 +220,13 @@ class MainActivity :
override fun onSupportActionModeStarted(mode: ActionMode) { override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode) super.onSupportActionModeStarted(mode)
adjustFabVisibility()
showBottomNav(false) showBottomNav(false)
} }
override fun onSupportActionModeFinished(mode: ActionMode) { override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode) super.onSupportActionModeFinished(mode)
adjustFabVisibility()
showBottomNav(true) showBottomNav(true)
} }
@@ -330,7 +334,12 @@ class MainActivity :
isSearchOpened: Boolean = isSearchOpened(), isSearchOpened: Boolean = isSearchOpened(),
) { ) {
val fab = binding.fab val fab = binding.fab
if (isResumeEnabled && !isSearchOpened && topFragment is LibraryFragment) { if (
isResumeEnabled &&
!actionModeDelegate.isActionModeStarted &&
!isSearchOpened &&
topFragment is LibraryFragment
) {
if (!fab.isVisible) { if (!fab.isVisible) {
fab.show() fab.show()
} }