Update chapters list ui, add bookmark indicator

pull/377/head
Koitharu 3 years ago
parent 84f41810c5
commit 2e3be00e26
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -1,6 +1,10 @@
package org.koitharu.kotatsu.bookmarks.data package org.koitharu.kotatsu.bookmarks.data
import androidx.room.* import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaWithTags import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@ -18,7 +22,7 @@ abstract class BookmarksDao {
@Transaction @Transaction
@Query( @Query(
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at" "SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at",
) )
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>> abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
@ -29,5 +33,8 @@ abstract class BookmarksDao {
abstract suspend fun delete(entity: BookmarkEntity) abstract suspend fun delete(entity: BookmarkEntity)
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId") @Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun delete(mangaId: Long, pageId: Long) abstract suspend fun delete(mangaId: Long, pageId: Long): Int
}
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
abstract suspend fun delete(mangaId: Long, chapterId: Long, page: Int): Int
}

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.bookmarks.domain package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.util.* import java.util.Date
class Bookmark( class Bookmark(
val manga: Manga, val manga: Manga,
@ -27,9 +27,7 @@ class Bookmark(
if (scroll != other.scroll) return false if (scroll != other.scroll) return false
if (imageUrl != other.imageUrl) return false if (imageUrl != other.imageUrl) return false
if (createdAt != other.createdAt) return false if (createdAt != other.createdAt) return false
if (percent != other.percent) return false return percent == other.percent
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -43,4 +41,4 @@ class Bookmark(
result = 31 * result + percent.hashCode() result = 31 * result + percent.hashCode()
return result return result
} }
} }

@ -52,8 +52,14 @@ class BookmarksRepository @Inject constructor(
} }
} }
suspend fun removeBookmark(mangaId: Long, pageId: Long) { suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
db.bookmarksDao.delete(mangaId, pageId) check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) {
"Bookmark not found"
}
}
suspend fun removeBookmark(bookmark: Bookmark) {
removeBookmark(bookmark.manga.id, bookmark.chapterId, bookmark.page)
} }
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle { suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {

@ -19,7 +19,9 @@ class BookmarksAdapter(
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() { private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
return oldItem.manga.id == newItem.manga.id && oldItem.pageId == newItem.pageId return oldItem.manga.id == newItem.manga.id &&
oldItem.chapterId == newItem.chapterId &&
oldItem.page == newItem.page
} }
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean { override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {

@ -0,0 +1,23 @@
package org.koitharu.kotatsu.core.util.ext
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
fun BottomSheetBehavior<*>.doOnExpansionsChanged(callback: (isExpanded: Boolean) -> Unit) {
var isExpended = state == BottomSheetBehavior.STATE_EXPANDED
callback(isExpended)
addBottomSheetCallback(
object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
val expanded = newState == BottomSheetBehavior.STATE_EXPANDED
if (expanded != isExpended) {
isExpended = expanded
callback(expanded)
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
},
)
}

@ -6,18 +6,25 @@ import androidx.activity.OnBackPressedCallback
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.koitharu.kotatsu.core.ui.util.ActionModeListener import org.koitharu.kotatsu.core.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
class ChaptersBottomSheetMediator( class ChaptersBottomSheetMediator(
bottomSheet: View, private val behavior: BottomSheetBehavior<*>,
) : OnBackPressedCallback(false), ) : OnBackPressedCallback(false),
ActionModeListener, ActionModeListener,
BottomSheetHeaderBar.OnExpansionChangeListener,
OnLayoutChangeListener { OnLayoutChangeListener {
private val behavior = BottomSheetBehavior.from(bottomSheet)
private var lockCounter = 0 private var lockCounter = 0
init {
behavior.doOnExpansionsChanged { isExpanded ->
isEnabled = isExpanded
if (!isExpanded) {
unlock()
}
}
}
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED behavior.state = BottomSheetBehavior.STATE_COLLAPSED
} }
@ -30,13 +37,6 @@ class ChaptersBottomSheetMediator(
unlock() unlock()
} }
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
isEnabled = isExpanded
if (!isExpanded) {
unlock()
}
}
override fun onLayoutChange( override fun onLayoutChange(
v: View?, v: View?,
left: Int, left: Int,
@ -61,6 +61,9 @@ class ChaptersBottomSheetMediator(
fun unlock() { fun unlock() {
lockCounter-- lockCounter--
if (lockCounter < 0) {
lockCounter = 0
}
behavior.isDraggable = lockCounter <= 0 behavior.isDraggable = lockCounter <= 0
} }
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
@ -56,6 +57,9 @@ class ChaptersFragment :
checkNotNull(selectionController).attachToRecyclerView(this) checkNotNull(selectionController).attachToRecyclerView(this)
setHasFixedSize(true) setHasFixedSize(true)
adapter = chaptersAdapter adapter = chaptersAdapter
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scrollIndicators = if (resources.getBoolean(R.bool.is_tablet)) 0 else View.SCROLL_INDICATOR_TOP
}
} }
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged) viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)

