Redesign details screen

This commit is contained in:
Koitharu
2022-07-28 16:46:20 +03:00
parent 0eebddb24c
commit ea3b43ba88
29 changed files with 762 additions and 606 deletions

View File

@@ -2,17 +2,22 @@ package org.koitharu.kotatsu.base.ui.widgets
import android.animation.LayoutTransition
import android.content.Context
import android.transition.AutoTransition
import android.transition.TransitionManager
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.annotation.AttrRes
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.withStyledAttributes
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone
import androidx.core.view.*
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
@@ -28,7 +33,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = materialR.attr.appBarLayoutStyle,
) : AppBarLayout(context, attrs, defStyleAttr) {
) : AppBarLayout(context, attrs, defStyleAttr), MenuHost {
private val binding = LayoutSheetHeaderBinding.inflate(LayoutInflater.from(context), this)
private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable)
@@ -36,10 +41,25 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
private var bottomSheetBehavior: BottomSheetBehavior<*>? = null
private val locationBuffer = IntArray(2)
private val expansionListeners = LinkedList<OnExpansionChangeListener>()
private var fitStatusBar = false
private var transition: AutoTransition? = null
@Deprecated("")
val toolbar: MaterialToolbar
get() = binding.toolbar
var title: CharSequence?
get() = binding.toolbar.title
set(value) {
binding.toolbar.title = value
}
var subtitle: CharSequence?
get() = binding.toolbar.subtitle
set(value) {
binding.toolbar.subtitle = value
}
init {
setBackgroundResource(R.drawable.sheet_toolbar_background)
layoutTransition = LayoutTransition().apply {
@@ -47,6 +67,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
context.withStyledAttributes(attrs, R.styleable.BottomSheetHeaderBar, defStyleAttr) {
binding.toolbar.title = getString(R.styleable.BottomSheetHeaderBar_title)
fitStatusBar = getBoolean(R.styleable.BottomSheetHeaderBar_fitStatusBar, fitStatusBar)
val menuResId = getResourceId(R.styleable.BottomSheetHeaderBar_menu, 0)
if (menuResId != 0) {
binding.toolbar.inflateMenu(menuResId)
@@ -89,6 +110,31 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
}
override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets {
dispatchInsets(if (insets != null) WindowInsetsCompat.toWindowInsetsCompat(insets) else null)
return super.onApplyWindowInsets(insets)
}
override fun addMenuProvider(provider: MenuProvider) {
binding.toolbar.addMenuProvider(provider)
}
override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner) {
binding.toolbar.addMenuProvider(provider, owner)
}
override fun addMenuProvider(provider: MenuProvider, owner: LifecycleOwner, state: Lifecycle.State) {
binding.toolbar.addMenuProvider(provider, owner, state)
}
override fun removeMenuProvider(provider: MenuProvider) {
binding.toolbar.removeMenuProvider(provider)
}
override fun invalidateMenu() {
binding.toolbar.invalidateMenu()
}
fun setNavigationOnClickListener(onClickListener: OnClickListener) {
binding.toolbar.setNavigationOnClickListener(onClickListener)
}
@@ -115,9 +161,24 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
if (isExpanded == binding.dragHandle.isGone) {
return
}
TransitionManager.beginDelayedTransition(this, getTransition())
binding.toolbar.navigationIcon = (if (isExpanded) closeDrawable else null)
binding.dragHandle.isGone = isExpanded
expansionListeners.forEach { it.onExpansionStateChanged(this, isExpanded) }
dispatchInsets(ViewCompat.getRootWindowInsets(this))
}
private fun dispatchInsets(insets: WindowInsetsCompat?) {
if (!fitStatusBar) {
return
}
val isExpanded = binding.dragHandle.isGone
if (isExpanded) {
val topInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0
updatePadding(top = topInset)
} else {
updatePadding(top = 0)
}
}
private fun findParentBottomSheetBehavior(): BottomSheetBehavior<*>? {
@@ -142,7 +203,12 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
private fun dismissBottomSheet() {
bottomSheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN
val behavior = bottomSheetBehavior ?: return
if (behavior.isHideable) {
behavior.state = BottomSheetBehavior.STATE_HIDDEN
} else {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
private fun shouldAddView(child: View?): Boolean {
@@ -167,6 +233,15 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
}
}
private fun getTransition(): AutoTransition {
transition?.let { return it }
val t = AutoTransition()
t.duration = context.getAnimationDuration(R.integer.config_tinyAnimTime)
// t.interpolator = AccelerateDecelerateInterpolator()
transition = t
return t
}
private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), View.OnClickListener {
override fun onStateChanged(bottomSheet: View, newState: Int) {

View File

@@ -0,0 +1,63 @@
package org.koitharu.kotatsu.details.ui
import android.view.View
import android.view.View.OnLayoutChangeListener
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.view.ActionMode
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.koitharu.kotatsu.base.ui.util.ActionModeListener
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
class ChaptersBottomSheetMediator(
bottomSheet: View,
) : OnBackPressedCallback(false),
ActionModeListener,
BottomSheetHeaderBar.OnExpansionChangeListener,
OnLayoutChangeListener {
private val behavior = BottomSheetBehavior.from(bottomSheet)
private var lockCounter = 0
override fun handleOnBackPressed() {
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
}
override fun onActionModeStarted(mode: ActionMode) {
lock()
}
override fun onActionModeFinished(mode: ActionMode) {
unlock()
}
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
isEnabled = isExpanded
}
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int,
) {
val height = bottom - top
if (height != behavior.peekHeight) {
behavior.peekHeight = height
}
}
fun lock() {
lockCounter++
behavior.isDraggable = lockCounter <= 0
}
fun unlock() {
lockCounter--
behavior.isDraggable = lockCounter <= 0
}
}

View File

@@ -2,16 +2,14 @@ package org.koitharu.kotatsu.details.ui
import android.os.Bundle
import android.view.*
import android.widget.AdapterView
import android.widget.Spinner
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import kotlin.math.roundToInt
import org.koitharu.kotatsu.R
@@ -19,7 +17,6 @@ import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
@@ -29,16 +26,13 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.parents
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem>,
AdapterView.OnItemSelectedListener,
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener,
ListSelectionController.Callback {
ListSelectionController.Callback2 {
private val viewModel by activityViewModels<DetailsViewModel>()
@@ -64,23 +58,16 @@ class ChaptersFragment :
setHasFixedSize(true)
adapter = chaptersAdapter
}
binding.spinnerBranches?.let(::initSpinner)
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
viewModel.isChaptersReversed.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu()
}
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
binding.textViewHolder.isVisible = it
activity?.invalidateOptionsMenu()
}
addMenuProvider(ChaptersMenuProvider())
}
override fun onDestroyView() {
chaptersAdapter = null
selectionController = null
binding.spinnerBranches?.adapter = null
super.onDestroyView()
}
@@ -106,7 +93,7 @@ class ChaptersFragment :
return selectionController?.onItemLongClick(item.chapter.id) ?: false
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_save -> {
DownloadService.start(
@@ -136,7 +123,6 @@ class ChaptersFragment :
true
}
R.id.action_select_range -> {
val controller = selectionController ?: return false
val items = chaptersAdapter?.items ?: return false
val ids = HashSet(controller.peekCheckedIds())
val buffer = HashSet<Long>()
@@ -164,19 +150,12 @@ class ChaptersFragment :
}
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val spinner = binding.spinnerBranches ?: return
viewModel.setSelectedBranch(spinner.selectedItem as String?)
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_chapters, menu)
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
override fun onPrepareActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
val selectedIds = selectionController?.peekCheckedIds() ?: return false
val allItems = chaptersAdapter?.items.orEmpty()
val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds }
@@ -199,49 +178,19 @@ class ChaptersFragment :
return true
}
override fun onSelectionChanged(count: Int) {
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
binding.recyclerViewChapters.invalidateItemDecorations()
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean = true
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
(item?.actionView as? SearchView)?.setQuery("", false)
viewModel.performChapterSearch(null)
return true
}
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.performChapterSearch(newText)
return true
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerViewChapters.updatePadding(
bottom = insets.bottom + (binding.spinnerBranches?.height ?: 0),
bottom = insets.bottom,
)
binding.recyclerViewChapters.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}
private fun initSpinner(spinner: Spinner) {
val branchesAdapter = BranchesAdapter()
spinner.adapter = branchesAdapter
spinner.onItemSelectedListener = this
viewModel.branches.observe(viewLifecycleOwner) {
branchesAdapter.setItems(it)
spinner.isVisible = it.size > 1
}
viewModel.selectedBranchIndex.observe(viewLifecycleOwner) {
if (it != -1 && it != spinner.selectedItemPosition) {
spinner.setSelection(it)
}
}
}
private fun onChaptersChanged(list: List<ChapterListItem>) {
val adapter = chaptersAdapter ?: return
if (adapter.itemCount == 0) {
@@ -261,29 +210,17 @@ class ChaptersFragment :
binding.progressBar.isVisible = isLoading
}
private inner class ChaptersMenuProvider : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_chapters, menu)
val searchMenuItem = menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this@ChaptersFragment)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this@ChaptersFragment)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_reversed -> {
viewModel.setChaptersReversed(!menuItem.isChecked)
true
private fun findBottomSheetBehavior(): BottomSheetBehavior<*>? {
val v = view ?: return null
for (p in v.parents) {
val layoutParams = (p as? View)?.layoutParams
if (layoutParams is CoordinatorLayout.LayoutParams) {
val behavior = layoutParams.behavior
if (behavior is BottomSheetBehavior<*>) {
return behavior
}
}
else -> false
}
return null
}
}

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.details.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R
class ChaptersMenuProvider(
private val viewModel: DetailsViewModel,
private val bottomSheetMediator: ChaptersBottomSheetMediator?,
) : MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_chapters, menu)
val searchMenuItem = menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_reversed).isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_search).isVisible = viewModel.isChaptersEmpty.value == false
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_reversed -> {
viewModel.setChaptersReversed(!menuItem.isChecked)
true
}
else -> false
}
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
bottomSheetMediator?.lock()
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
(item?.actionView as? SearchView)?.setQuery("", false)
viewModel.performChapterSearch(null)
bottomSheetMediator?.unlock()
return true
}
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.performChapterSearch(newText)
return true
}
}