@ -1,9 +1,11 @@
package org.koitharu.kotatsu.details.ui package org.koitharu.kotatsu.details.ui
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet
fun mapChapters( fun mapChapters(
remoteManga: Manga?, remoteManga: Manga?,
@ -11,12 +13,14 @@ fun mapChapters(
history: MangaHistory?, history: MangaHistory?,
newCount: Int, newCount: Int,
branch: String?, branch: String?,
bookmarks: List<Bookmark>,
): List<ChapterListItem> { ): List<ChapterListItem> {
val remoteChapters = remoteManga?.getChapters(branch).orEmpty() val remoteChapters = remoteManga?.getChapters(branch).orEmpty()
val localChapters = localManga?.getChapters(branch).orEmpty() val localChapters = localManga?.getChapters(branch).orEmpty()
if (remoteChapters.isEmpty() && localChapters.isEmpty()) { if (remoteChapters.isEmpty() && localChapters.isEmpty()) {
return emptyList() return emptyList()
} }
val bookmarked = bookmarks.mapToSet { it.chapterId }
val currentId = history?.chapterId ?: 0L val currentId = history?.chapterId ?: 0L
val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount val newFrom = if (newCount == 0 || remoteChapters.isEmpty()) Int.MAX_VALUE else remoteChapters.size - newCount
val chaptersSize = maxOf(remoteChapters.size, localChapters.size) val chaptersSize = maxOf(remoteChapters.size, localChapters.size)
@ -41,6 +45,7 @@ fun mapChapters(
isUnread = isUnread, isUnread = isUnread,
isNew = isUnread && result.size >= newFrom, isNew = isUnread && result.size >= newFrom,
isDownloaded = local != null, isDownloaded = local != null,
isBookmarked = chapter.id in bookmarked,
) )
} }
if (!localMap.isNullOrEmpty()) { if (!localMap.isNullOrEmpty()) {
@ -53,6 +58,7 @@ fun mapChapters(
isUnread = isUnread, isUnread = isUnread,
isNew = false, isNew = false,
isDownloaded = remoteManga != null, isDownloaded = remoteManga != null,
isBookmarked = chapter.id in bookmarked,
) )
} }
} }