View File

@@ -6,52 +6,41 @@ import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.Toast
import androidx.appcompat.view.ActionMode
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.list.ui.adapter.bindBadge
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.isReportable
import org.koitharu.kotatsu.utils.ext.report
import org.koitharu.kotatsu.utils.ext.*
@AndroidEntryPoint
class DetailsActivity :
BaseActivity<ActivityDetailsBinding>(),
TabLayoutMediator.TabConfigurationStrategy,
AdapterView.OnItemSelectedListener {
View.OnClickListener,
BottomSheetHeaderBar.OnExpansionChangeListener {
@Inject
lateinit var viewModelFactory: DetailsViewModel.Factory
@@ -59,9 +48,12 @@ class DetailsActivity :
@Inject
lateinit var shortcutsUpdater: ShortcutsUpdater
private val viewModel by assistedViewModels<DetailsViewModel> {
private var badge: BadgeDrawable? = null
private val viewModel: DetailsViewModel by assistedViewModels {
viewModelFactory.create(MangaIntent(intent))
}
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
private val downloadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
@@ -77,23 +69,51 @@ class DetailsActivity :
setDisplayHomeAsUpEnabled(true)
setDisplayShowTitleEnabled(false)
}
val pager = binding.pager
if (pager != null) {
pager.adapter = MangaDetailsAdapter(this)
TabLayoutMediator(checkNotNull(binding.tabs), pager, this).attach()
binding.buttonRead.setOnClickListener(this)
binding.buttonDropdown.setOnClickListener(this)
chaptersMenuProvider = if (binding.layoutBottom != null) {
val bsMediator = ChaptersBottomSheetMediator(checkNotNull(binding.layoutBottom))
actionModeDelegate.addListener(bsMediator)
checkNotNull(binding.headerChapters).addOnExpansionChangeListener(bsMediator)
checkNotNull(binding.headerChapters).addOnLayoutChangeListener(bsMediator)
onBackPressedDispatcher.addCallback(bsMediator)
ChaptersMenuProvider(viewModel, bsMediator)
} else {
ChaptersMenuProvider(viewModel, null)
}
gcFragments()
binding.spinnerBranches?.let(::initSpinner)
viewModel.manga.observe(this, ::onMangaUpdated)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(this, ::onError)
viewModel.onShowToast.observe(this) {
binding.snackbar.show(messageText = getString(it))
}
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranchName.observe(this) {
binding.headerChapters?.subtitle = it
binding.textViewSubtitle?.textAndVisible = it
}
viewModel.isChaptersReversed.observe(this) {
binding.headerChapters?.invalidateMenu() ?: invalidateOptionsMenu()
}
viewModel.favouriteCategories.observe(this) {
invalidateOptionsMenu()
}
viewModel.branches.observe(this) {
binding.buttonDropdown.isVisible = it.size > 1
}
registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE))
addMenuProvider(
DetailsMenuProvider(
activity = this,
viewModel = viewModel,
snackbarHost = binding.containerChapters,
shortcutsUpdater = shortcutsUpdater,
),
)
binding.headerChapters?.addOnExpansionChangeListener(this) ?: addMenuProvider(chaptersMenuProvider)
}
override fun onDestroy() {
@@ -101,8 +121,39 @@ class DetailsActivity :
super.onDestroy()
}
override fun onClick(v: View) {
val manga = viewModel.manga.value ?: return
when (v.id) {
R.id.button_read -> {
val chapterId = viewModel.historyInfo.value?.history?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
showChapterMissingDialog(chapterId)
} else {
startActivity(
ReaderActivity.newIntent(
context = this,
manga = manga,
branch = viewModel.selectedBranchValue,
),
)
}
}
R.id.button_dropdown -> showBranchPopupMenu()
}
}
override fun onExpansionStateChanged(headerBar: BottomSheetHeaderBar, isExpanded: Boolean) {
if (isExpanded) {
headerBar.addMenuProvider(chaptersMenuProvider)
} else {
headerBar.removeMenuProvider(chaptersMenuProvider)
}
binding.buttonRead.isGone = isExpanded
}
private fun onMangaUpdated(manga: Manga) {
title = manga.title
binding.buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
invalidateOptionsMenu()
}
@@ -124,149 +175,65 @@ class DetailsActivity :
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition()
}
e.isReportable() -> {
binding.snackbar.show(
messageText = e.getDisplayMessage(resources),
actionId = R.string.report,
duration = if (viewModel.manga.value?.chapters == null) {
else -> {
val snackbar = Snackbar.make(
binding.containerDetails,
e.getDisplayMessage(resources),
if (viewModel.manga.value?.chapters == null) {
Snackbar.LENGTH_INDEFINITE
} else {
Snackbar.LENGTH_LONG
},
onActionClick = {
e.report("DetailsActivity::onError")
dismiss()
},
)
}
else -> {
binding.snackbar.show(e.getDisplayMessage(resources))
if (e.isReportable()) {
snackbar.setAction(R.string.report) {
e.report("DetailsActivity::onError")
}
}
snackbar.show()
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.snackbar.updatePadding(
bottom = insets.bottom,
)
binding.root.updatePadding(
left = insets.left,
right = insets.right,
)
if (insets.bottom > 0) {
window.setNavigationBarTransparentCompat(this, binding.layoutBottom?.elevation ?: 0f)
}
}
private fun onHistoryChanged(info: HistoryInfo?) {
with(binding.buttonRead) {
if (info?.history != null) {
setText(R.string._continue)
setIconResource(R.drawable.ic_play)
} else {
setText(R.string.read)
setIconResource(R.drawable.ic_read)
}
}
val text = when {
info == null -> getString(R.string.loading_)
info.currentChapter >= 0 -> getString(R.string.chapter_d_of_d, info.currentChapter + 1, info.totalChapters)
info.totalChapters == 0 -> getString(R.string.no_chapters)
else -> resources.getQuantityString(R.plurals.chapters, info.totalChapters, info.totalChapters)
}
binding.headerChapters?.title = text
binding.textViewTitle?.text = text
}
private fun onNewChaptersChanged(newChapters: Int) {
val tab = binding.tabs?.getTabAt(1) ?: return
if (newChapters == 0) {
tab.removeBadge()
} else {
val badge = tab.orCreateBadge
badge.number = newChapters
badge.isVisible = true
}
badge = binding.buttonRead.bindBadge(badge, newChapters)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_details, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val manga = viewModel.manga.value
menu.findItem(R.id.action_save).isVisible = manga?.source != null && manga.source != MangaSource.LOCAL
menu.findItem(R.id.action_delete).isVisible = manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(this)
menu.findItem(R.id.action_shiki_track).isVisible = viewModel.isScrobblingAvailable
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_delete -> {
val title = viewModel.manga.value?.title.orEmpty()
MaterialAlertDialogBuilder(this)
.setTitle(R.string.delete_manga)
.setMessage(getString(R.string.text_delete_local_manga, title))
.setPositiveButton(R.string.delete) { _, _ ->
viewModel.deleteLocal()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
true
}
R.id.action_save -> {
viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0
val branches = viewModel.branches.value.orEmpty()
if (chaptersCount > 5 || branches.size > 1) {
showSaveConfirmation(it, chaptersCount, branches)
} else {
DownloadService.start(this, it)
}
}
true
}
R.id.action_browser -> {
viewModel.manga.value?.let {
startActivity(BrowserActivity.newIntent(this, it.publicUrl, it.title))
}
true
}
R.id.action_related -> {
viewModel.manga.value?.let {
startActivity(MultiSearchActivity.newIntent(this, it.title))
}
true
}
R.id.action_shiki_track -> {
viewModel.manga.value?.let {
ScrobblingSelectorBottomSheet.show(supportFragmentManager, it)
}
true
}
R.id.action_shortcut -> {
viewModel.manga.value?.let {
lifecycleScope.launch {
if (!shortcutsUpdater.requestPinShortcut(it)) {
binding.snackbar.show(getString(R.string.operation_not_supported))
}
}
}
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
tab.text = when (position) {
0 -> getString(R.string.details)
1 -> getString(R.string.chapters)
else -> null
}
}
override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode)
binding.pager?.isUserInputEnabled = false
}
override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode)
binding.pager?.isUserInputEnabled = true
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val spinner = binding.spinnerBranches ?: return
viewModel.setSelectedBranch(spinner.selectedItem as String?)
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
fun showChapterMissingDialog(chapterId: Long) {
val remoteManga = viewModel.getRemoteManga()
if (remoteManga == null) {
binding.snackbar.show(getString(R.string.chapter_is_missing))
Snackbar.make(binding.containerDetails, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
.show()
return
}
MaterialAlertDialogBuilder(this).apply {
@@ -289,19 +256,19 @@ class DetailsActivity :
}.show()
}
private fun initSpinner(spinner: Spinner) {
val branchesAdapter = BranchesAdapter()
spinner.adapter = branchesAdapter
spinner.onItemSelectedListener = this
viewModel.branches.observe(this) {
branchesAdapter.setItems(it)
spinner.isVisible = it.size > 1
private fun showBranchPopupMenu() {
val menu = PopupMenu(this, binding.headerChapters ?: binding.buttonDropdown)
val currentBranch = viewModel.selectedBranchValue
for (branch in viewModel.branches.value ?: return) {
val item = menu.menu.add(R.id.group_branches, Menu.NONE, Menu.NONE, branch)
item.isChecked = branch == currentBranch
}
viewModel.selectedBranchIndex.observe(this) {
if (it != -1 && it != spinner.selectedItemPosition) {
spinner.setSelection(it)
}
menu.menu.setGroupCheckable(R.id.group_branches, true, true)
menu.setOnMenuItemClickListener { item ->
viewModel.setSelectedBranch(item.title?.toString())
true
}
menu.show()
}
private fun resolveError(e: Throwable) {
@@ -315,52 +282,7 @@ class DetailsActivity :
}
}
private fun gcFragments() {
val mustHaveId = binding.pager == null
val fm = supportFragmentManager
val fragmentsToRemove = fm.fragments.filter { f ->
(f.id == 0) == mustHaveId
}
if (fragmentsToRemove.isEmpty()) {
return
}
fm.commit {
setReorderingAllowed(true)
for (f in fragmentsToRemove) {
remove(f)
}
}
}
private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List<String?>) {
val dialogBuilder = MaterialAlertDialogBuilder(this)
.setTitle(R.string.save_manga)
.setNegativeButton(android.R.string.cancel, null)
if (branches.size > 1) {
val items = Array(branches.size) { i -> branches[i].orEmpty() }
val currentBranch = viewModel.selectedBranchIndex.value ?: -1
val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch }
dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked ->
checkedIndices[i] = checked
}.setPositiveButton(R.string.save) { _, _ ->
val selectedBranches = branches.filterIndexedTo(HashSet()) { i, _ -> checkedIndices[i] }
val chaptersIds = manga.chapters?.mapNotNullToSet { c ->
if (c.branch in selectedBranches) c.id else null
}
DownloadService.start(this, manga, chaptersIds)
}
} else {
dialogBuilder.setMessage(
getString(
R.string.large_manga_save_confirm,
resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount),
),
).setPositiveButton(R.string.save) { _, _ ->
DownloadService.start(this, manga)
}
}
dialogBuilder.show()
}
private fun isTabletLayout() = binding.layoutBottom == null
companion object {

View File

@@ -2,13 +2,13 @@ package org.koitharu.kotatsu.details.ui
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.view.MenuProvider
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
@@ -30,7 +30,6 @@ import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoBottomSheet
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.parsers.model.Manga
@@ -43,7 +42,6 @@ import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.*
@AndroidEntryPoint
@@ -67,21 +65,16 @@ class DetailsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textViewAuthor.setOnClickListener(this)
binding.buttonFavorite.setOnClickListener(this)
binding.buttonRead.setOnClickListener(this)
binding.buttonRead.setOnLongClickListener(this)
binding.imageViewCover.setOnClickListener(this)
binding.scrobblingLayout.root.setOnClickListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
binding.chipsTags.onChipClickListener = this
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
addMenuProvider(DetailsMenuProvider())
}
override fun onItemClick(item: Bookmark, view: View) {
@@ -165,9 +158,6 @@ class DetailsFragment :
infoLayout.textViewNsfw.isVisible = manga.isNsfw
// Buttons
buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
// Chips
bindTags(manga)
}
@@ -182,27 +172,9 @@ class DetailsFragment :
}
private fun onHistoryChanged(history: MangaHistory?) {
with(binding.buttonRead) {
if (history == null) {
setText(R.string.read)
setIconResource(R.drawable.ic_read)
} else {
setText(R.string._continue)
setIconResource(R.drawable.ic_play)
}
}
binding.progressView.setPercent(history?.percent ?: PROGRESS_NONE, animate = true)
}
private fun onFavouriteChanged(isFavourite: Boolean) {
val iconRes = if (isFavourite) {
R.drawable.ic_heart
} else {
R.drawable.ic_heart_outline
}
binding.buttonFavorite.setIconResource(iconRes)
}
private fun onLoadingStateChanged(isLoading: Boolean) {
if (isLoading) {
binding.progressBar.show()
@@ -251,26 +223,9 @@ class DetailsFragment :
override fun onClick(v: View) {
val manga = viewModel.manga.value ?: return
when (v.id) {
R.id.button_favorite -> {
FavouriteCategoriesBottomSheet.show(childFragmentManager, manga)
}
R.id.scrobbling_layout -> {
ScrobblingInfoBottomSheet.show(childFragmentManager)
}
R.id.button_read -> {
val chapterId = viewModel.readingHistory.value?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
(activity as? DetailsActivity)?.showChapterMissingDialog(chapterId)
} else {
startActivity(
ReaderActivity.newIntent(
context = context ?: return,
manga = manga,
branch = viewModel.selectedBranchValue,
),
)
}
}
R.id.textView_author -> {
startActivity(
SearchActivity.newIntent(
@@ -331,8 +286,6 @@ class DetailsFragment :
override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
)
}
@@ -368,26 +321,4 @@ class DetailsFragment :
} ?: request.fallback(R.drawable.ic_placeholder)
request.enqueueWith(coil)
}
private inner class DetailsMenuProvider : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_details_info, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_share -> {
viewModel.manga.value?.let {
val context = requireContext()
if (it.source == MangaSource.LOCAL) {
ShareHelper(context).shareCbz(listOf(it.url.toUri().toFile()))
} else {
ShareHelper(context).shareMangaLink(it)
}
}
true
}
else -> false
}
}
}