@ -3,14 +3,18 @@ package org.koitharu.kotatsu.details.ui
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import java.lang.ref.WeakReference
class ChaptersMenuProvider( class ChaptersMenuProvider(
private val viewModel: DetailsViewModel, private val viewModel: DetailsViewModel,
private val bottomSheetMediator: ChaptersBottomSheetMediator?, private val bottomSheetMediator: ChaptersBottomSheetMediator?,
) : MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener { ) : OnBackPressedCallback(false), MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
private var searchItemRef: WeakReference<MenuItem>? = null
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_chapters, menu) menuInflater.inflate(R.menu.opt_chapters, menu)
@ -20,6 +24,7 @@ class ChaptersMenuProvider(
searchView.setOnQueryTextListener(this) searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false) searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title searchView.queryHint = searchMenuItem.title
searchItemRef = WeakReference(searchMenuItem)
} }
override fun onPrepareMenu(menu: Menu) { override fun onPrepareMenu(menu: Menu) {
@ -32,15 +37,22 @@ class ChaptersMenuProvider(
viewModel.setChaptersReversed(!menuItem.isChecked) viewModel.setChaptersReversed(!menuItem.isChecked)
true true
} }
else -> false else -> false
} }
override fun handleOnBackPressed() {
searchItemRef?.get()?.collapseActionView()
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
bottomSheetMediator?.lock() bottomSheetMediator?.lock()
isEnabled = true
return true return true
} }
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
isEnabled = false
(item.actionView as? SearchView)?.setQuery("", false) (item.actionView as? SearchView)?.setQuery("", false)
viewModel.performChapterSearch(null) viewModel.performChapterSearch(null)
bottomSheetMediator?.unlock() bottomSheetMediator?.unlock()

@ -4,6 +4,7 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.transition.AutoTransition
import android.transition.Slide import android.transition.Slide
import android.transition.TransitionManager import android.transition.TransitionManager
import android.view.Gravity import android.view.Gravity
@ -18,6 +19,7 @@ import androidx.core.graphics.Insets
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -31,8 +33,11 @@ import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.util.ViewBadge import org.koitharu.kotatsu.core.util.ViewBadge
import org.koitharu.kotatsu.core.util.ext.doOnExpansionsChanged
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
@ -49,19 +54,16 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint @AndroidEntryPoint
class DetailsActivity : class DetailsActivity :
BaseActivity<ActivityDetailsBinding>(), BaseActivity<ActivityDetailsBinding>(),
View.OnClickListener, View.OnClickListener,
BottomSheetHeaderBar.OnExpansionChangeListener,
NoModalBottomSheetOwner, NoModalBottomSheetOwner,
View.OnLongClickListener, View.OnLongClickListener,
PopupMenu.OnMenuItemClickListener { PopupMenu.OnMenuItemClickListener {
override val bsHeader: BottomSheetHeaderBar?
get() = viewBinding.headerChapters
@Inject @Inject
lateinit var shortcutsUpdater: ShortcutsUpdater lateinit var shortcutsUpdater: ShortcutsUpdater
@ -82,16 +84,22 @@ class DetailsActivity :
viewBinding.buttonDropdown.setOnClickListener(this) viewBinding.buttonDropdown.setOnClickListener(this)
viewBadge = ViewBadge(viewBinding.buttonRead, this) viewBadge = ViewBadge(viewBinding.buttonRead, this)
chaptersMenuProvider = if (viewBinding.layoutBottom != null) { if (viewBinding.layoutBottom != null) {
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(viewBinding.layoutBottom)) val behavior = BottomSheetBehavior.from(checkNotNull(viewBinding.layoutBottom))
val bsMediator = ChaptersBottomSheetMediator(behavior)
actionModeDelegate.addListener(bsMediator) actionModeDelegate.addListener(bsMediator)
checkNotNull(viewBinding.headerChapters).addOnExpansionChangeListener(bsMediator) checkNotNull(viewBinding.layoutBsHeader).addOnLayoutChangeListener(bsMediator)
checkNotNull(viewBinding.headerChapters).addOnLayoutChangeListener(bsMediator)
onBackPressedDispatcher.addCallback(bsMediator) onBackPressedDispatcher.addCallback(bsMediator)
ChaptersMenuProvider(viewModel, bsMediator) chaptersMenuProvider = ChaptersMenuProvider(viewModel, bsMediator)
behavior.doOnExpansionsChanged(::onChaptersSheetStateChanged)
viewBinding.toolbarChapters?.setNavigationOnClickListener {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
} else { } else {
ChaptersMenuProvider(viewModel, null) chaptersMenuProvider = ChaptersMenuProvider(viewModel, null)
addMenuProvider(chaptersMenuProvider)
} }
onBackPressedDispatcher.addCallback(chaptersMenuProvider)
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated) viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged) viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
@ -114,11 +122,11 @@ class DetailsActivity :
} }
viewModel.historyInfo.observe(this, ::onHistoryChanged) viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranch.observe(this) { viewModel.selectedBranch.observe(this) {
viewBinding.headerChapters?.subtitle = it viewBinding.toolbarChapters?.subtitle = it
viewBinding.textViewSubtitle?.textAndVisible = it viewBinding.textViewSubtitle?.textAndVisible = it
} }
viewModel.isChaptersReversed.observe(this) { viewModel.isChaptersReversed.observe(this) {
viewBinding.headerChapters?.invalidateMenu() ?: invalidateOptionsMenu() viewBinding.toolbarChapters?.invalidateMenu() ?: invalidateOptionsMenu()
} }
viewModel.favouriteCategories.observe(this) { viewModel.favouriteCategories.observe(this) {
invalidateOptionsMenu() invalidateOptionsMenu()
@ -137,7 +145,10 @@ class DetailsActivity :
shortcutsUpdater = shortcutsUpdater, shortcutsUpdater = shortcutsUpdater,
), ),
) )
viewBinding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider) }
override fun getBottomSheetCollapsedHeight(): Int {
return viewBinding.layoutBsHeader?.measureHeight() ?: 0
} }
override fun onClick(v: View) { override fun onClick(v: View) {
@ -184,11 +195,19 @@ class DetailsActivity :
} }
} }
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) { private fun onChaptersSheetStateChanged(isExpanded: Boolean) {
val toolbar = viewBinding.toolbarChapters ?: return
if (isAnimationsEnabled) {
val transition = AutoTransition()
transition.duration = getAnimationDuration(R.integer.config_tinyAnimTime)
TransitionManager.beginDelayedTransition(toolbar, transition)
}
if (isExpanded) { if (isExpanded) {
headerBar.addMenuProvider(chaptersMenuProvider) toolbar.addMenuProvider(chaptersMenuProvider)
toolbar.setNavigationIcon(materialR.drawable.abc_ic_clear_material)
} else { } else {
headerBar.removeMenuProvider(chaptersMenuProvider) toolbar.removeMenuProvider(chaptersMenuProvider)
toolbar.navigationIcon = null
} }
viewBinding.buttonRead.isGone = isExpanded viewBinding.buttonRead.isGone = isExpanded
} }
@ -237,7 +256,7 @@ class DetailsActivity :
info.totalChapters == 0 -> getString(R.string.no_chapters) info.totalChapters == 0 -> getString(R.string.no_chapters)
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters) else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
} }
viewBinding.headerChapters?.title = text viewBinding.toolbarChapters?.title = text
viewBinding.textViewTitle?.text = text viewBinding.textViewTitle?.text = text
} }
@ -282,8 +301,6 @@ class DetailsActivity :
} }
} }
private fun isTabletLayout() = viewBinding.layoutBottom == null
private fun showBottomSheet(isVisible: Boolean) { private fun showBottomSheet(isVisible: Boolean) {
val view = viewBinding.layoutBottom ?: return val view = viewBinding.layoutBottom ?: return
if (view.isVisible == isVisible) return if (view.isVisible == isVisible) return
@ -297,7 +314,7 @@ class DetailsActivity :
private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar { private fun makeSnackbar(text: CharSequence, @BaseTransientBottomBar.Duration duration: Int): Snackbar {
val sb = Snackbar.make(viewBinding.containerDetails, text, duration) val sb = Snackbar.make(viewBinding.containerDetails, text, duration)
if (viewBinding.layoutBottom?.isVisible == true) { if (viewBinding.layoutBottom?.isVisible == true) {
sb.anchorView = viewBinding.headerChapters sb.anchorView = viewBinding.toolbarChapters
} }
return sb return sb
} }

@ -34,7 +34,6 @@ import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.drawableTop import org.koitharu.kotatsu.core.util.ext.drawableTop
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.measureHeight
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.resolveDp import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
@ -271,7 +270,7 @@ class DetailsFragment :
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
requireViewBinding().root.updatePadding( requireViewBinding().root.updatePadding(
bottom = ( bottom = (
(activity as? NoModalBottomSheetOwner)?.bsHeader?.measureHeight() (activity as? NoModalBottomSheetOwner)?.getBottomSheetCollapsedHeight()
?.plus(insets.bottom)?.plus(resources.resolveDp(16)) ?.plus(insets.bottom)?.plus(resources.resolveDp(16))
) )
?: insets.bottom, ?: insets.bottom,

@ -171,8 +171,9 @@ class DetailsViewModel @Inject constructor(
history, history,
selectedBranch, selectedBranch,
newChaptersCount, newChaptersCount,
) { manga, history, branch, news -> bookmarks,
mapChapters(manga?.remote, manga?.local, history, news, branch) ) { manga, history, branch, news, bookmarks ->
mapChapters(manga?.remote, manga?.local, history, news, branch, bookmarks)
}, },
isChaptersReversed, isChaptersReversed,
chaptersQuery, chaptersQuery,
@ -209,8 +210,8 @@ class DetailsViewModel @Inject constructor(
} }
fun removeBookmark(bookmark: Bookmark) { fun removeBookmark(bookmark: Bookmark) {
launchJob { launchJob(Dispatchers.Default) {
bookmarksRepository.removeBookmark(bookmark.manga.id, bookmark.pageId) bookmarksRepository.removeBookmark(bookmark)
onShowToast.call(R.string.bookmark_removed) onShowToast.call(R.string.bookmark_removed)
} }
} }

@ -1,10 +1,12 @@
package org.koitharu.kotatsu.details.ui.adapter package org.koitharu.kotatsu.details.ui.adapter
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
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.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemChapterBinding import org.koitharu.kotatsu.databinding.ItemChapterBinding
@ -43,7 +45,13 @@ fun chapterListItemAD(
binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary)) binding.textViewNumber.setTextColor(context.getThemeColor(android.R.attr.textColorTertiary))
} }
} }
binding.imageViewBookmarked.isVisible = item.isBookmarked
binding.imageViewDownloaded.isVisible = item.isDownloaded binding.imageViewDownloaded.isVisible = item.isDownloaded
binding.imageViewNew.isVisible = item.isNew // binding.imageViewNew.isVisible = item.isNew
binding.textViewTitle.drawableStart = if (item.isNew) {
ContextCompat.getDrawable(context, R.drawable.ic_new)
} else {
null
}
} }
} }