View File

@@ -0,0 +1,149 @@
package org.koitharu.kotatsu.details.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.view.MenuProvider
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.utils.ShareHelper
class DetailsMenuProvider(
private val activity: FragmentActivity,
private val viewModel: DetailsViewModel,
private val snackbarHost: View,
private val shortcutsUpdater: ShortcutsUpdater,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_details, menu)
}
override fun onPrepareMenu(menu: Menu) {
val manga = viewModel.manga.value
menu.findItem(R.id.action_save).isVisible = manga?.source != null && manga.source != MangaSource.LOCAL
menu.findItem(R.id.action_delete).isVisible = manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_shiki_track).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value == true) R.drawable.ic_heart else R.drawable.ic_heart_outline,
)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.action_share -> {
viewModel.manga.value?.let {
val shareHelper = ShareHelper(activity)
if (it.source == MangaSource.LOCAL) {
shareHelper.shareCbz(listOf(it.url.toUri().toFile()))
} else {
shareHelper.shareMangaLink(it)
}
}
}
R.id.action_favourite -> {
viewModel.manga.value?.let {
FavouriteCategoriesBottomSheet.show(activity.supportFragmentManager, it)
}
}
R.id.action_delete -> {
val title = viewModel.manga.value?.title.orEmpty()
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.delete_manga)
.setMessage(activity.getString(R.string.text_delete_local_manga, title))
.setPositiveButton(R.string.delete) { _, _ ->
viewModel.deleteLocal()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
R.id.action_save -> {
viewModel.manga.value?.let {
val chaptersCount = it.chapters?.size ?: 0
val branches = viewModel.branches.value.orEmpty()
if (chaptersCount > 5 || branches.size > 1) {
showSaveConfirmation(it, chaptersCount, branches)
} else {
DownloadService.start(activity, it)
}
}
}
R.id.action_browser -> {
viewModel.manga.value?.let {
activity.startActivity(BrowserActivity.newIntent(activity, it.publicUrl, it.title))
}
}
R.id.action_related -> {
viewModel.manga.value?.let {
activity.startActivity(MultiSearchActivity.newIntent(activity, it.title))
}
}
R.id.action_shiki_track -> {
viewModel.manga.value?.let {
ScrobblingSelectorBottomSheet.show(activity.supportFragmentManager, it)
}
}
R.id.action_shortcut -> {
viewModel.manga.value?.let {
activity.lifecycleScope.launch {
if (!shortcutsUpdater.requestPinShortcut(it)) {
Snackbar.make(snackbarHost, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
.show()
}
}
}
}
else -> return false
}
return true
}
private fun showSaveConfirmation(manga: Manga, chaptersCount: Int, branches: List<String?>) {
val dialogBuilder = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.save_manga)
.setNegativeButton(android.R.string.cancel, null)
if (branches.size > 1) {
val items = Array(branches.size) { i -> branches[i].orEmpty() }
val currentBranch = viewModel.selectedBranchIndex.value ?: -1
val checkedIndices = BooleanArray(branches.size) { i -> i == currentBranch }
dialogBuilder.setMultiChoiceItems(items, checkedIndices) { _, i, checked ->
checkedIndices[i] = checked
}.setPositiveButton(R.string.save) { _, _ ->
val selectedBranches = branches.filterIndexedTo(HashSet()) { i, _ -> checkedIndices[i] }
val chaptersIds = manga.chapters?.mapNotNullToSet { c ->
if (c.branch in selectedBranches) c.id else null
}
DownloadService.start(activity, manga, chaptersIds)
}
} else {
dialogBuilder.setMessage(
activity.getString(
R.string.large_manga_save_confirm,
activity.resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount),
),
).setPositiveButton(R.string.save) { _, _ ->
DownloadService.start(activity, manga)
}
}
dialogBuilder.show()
}
}

View File

@@ -26,6 +26,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
@@ -36,6 +37,7 @@ import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
@@ -85,9 +87,18 @@ class DetailsViewModel @AssistedInject constructor(
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
@Deprecated("")
val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
val isChaptersReversed = chaptersReversed.asLiveData(viewModelScope.coroutineContext)
val historyInfo = combine(
delegate.manga,
history,
) { m, h ->
HistoryInfo(m, h)
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, null)
val bookmarks = delegate.manga.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
@@ -114,7 +125,7 @@ class DetailsViewModel @AssistedInject constructor(
val branches: LiveData<List<String?>> = delegate.manga.map {
val chapters = it?.chapters ?: return@map emptyList()
chapters.mapToSet { x -> x.branch }.sortedWith(BranchComparator())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val selectedBranchIndex = combine(
branches.asFlow(),
@@ -123,6 +134,9 @@ class DetailsViewModel @AssistedInject constructor(
branches.indexOf(selected)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, -1)
val selectedBranchName = delegate.selectedBranch
.asFlowLiveData(viewModelScope.coroutineContext, null)
val isChaptersEmpty: LiveData<Boolean> = combine(
delegate.manga,
isLoading.asFlow(),

View File

@@ -6,10 +6,11 @@ import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.view.View
import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.R as materialR
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.utils.ext.getThemeColor
import com.google.android.material.R as materialR
class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() {
@@ -17,7 +18,10 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
private val radius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
init {
paint.color = context.getThemeColor(materialR.attr.colorSecondaryContainer, Color.LTGRAY)
paint.color = ColorUtils.setAlphaComponent(
context.getThemeColor(materialR.attr.colorPrimary, Color.DKGRAY),
98,
)
paint.style = Paint.Style.FILL
}
@@ -30,4 +34,4 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
) {
canvas.drawRoundRect(bounds, radius, radius, paint)
}
}
}

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu.details.ui.model
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.parsers.model.Manga
class HistoryInfo(
val totalChapters: Int,
val currentChapter: Int,
val history: MangaHistory?,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as HistoryInfo
if (totalChapters != other.totalChapters) return false
if (currentChapter != other.currentChapter) return false
if (history != other.history) return false
return true
}
override fun hashCode(): Int {
var result = totalChapters
result = 31 * result + currentChapter
result = 31 * result + (history?.hashCode() ?: 0)
return result
}
}
@Suppress("FunctionName")
fun HistoryInfo(manga: Manga?, history: MangaHistory?): HistoryInfo? {
val chapters = manga?.chapters ?: return null
return HistoryInfo(
totalChapters = chapters.size,
currentChapter = if (history != null) {
chapters.indexOfFirst { it.id == history.chapterId }
} else {
-1
},
history = history,
)
}

View File

@@ -31,8 +31,8 @@ class DownloadNotification(private val context: Context, startId: Int) {
context,
startId,
DownloadService.getCancelIntent(startId),
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
),
)
private val listIntent = PendingIntent.getActivity(
context,
@@ -63,7 +63,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
NotificationCompat.VISIBILITY_PRIVATE
} else {
NotificationCompat.VISIBILITY_PUBLIC
}
},
)
when (state) {
is DownloadState.Cancelled -> {
@@ -143,7 +143,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
context,
manga.hashCode(),
DetailsActivity.newIntent(context, manga),
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
)
companion object {
@@ -158,7 +158,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
val channel = NotificationChannel(
CHANNEL_ID,
context.getString(R.string.downloads),
NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW,
)
channel.enableVibration(false)
channel.enableLights(false)
@@ -168,4 +168,4 @@ class DownloadNotification(private val context: Context, startId: Int) {
}
}
}
}
}