@ -31,6 +31,9 @@ class ChapterListItem(
val isDownloaded: Boolean val isDownloaded: Boolean
get() = hasFlag(FLAG_DOWNLOADED) get() = hasFlag(FLAG_DOWNLOADED)
val isBookmarked: Boolean
get() = hasFlag(FLAG_BOOKMARKED)
val isNew: Boolean val isNew: Boolean
get() = hasFlag(FLAG_NEW) get() = hasFlag(FLAG_NEW)
@ -70,6 +73,7 @@ class ChapterListItem(
const val FLAG_UNREAD = 2 const val FLAG_UNREAD = 2
const val FLAG_CURRENT = 4 const val FLAG_CURRENT = 4
const val FLAG_NEW = 8 const val FLAG_NEW = 8
const val FLAG_BOOKMARKED = 16
const val FLAG_DOWNLOADED = 32 const val FLAG_DOWNLOADED = 32
} }
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.details.ui.model package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_BOOKMARKED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_NEW
@ -11,11 +12,13 @@ fun MangaChapter.toListItem(
isUnread: Boolean, isUnread: Boolean,
isNew: Boolean, isNew: Boolean,
isDownloaded: Boolean, isDownloaded: Boolean,
isBookmarked: Boolean,
): ChapterListItem { ): ChapterListItem {
var flags = 0 var flags = 0
if (isCurrent) flags = flags or FLAG_CURRENT if (isCurrent) flags = flags or FLAG_CURRENT
if (isUnread) flags = flags or FLAG_UNREAD if (isUnread) flags = flags or FLAG_UNREAD
if (isNew) flags = flags or FLAG_NEW if (isNew) flags = flags or FLAG_NEW
if (isBookmarked) flags = flags or FLAG_BOOKMARKED
if (isDownloaded) flags = flags or FLAG_DOWNLOADED if (isDownloaded) flags = flags or FLAG_DOWNLOADED
return ChapterListItem( return ChapterListItem(
chapter = this, chapter = this,

@ -1,8 +1,6 @@
package org.koitharu.kotatsu.main.ui.owners package org.koitharu.kotatsu.main.ui.owners
import org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar
interface NoModalBottomSheetOwner { interface NoModalBottomSheetOwner {
val bsHeader: BottomSheetHeaderBar? fun getBottomSheetCollapsedHeight(): Int
} }

@ -47,6 +47,7 @@ class ChaptersSheet : BaseAdaptiveSheet<SheetChaptersBinding>(), OnListItemClick
isUnread = index > currentPosition, isUnread = index > currentPosition,
isNew = false, isNew = false,
isDownloaded = false, isDownloaded = false,
isBookmarked = false,
) )
} }
binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter -> binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter ->