View File

@@ -30,7 +30,7 @@ abstract class HistoryDao {
WHERE history.deleted_at = 0
GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_tags.manga_id) DESC
LIMIT :limit"""
LIMIT :limit""",
)
abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
@@ -49,7 +49,9 @@ abstract class HistoryDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: HistoryEntity): Long
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, percent = :percent, updated_at = :updatedAt WHERE manga_id = :mangaId")
@Query(
"UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, percent = :percent, updated_at = :updatedAt, deleted_at = 0 WHERE manga_id = :mangaId",
)
abstract suspend fun update(
mangaId: Long,
page: Int,
@@ -76,7 +78,7 @@ abstract class HistoryDao {
chapterId = entity.chapterId,
scroll = entity.scroll,
percent = entity.percent,
updatedAt = entity.updatedAt
updatedAt = entity.updatedAt,
)
@Transaction

View File

@@ -331,9 +331,9 @@ class ReaderActivity :
binding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
}
}
if (uiState.totalPages > 0) {
binding.slider.value = uiState.currentPage.toFloat()
if (uiState.totalPages > 1) {
binding.slider.valueTo = uiState.totalPages.toFloat() - 1
binding.slider.value = uiState.currentPage.toFloat()
binding.slider.isVisible = true
} else {
binding.slider.isVisible = false

View File

@@ -6,6 +6,7 @@ import android.view.View
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.model.ZoomMode
@@ -15,6 +16,7 @@ import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
@AndroidEntryPoint
class ReaderSettingsFragment :
BasePreferenceFragment(R.string.reader_settings),
SharedPreferences.OnSharedPreferenceChangeListener {
@@ -65,4 +67,4 @@ class ReaderSettingsFragment :
isEnabled = settings.defaultReaderMode != ReaderMode.WEBTOON
}
}
}
}

View File

@@ -48,5 +48,5 @@ fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) {
}
fun Fragment.addMenuProvider(provider: MenuProvider) {
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.STARTED)
}