@ -41,6 +41,7 @@ import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase import org.koitharu.kotatsu.details.domain.DoubleMangaLoadUseCase
import org.koitharu.kotatsu.details.domain.model.DoubleManga import org.koitharu.kotatsu.details.domain.model.DoubleManga
@ -140,7 +141,9 @@ class ReaderViewModel @Inject constructor(
flowOf(false) flowOf(false)
} else { } else {
bookmarksRepository.observeBookmark(manga, state.chapterId, state.page) bookmarksRepository.observeBookmark(manga, state.chapterId, state.page)
.map { it != null } .map {
it != null && it.chapterId == state.chapterId && it.page == state.page
}
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
@ -285,7 +288,7 @@ class ReaderViewModel @Inject constructor(
chapterId = state.chapterId, chapterId = state.chapterId,
page = state.page, page = state.page,
scroll = state.scroll, scroll = state.scroll,
imageUrl = page.preview ?: pageLoader.getPageUrl(page), imageUrl = page.preview.ifNullOrEmpty { pageLoader.getPageUrl(page) },
createdAt = Date(), createdAt = Date(),
percent = computePercent(state.chapterId, state.page), percent = computePercent(state.chapterId, state.page),
) )
@ -301,8 +304,8 @@ class ReaderViewModel @Inject constructor(
bookmarkJob = launchJob { bookmarkJob = launchJob {
loadingJob?.join() loadingJob?.join()
val manga = checkNotNull(mangaData.value?.any) val manga = checkNotNull(mangaData.value?.any)
val page = checkNotNull(getCurrentPage()) { "Page not found" } val state = checkNotNull(getCurrentState())
bookmarksRepository.removeBookmark(manga.id, page.id) bookmarksRepository.removeBookmark(manga.id, state.chapterId, state.page)
onShowToast.call(R.string.bookmark_removed) onShowToast.call(R.string.bookmark_removed)
} }
} }

@ -2,7 +2,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:tint="?colorControlNormal" android:tint="?colorError"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path

@ -48,38 +48,51 @@
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.BottomSheet" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.BottomSheet"
tools:visibility="visible"> tools:visibility="visible">
<org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar <LinearLayout
android:id="@+id/header_chapters" android:id="@+id/layout_bs_header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar" android:orientation="vertical">
app:fitStatusBar="true"
tools:menu="@menu/opt_chapters">
<ImageView <com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/button_dropdown" android:id="@+id/dragHandle"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:layout_gravity="start|center_vertical"
android:background="?selectableItemBackgroundBorderless"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_expand_more" />
<com.google.android.material.button.MaterialButton <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/button_read" android:id="@+id/toolbar_chapters"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical" android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar"
android:layout_marginHorizontal="@dimen/toolbar_button_margin" tools:menu="@menu/opt_chapters">
android:enabled="false"
android:text="@string/read" <ImageView
android:textAllCaps="false" android:id="@+id/button_dropdown"
app:iconGravity="textStart" android:layout_width="wrap_content"
app:iconPadding="8dp" android:layout_height="wrap_content"
tools:enabled="true" android:layout_gravity="start|center_vertical"
tools:icon="@drawable/ic_read" /> android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/translations"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_expand_more" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:enabled="false"
android:text="@string/read"
android:textAllCaps="false"
app:iconGravity="textStart"
app:iconPadding="8dp"
tools:enabled="true"
tools:icon="@drawable/ic_read" />
</com.google.android.material.appbar.MaterialToolbar>
</org.koitharu.kotatsu.core.ui.widgets.BottomSheetHeaderBar> </LinearLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/container_chapters" android:id="@+id/container_chapters"

@ -22,7 +22,6 @@
android:paddingRight="@dimen/list_spacing" android:paddingRight="@dimen/list_spacing"
android:paddingBottom="@dimen/grid_spacing_outer" android:paddingBottom="@dimen/grid_spacing_outer"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:trackColor="?attr/colorOutline"
tools:listitem="@layout/item_feed" /> tools:listitem="@layout/item_feed" />
</FrameLayout> </FrameLayout>

@ -38,6 +38,7 @@
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodyLarge" android:textAppearance="?attr/textAppearanceBodyLarge"
@ -55,18 +56,19 @@
</LinearLayout> </LinearLayout>
<ImageView <ImageView
android:id="@+id/imageView_new" android:id="@+id/imageView_bookmarked"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="?android:listPreferredItemPaddingEnd" android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
android:src="@drawable/ic_new" android:contentDescription="@string/bookmarks"
app:tint="?colorError" /> app:srcCompat="@drawable/ic_bookmark" />
<ImageView <ImageView
android:id="@+id/imageView_downloaded" android:id="@+id/imageView_downloaded"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="?android:listPreferredItemPaddingEnd" android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
android:src="@drawable/ic_save_ok" /> android:contentDescription="@string/downloaded"
app:srcCompat="@drawable/ic_save_ok" />
</LinearLayout> </LinearLayout>

@ -26,8 +26,6 @@
android:scrollIndicators="top" android:scrollIndicators="top"
app:bubbleSize="normal" app:bubbleSize="normal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:scrollerOffset="6dp"
app:trackColor="?attr/colorOutline"
tools:ignore="UnusedAttribute" tools:ignore="UnusedAttribute"
tools:listitem="@layout/item_checkable_new" /> tools:listitem="@layout/item_checkable_new" />
</FrameLayout> </FrameLayout>

@ -26,7 +26,6 @@
app:bubbleSize="small" app:bubbleSize="small"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="3" app:spanCount="3"
app:trackColor="?attr/colorOutline"
tools:listitem="@layout/item_page_thumb" tools:listitem="@layout/item_page_thumb"
tools:targetApi="m" /> tools:targetApi="m" />

@ -425,4 +425,5 @@
<string name="proxy">Proxy</string> <string name="proxy">Proxy</string>
<string name="invalid_value_message">Invalid value</string> <string name="invalid_value_message">Invalid value</string>
<string name="manga_branch_title_template">%1$s (%2$s)</string> <string name="manga_branch_title_template">%1$s (%2$s)</string>
<string name="downloaded">Downloaded</string>
</resources> </resources>

@ -133,6 +133,7 @@
<item name="bubbleTextColor">?colorOnTertiary</item> <item name="bubbleTextColor">?colorOnTertiary</item>
<item name="trackColor">?colorOutline</item> <item name="trackColor">?colorOutline</item>
<item name="bubbleSize">normal</item> <item name="bubbleSize">normal</item>
<item name="scrollerOffset">6dp</item>
</style> </style>
<style name="Widget.Kotatsu.ListItemTextView" parent=""> <style name="Widget.Kotatsu.ListItemTextView" parent="">

Loading…
Cancel
Save