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

@@ -3,6 +3,9 @@
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="BooleanLiteralArgument" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" /> <inspection_tool class="Destructure" enabled="true" level="INFO" enabled_by_default="true" />
<inspection_tool class="FillClass" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" />
</inspection_tool>
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" /> <inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="KotlinFunctionArgumentsHelper" enabled="true" level="INFORMATION" enabled_by_default="true"> <inspection_tool class="KotlinFunctionArgumentsHelper" enabled="true" level="INFORMATION" enabled_by_default="true">
<option name="withoutDefaultValues" value="true" /> <option name="withoutDefaultValues" value="true" />

View File

@@ -84,7 +84,7 @@ afterEvaluate {
} }
} }
dependencies { dependencies {
implementation('com.github.nv95:kotatsu-parsers:7588617316') { implementation('com.github.KotatsuApp:kotatsu-parsers:7588617316') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }

View File

@@ -2,17 +2,22 @@ package org.koitharu.kotatsu.base.ui.widgets
import android.animation.LayoutTransition import android.animation.LayoutTransition
import android.content.Context import android.content.Context
import android.transition.AutoTransition
import android.transition.TransitionManager
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowInsets
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import androidx.core.view.ViewCompat import androidx.core.view.*
import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.Lifecycle
import androidx.core.view.isGone import androidx.lifecycle.LifecycleOwner
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
@@ -28,7 +33,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = materialR.attr.appBarLayoutStyle, @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 binding = LayoutSheetHeaderBinding.inflate(LayoutInflater.from(context), this)
private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable) private val closeDrawable = context.getThemeDrawable(materialR.attr.actionModeCloseDrawable)
@@ -36,10 +41,25 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
private var bottomSheetBehavior: BottomSheetBehavior<*>? = null private var bottomSheetBehavior: BottomSheetBehavior<*>? = null
private val locationBuffer = IntArray(2) private val locationBuffer = IntArray(2)
private val expansionListeners = LinkedList<OnExpansionChangeListener>() private val expansionListeners = LinkedList<OnExpansionChangeListener>()
private var fitStatusBar = false
private var transition: AutoTransition? = null
@Deprecated("")
val toolbar: MaterialToolbar val toolbar: MaterialToolbar
get() = binding.toolbar 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 { init {
setBackgroundResource(R.drawable.sheet_toolbar_background) setBackgroundResource(R.drawable.sheet_toolbar_background)
layoutTransition = LayoutTransition().apply { layoutTransition = LayoutTransition().apply {
@@ -47,6 +67,7 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
} }
context.withStyledAttributes(attrs, R.styleable.BottomSheetHeaderBar, defStyleAttr) { context.withStyledAttributes(attrs, R.styleable.BottomSheetHeaderBar, defStyleAttr) {
binding.toolbar.title = getString(R.styleable.BottomSheetHeaderBar_title) binding.toolbar.title = getString(R.styleable.BottomSheetHeaderBar_title)
fitStatusBar = getBoolean(R.styleable.BottomSheetHeaderBar_fitStatusBar, fitStatusBar)
val menuResId = getResourceId(R.styleable.BottomSheetHeaderBar_menu, 0) val menuResId = getResourceId(R.styleable.BottomSheetHeaderBar_menu, 0)
if (menuResId != 0) { if (menuResId != 0) {
binding.toolbar.inflateMenu(menuResId) 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) { fun setNavigationOnClickListener(onClickListener: OnClickListener) {
binding.toolbar.setNavigationOnClickListener(onClickListener) binding.toolbar.setNavigationOnClickListener(onClickListener)
} }
@@ -115,9 +161,24 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
if (isExpanded == binding.dragHandle.isGone) { if (isExpanded == binding.dragHandle.isGone) {
return return
} }
TransitionManager.beginDelayedTransition(this, getTransition())
binding.toolbar.navigationIcon = (if (isExpanded) closeDrawable else null) binding.toolbar.navigationIcon = (if (isExpanded) closeDrawable else null)
binding.dragHandle.isGone = isExpanded binding.dragHandle.isGone = isExpanded
expansionListeners.forEach { it.onExpansionStateChanged(this, 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<*>? { private fun findParentBottomSheetBehavior(): BottomSheetBehavior<*>? {
@@ -142,7 +203,12 @@ class BottomSheetHeaderBar @JvmOverloads constructor(
} }
private fun dismissBottomSheet() { 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 { 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 { private inner class Callback : BottomSheetBehavior.BottomSheetCallback(), View.OnClickListener {
override fun onStateChanged(bottomSheet: View, newState: Int) { 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.os.Bundle
import android.view.* import android.view.*
import android.widget.AdapterView
import android.widget.Spinner
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlin.math.roundToInt import kotlin.math.roundToInt
import org.koitharu.kotatsu.R 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.ListSelectionController
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding 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.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
import org.koitharu.kotatsu.details.ui.model.ChapterListItem 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.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback 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 import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
class ChaptersFragment : class ChaptersFragment :
BaseFragment<FragmentChaptersBinding>(), BaseFragment<FragmentChaptersBinding>(),
OnListItemClickListener<ChapterListItem>, OnListItemClickListener<ChapterListItem>,
AdapterView.OnItemSelectedListener, ListSelectionController.Callback2 {
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener,
ListSelectionController.Callback {
private val viewModel by activityViewModels<DetailsViewModel>() private val viewModel by activityViewModels<DetailsViewModel>()
@@ -64,23 +58,16 @@ class ChaptersFragment :
setHasFixedSize(true) setHasFixedSize(true)
adapter = chaptersAdapter adapter = chaptersAdapter
} }
binding.spinnerBranches?.let(::initSpinner)
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged) viewModel.chapters.observe(viewLifecycleOwner, this::onChaptersChanged)
viewModel.isChaptersReversed.observe(viewLifecycleOwner) {
activity?.invalidateOptionsMenu()
}
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) { viewModel.isChaptersEmpty.observe(viewLifecycleOwner) {
binding.textViewHolder.isVisible = it binding.textViewHolder.isVisible = it
activity?.invalidateOptionsMenu()
} }
addMenuProvider(ChaptersMenuProvider())
} }
override fun onDestroyView() { override fun onDestroyView() {
chaptersAdapter = null chaptersAdapter = null
selectionController = null selectionController = null
binding.spinnerBranches?.adapter = null
super.onDestroyView() super.onDestroyView()
} }
@@ -106,7 +93,7 @@ class ChaptersFragment :
return selectionController?.onItemLongClick(item.chapter.id) ?: false 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) { return when (item.itemId) {
R.id.action_save -> { R.id.action_save -> {
DownloadService.start( DownloadService.start(
@@ -136,7 +123,6 @@ class ChaptersFragment :
true true
} }
R.id.action_select_range -> { R.id.action_select_range -> {
val controller = selectionController ?: return false
val items = chaptersAdapter?.items ?: return false val items = chaptersAdapter?.items ?: return false
val ids = HashSet(controller.peekCheckedIds()) val ids = HashSet(controller.peekCheckedIds())
val buffer = HashSet<Long>() val buffer = HashSet<Long>()
@@ -164,19 +150,12 @@ class ChaptersFragment :
} }
} }
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
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 {
mode.menuInflater.inflate(R.menu.mode_chapters, menu) mode.menuInflater.inflate(R.menu.mode_chapters, menu)
return true 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 selectedIds = selectionController?.peekCheckedIds() ?: return false
val allItems = chaptersAdapter?.items.orEmpty() val allItems = chaptersAdapter?.items.orEmpty()
val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds } val items = allItems.withIndex().filter { (_, x) -> x.chapter.id in selectedIds }
@@ -199,49 +178,19 @@ class ChaptersFragment :
return true return true
} }
override fun onSelectionChanged(count: Int) { override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
binding.recyclerViewChapters.invalidateItemDecorations() 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) { override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerViewChapters.updatePadding( binding.recyclerViewChapters.updatePadding(
bottom = insets.bottom + (binding.spinnerBranches?.height ?: 0), bottom = insets.bottom,
) )
binding.recyclerViewChapters.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.recyclerViewChapters.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom 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>) { private fun onChaptersChanged(list: List<ChapterListItem>) {
val adapter = chaptersAdapter ?: return val adapter = chaptersAdapter ?: return
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
@@ -261,29 +210,17 @@ class ChaptersFragment :
binding.progressBar.isVisible = isLoading binding.progressBar.isVisible = isLoading
} }
private inner class ChaptersMenuProvider : MenuProvider { private fun findBottomSheetBehavior(): BottomSheetBehavior<*>? {
val v = view ?: return null
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { for (p in v.parents) {
menuInflater.inflate(R.menu.opt_chapters, menu) val layoutParams = (p as? View)?.layoutParams
val searchMenuItem = menu.findItem(R.id.action_search) if (layoutParams is CoordinatorLayout.LayoutParams) {
searchMenuItem.setOnActionExpandListener(this@ChaptersFragment) val behavior = layoutParams.behavior
val searchView = searchMenuItem.actionView as SearchView if (behavior is BottomSheetBehavior<*>) {
searchView.setOnQueryTextListener(this@ChaptersFragment) return behavior
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
} }
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.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.PopupMenu
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
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 androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar 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 dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaIntent import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseActivity 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.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.os.ShortcutsUpdater
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding 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.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.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.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.scrobbling.ui.selector.ScrobblingSelectorBottomSheet import org.koitharu.kotatsu.utils.ext.*
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
@AndroidEntryPoint @AndroidEntryPoint
class DetailsActivity : class DetailsActivity :
BaseActivity<ActivityDetailsBinding>(), BaseActivity<ActivityDetailsBinding>(),
TabLayoutMediator.TabConfigurationStrategy, View.OnClickListener,
AdapterView.OnItemSelectedListener { BottomSheetHeaderBar.OnExpansionChangeListener {
@Inject @Inject
lateinit var viewModelFactory: DetailsViewModel.Factory lateinit var viewModelFactory: DetailsViewModel.Factory
@@ -59,9 +48,12 @@ class DetailsActivity :
@Inject @Inject
lateinit var shortcutsUpdater: ShortcutsUpdater 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)) viewModelFactory.create(MangaIntent(intent))
} }
private lateinit var chaptersMenuProvider: ChaptersMenuProvider
private val downloadReceiver = object : BroadcastReceiver() { private val downloadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
@@ -77,23 +69,51 @@ class DetailsActivity :
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowTitleEnabled(false) setDisplayShowTitleEnabled(false)
} }
val pager = binding.pager binding.buttonRead.setOnClickListener(this)
if (pager != null) { binding.buttonDropdown.setOnClickListener(this)
pager.adapter = MangaDetailsAdapter(this)
TabLayoutMediator(checkNotNull(binding.tabs), pager, this).attach() 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.manga.observe(this, ::onMangaUpdated)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged) viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onMangaRemoved.observe(this, ::onMangaRemoved) viewModel.onMangaRemoved.observe(this, ::onMangaRemoved)
viewModel.onError.observe(this, ::onError) viewModel.onError.observe(this, ::onError)
viewModel.onShowToast.observe(this) { 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)) 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() { override fun onDestroy() {
@@ -101,8 +121,39 @@ class DetailsActivity :
super.onDestroy() 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) { private fun onMangaUpdated(manga: Manga) {
title = manga.title title = manga.title
binding.buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@@ -124,149 +175,65 @@ class DetailsActivity :
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finishAfterTransition() finishAfterTransition()
} }
e.isReportable() -> { else -> {
binding.snackbar.show( val snackbar = Snackbar.make(
messageText = e.getDisplayMessage(resources), binding.containerDetails,
actionId = R.string.report, e.getDisplayMessage(resources),
duration = if (viewModel.manga.value?.chapters == null) { if (viewModel.manga.value?.chapters == null) {
Snackbar.LENGTH_INDEFINITE Snackbar.LENGTH_INDEFINITE
} else { } else {
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
}, },
onActionClick = {
e.report("DetailsActivity::onError")
dismiss()
},
) )
} if (e.isReportable()) {
else -> { snackbar.setAction(R.string.report) {
binding.snackbar.show(e.getDisplayMessage(resources)) e.report("DetailsActivity::onError")
}
}
snackbar.show()
} }
} }
} }
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.snackbar.updatePadding(
bottom = insets.bottom,
)
binding.root.updatePadding( binding.root.updatePadding(
left = insets.left, left = insets.left,
right = insets.right, 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) { private fun onNewChaptersChanged(newChapters: Int) {
val tab = binding.tabs?.getTabAt(1) ?: return badge = binding.buttonRead.bindBadge(badge, newChapters)
if (newChapters == 0) {
tab.removeBadge()
} else {
val badge = tab.orCreateBadge
badge.number = newChapters
badge.isVisible = true
}
} }
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) { fun showChapterMissingDialog(chapterId: Long) {
val remoteManga = viewModel.getRemoteManga() val remoteManga = viewModel.getRemoteManga()
if (remoteManga == null) { 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 return
} }
MaterialAlertDialogBuilder(this).apply { MaterialAlertDialogBuilder(this).apply {
@@ -289,19 +256,19 @@ class DetailsActivity :
}.show() }.show()
} }
private fun initSpinner(spinner: Spinner) { private fun showBranchPopupMenu() {
val branchesAdapter = BranchesAdapter() val menu = PopupMenu(this, binding.headerChapters ?: binding.buttonDropdown)
spinner.adapter = branchesAdapter val currentBranch = viewModel.selectedBranchValue
spinner.onItemSelectedListener = this for (branch in viewModel.branches.value ?: return) {
viewModel.branches.observe(this) { val item = menu.menu.add(R.id.group_branches, Menu.NONE, Menu.NONE, branch)
branchesAdapter.setItems(it) item.isChecked = branch == currentBranch
spinner.isVisible = it.size > 1
} }
viewModel.selectedBranchIndex.observe(this) { menu.menu.setGroupCheckable(R.id.group_branches, true, true)
if (it != -1 && it != spinner.selectedItemPosition) { menu.setOnMenuItemClickListener { item ->
spinner.setSelection(it) viewModel.setSelectedBranch(item.title?.toString())
} true
} }
menu.show()
} }
private fun resolveError(e: Throwable) { private fun resolveError(e: Throwable) {
@@ -315,52 +282,7 @@ class DetailsActivity :
} }
} }
private fun gcFragments() { private fun isTabletLayout() = binding.layoutBottom == null
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()
}
companion object { companion object {

View File

@@ -2,13 +2,13 @@ package org.koitharu.kotatsu.details.ui
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod 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.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.MenuProvider
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
@@ -30,7 +30,6 @@ import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoBottomSheet 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.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.image.ui.ImageActivity import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.parsers.model.Manga 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.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
@AndroidEntryPoint @AndroidEntryPoint
@@ -67,21 +65,16 @@ class DetailsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.textViewAuthor.setOnClickListener(this) binding.textViewAuthor.setOnClickListener(this)
binding.buttonFavorite.setOnClickListener(this)
binding.buttonRead.setOnClickListener(this)
binding.buttonRead.setOnLongClickListener(this)
binding.imageViewCover.setOnClickListener(this) binding.imageViewCover.setOnClickListener(this)
binding.scrobblingLayout.root.setOnClickListener(this) binding.scrobblingLayout.root.setOnClickListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
binding.chipsTags.onChipClickListener = this binding.chipsTags.onChipClickListener = this
viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated) viewModel.manga.observe(viewLifecycleOwner, ::onMangaUpdated)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged) viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
viewModel.favouriteCategories.observe(viewLifecycleOwner, ::onFavouriteChanged)
viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged) viewModel.readingHistory.observe(viewLifecycleOwner, ::onHistoryChanged)
viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged) viewModel.bookmarks.observe(viewLifecycleOwner, ::onBookmarksChanged)
viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged) viewModel.scrobblingInfo.observe(viewLifecycleOwner, ::onScrobblingInfoChanged)
viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged) viewModel.description.observe(viewLifecycleOwner, ::onDescriptionChanged)
addMenuProvider(DetailsMenuProvider())
} }
override fun onItemClick(item: Bookmark, view: View) { override fun onItemClick(item: Bookmark, view: View) {
@@ -165,9 +158,6 @@ class DetailsFragment :
infoLayout.textViewNsfw.isVisible = manga.isNsfw infoLayout.textViewNsfw.isVisible = manga.isNsfw
// Buttons
buttonRead.isEnabled = !manga.chapters.isNullOrEmpty()
// Chips // Chips
bindTags(manga) bindTags(manga)
} }
@@ -182,27 +172,9 @@ class DetailsFragment :
} }
private fun onHistoryChanged(history: MangaHistory?) { 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) 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) { private fun onLoadingStateChanged(isLoading: Boolean) {
if (isLoading) { if (isLoading) {
binding.progressBar.show() binding.progressBar.show()
@@ -251,26 +223,9 @@ class DetailsFragment :
override fun onClick(v: View) { override fun onClick(v: View) {
val manga = viewModel.manga.value ?: return val manga = viewModel.manga.value ?: return
when (v.id) { when (v.id) {
R.id.button_favorite -> {
FavouriteCategoriesBottomSheet.show(childFragmentManager, manga)
}
R.id.scrobbling_layout -> { R.id.scrobbling_layout -> {
ScrobblingInfoBottomSheet.show(childFragmentManager) 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 -> { R.id.textView_author -> {
startActivity( startActivity(
SearchActivity.newIntent( SearchActivity.newIntent(
@@ -331,8 +286,6 @@ class DetailsFragment :
override fun onWindowInsetsChanged(insets: Insets) { override fun onWindowInsetsChanged(insets: Insets) {
binding.root.updatePadding( binding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom, bottom = insets.bottom,
) )
} }
@@ -368,26 +321,4 @@ class DetailsFragment :
} ?: request.fallback(R.drawable.ic_placeholder) } ?: request.fallback(R.drawable.ic_placeholder)
request.enqueueWith(coil) 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.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.domain.BranchComparator import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem 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.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository 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.scrobbling.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent 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.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
@@ -85,9 +87,18 @@ class DetailsViewModel @AssistedInject constructor(
val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext) val manga = delegate.manga.filterNotNull().asLiveData(viewModelScope.coroutineContext)
val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext) val favouriteCategories = favourite.asLiveData(viewModelScope.coroutineContext)
val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext) val newChaptersCount = newChapters.asLiveData(viewModelScope.coroutineContext)
@Deprecated("")
val readingHistory = history.asLiveData(viewModelScope.coroutineContext) val readingHistory = history.asLiveData(viewModelScope.coroutineContext)
val isChaptersReversed = chaptersReversed.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 { val bookmarks = delegate.manga.flatMapLatest {
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList()) if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
@@ -114,7 +125,7 @@ class DetailsViewModel @AssistedInject constructor(
val branches: LiveData<List<String?>> = delegate.manga.map { val branches: LiveData<List<String?>> = delegate.manga.map {
val chapters = it?.chapters ?: return@map emptyList() val chapters = it?.chapters ?: return@map emptyList()
chapters.mapToSet { x -> x.branch }.sortedWith(BranchComparator()) chapters.mapToSet { x -> x.branch }.sortedWith(BranchComparator())
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, emptyList()) }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, emptyList())
val selectedBranchIndex = combine( val selectedBranchIndex = combine(
branches.asFlow(), branches.asFlow(),
@@ -123,6 +134,9 @@ class DetailsViewModel @AssistedInject constructor(
branches.indexOf(selected) branches.indexOf(selected)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, -1) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, -1)
val selectedBranchName = delegate.selectedBranch
.asFlowLiveData(viewModelScope.coroutineContext, null)
val isChaptersEmpty: LiveData<Boolean> = combine( val isChaptersEmpty: LiveData<Boolean> = combine(
delegate.manga, delegate.manga,
isLoading.asFlow(), isLoading.asFlow(),

View File

@@ -6,10 +6,11 @@ import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.view.View import android.view.View
import androidx.core.graphics.ColorUtils
import androidx.recyclerview.widget.RecyclerView 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.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.utils.ext.getThemeColor import org.koitharu.kotatsu.utils.ext.getThemeColor
import com.google.android.material.R as materialR
class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecoration() { 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) private val radius = context.resources.getDimension(materialR.dimen.abc_control_corner_material)
init { 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 paint.style = Paint.Style.FILL
} }
@@ -30,4 +34,4 @@ class ChaptersSelectionDecoration(context: Context) : AbstractSelectionItemDecor
) { ) {
canvas.drawRoundRect(bounds, radius, radius, paint) 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, context,
startId, startId,
DownloadService.getCancelIntent(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( private val listIntent = PendingIntent.getActivity(
context, context,
@@ -63,7 +63,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
NotificationCompat.VISIBILITY_PRIVATE NotificationCompat.VISIBILITY_PRIVATE
} else { } else {
NotificationCompat.VISIBILITY_PUBLIC NotificationCompat.VISIBILITY_PUBLIC
} },
) )
when (state) { when (state) {
is DownloadState.Cancelled -> { is DownloadState.Cancelled -> {
@@ -143,7 +143,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
context, context,
manga.hashCode(), manga.hashCode(),
DetailsActivity.newIntent(context, manga), DetailsActivity.newIntent(context, manga),
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
) )
companion object { companion object {
@@ -158,7 +158,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
val channel = NotificationChannel( val channel = NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
context.getString(R.string.downloads), context.getString(R.string.downloads),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW,
) )
channel.enableVibration(false) channel.enableVibration(false)
channel.enableLights(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 WHERE history.deleted_at = 0
GROUP BY manga_tags.tag_id GROUP BY manga_tags.tag_id
ORDER BY COUNT(manga_tags.manga_id) DESC ORDER BY COUNT(manga_tags.manga_id) DESC
LIMIT :limit""" LIMIT :limit""",
) )
abstract suspend fun findPopularTags(limit: Int): List<TagEntity> abstract suspend fun findPopularTags(limit: Int): List<TagEntity>
@@ -49,7 +49,9 @@ abstract class HistoryDao {
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: HistoryEntity): Long 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( abstract suspend fun update(
mangaId: Long, mangaId: Long,
page: Int, page: Int,
@@ -76,7 +78,7 @@ abstract class HistoryDao {
chapterId = entity.chapterId, chapterId = entity.chapterId,
scroll = entity.scroll, scroll = entity.scroll,
percent = entity.percent, percent = entity.percent,
updatedAt = entity.updatedAt updatedAt = entity.updatedAt,
) )
@Transaction @Transaction

View File

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

View File

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

View File

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

View File

@@ -107,47 +107,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_favorite"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:contentDescription="@string/add_to_favourites"
android:paddingStart="0dp"
android:paddingEnd="0dp"
app:icon="@drawable/ic_heart_outline"
app:iconGravity="textTop"
app:iconPadding="0dp"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/button_read" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_read"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:enabled="false"
android:text="@string/read"
android:textAllCaps="false"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_favorite"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/_continue" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout> </LinearLayout>
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier

View File

@@ -5,14 +5,15 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".details.ui.DetailsActivity"> tools:context=".details.ui.DetailsActivity">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:elevation="0dp"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:stateListAnimator="@null"
app:elevation="0dp" app:elevation="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -23,17 +24,28 @@
android:id="@id/toolbar" android:id="@id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"> android:background="@drawable/m3_tabs_background"
android:theme="?attr/actionBarTheme"
app:layout_scrollFlags="noScroll"
tools:ignore="PrivateResource"
tools:menu="@menu/opt_details">
<Spinner
android:id="@+id/spinner_branches" <com.google.android.material.button.MaterialButton
android:id="@+id/button_read"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end|center_vertical"
android:layout_marginEnd="8dp" android:layout_marginHorizontal="@dimen/toolbar_button_margin"
android:visibility="gone" android:enabled="false"
tools:listitem="@layout/item_branch" android:text="@string/read"
tools:visibility="visible" /> android:textAllCaps="false"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
tools:enabled="true"
tools:icon="@drawable/ic_read" />
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>
@@ -44,12 +56,64 @@
android:name="org.koitharu.kotatsu.details.ui.DetailsFragment" android:name="org.koitharu.kotatsu.details.ui.DetailsFragment"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_constraintWidth_percent="0.5"
tools:layout="@layout/fragment_details" /> tools:layout="@layout/fragment_details" />
<ImageView
android:id="@+id/button_dropdown"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_expand_more"
app:layout_constraintBottom_toTopOf="@id/divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/margin_normal"
android:singleLine="true"
android:textAppearance="?textAppearanceTitleMedium"
app:layout_constraintBottom_toTopOf="@id/textView_subtitle"
app:layout_constraintEnd_toStartOf="@id/button_dropdown"
app:layout_constraintStart_toStartOf="@id/container_chapters"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/chapter_d_of_d" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/margin_normal"
android:singleLine="true"
android:textAppearance="?textAppearanceTitleSmall"
android:textColor="?android:textColorSecondary"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/divider"
app:layout_constraintEnd_toStartOf="@id/button_dropdown"
app:layout_constraintStart_toStartOf="@id/container_chapters"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:text="English"
tools:visibility="visible" />
<View
android:id="@+id/divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="48dp"
android:background="?colorSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/container_details"
app:layout_constraintTop_toBottomOf="@id/appbar" />
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/container_chapters" android:id="@+id/container_chapters"
android:name="org.koitharu.kotatsu.details.ui.ChaptersFragment" android:name="org.koitharu.kotatsu.details.ui.ChaptersFragment"
@@ -57,30 +121,8 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline" app:layout_constraintStart_toEndOf="@id/container_details"
app:layout_constraintTop_toBottomOf="@id/appbar" app:layout_constraintTop_toBottomOf="@id/divider"
tools:layout="@layout/fragment_chapters" /> tools:layout="@layout/fragment_chapters" />
<org.koitharu.kotatsu.base.ui.widgets.FadingSnackbar </androidx.constraintlayout.widget.ConstraintLayout>
android:id="@+id/snackbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<View
android:id="@+id/guideline"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginBottom="6dp"
android:background="?colorOutline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.6"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_chapter" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/margin_normal"
android:gravity="center"
android:text="@string/chapters_empty"
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@@ -7,12 +7,15 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".details.ui.DetailsActivity"> tools:context=".details.ui.DetailsActivity">
<com.google.android.material.appbar.KotatsuAppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:elevation="0dp"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
app:elevation="0dp"> android:stateListAnimator="@null"
app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar" android:id="@id/toolbar"
@@ -20,32 +23,69 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="@drawable/m3_tabs_background" android:background="@drawable/m3_tabs_background"
android:theme="?attr/actionBarTheme" android:theme="?attr/actionBarTheme"
app:layout_scrollFlags="scroll|enterAlways|snap" app:layout_scrollFlags="noScroll"
tools:ignore="PrivateResource"> tools:ignore="PrivateResource" />
<com.google.android.material.tabs.TabLayout </com.google.android.material.appbar.AppBarLayout>
android:id="@+id/tabs"
style="@style/Widget.Kotatsu.SuperTabs"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:background="@null" />
</com.google.android.material.appbar.MaterialToolbar> <androidx.fragment.app.FragmentContainerView
android:id="@+id/container_details"
</com.google.android.material.appbar.KotatsuAppBarLayout> android:name="org.koitharu.kotatsu.details.ui.DetailsFragment"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:layout="@layout/fragment_details" />
<org.koitharu.kotatsu.base.ui.widgets.FadingSnackbar <LinearLayout
android:id="@+id/snackbar" android:id="@+id/layout_bottom"
style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_gravity="bottom" android:orientation="vertical"
android:visibility="gone" /> app:behavior_hideable="false"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.BottomSheet">
</org.koitharu.kotatsu.base.ui.widgets.KotatsuCoordinatorLayout> <org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar
android:id="@+id/header_chapters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Kotatsu.MainToolbar"
app:fitStatusBar="true"
tools:menu="@menu/opt_chapters">
<ImageView
android:id="@+id/button_dropdown"
android:layout_width="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
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" />
</org.koitharu.kotatsu.base.ui.widgets.BottomSheetHeaderBar>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container_chapters"
android:name="org.koitharu.kotatsu.details.ui.ChaptersFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_chapters" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,31 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<Spinner
android:id="@+id/spinner_branches"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:visibility="gone"
tools:listitem="@layout/item_branch"
tools:visibility="visible" />
<org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView <org.koitharu.kotatsu.base.ui.list.fastscroll.FastScrollRecyclerView
android:id="@+id/recyclerView_chapters" android:id="@+id/recyclerView_chapters"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="match_parent"
android:layout_alignWithParentIfMissing="true"
android:layout_below="@id/spinner_branches"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
@@ -35,7 +19,6 @@
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center" android:layout_gravity="center"
android:indeterminate="true" android:indeterminate="true"
android:visibility="gone" android:visibility="gone"
@@ -45,12 +28,11 @@
android:id="@+id/textView_holder" android:id="@+id/textView_holder"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignWithParentIfMissing="true" android:layout_gravity="center"
android:layout_below="@id/spinner_branches" android:layout_marginStart="@dimen/margin_normal"
android:layout_alignParentStart="true" android:layout_marginTop="@dimen/margin_normal"
android:layout_alignParentEnd="true" android:layout_marginEnd="@dimen/margin_normal"
android:layout_alignParentBottom="true" android:layout_marginBottom="@dimen/margin_normal"
android:layout_margin="@dimen/margin_normal"
android:gravity="center" android:gravity="center"
android:text="@string/chapters_empty" android:text="@string/chapters_empty"
android:textAlignment="center" android:textAlignment="center"
@@ -58,4 +40,4 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
</RelativeLayout> </FrameLayout>

View File

@@ -46,8 +46,8 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:maxLines="5"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="5"
android:textAppearance="?attr/textAppearanceHeadlineSmall" android:textAppearance="?attr/textAppearanceHeadlineSmall"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover" app:layout_constraintStart_toEndOf="@id/imageView_cover"
@@ -121,40 +121,6 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" /> app:layout_constraintTop_toBottomOf="@id/barrier_header" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_favorite"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:contentDescription="@string/add_to_favourites"
android:paddingStart="0dp"
android:paddingEnd="0dp"
app:icon="@drawable/ic_heart_outline"
app:iconGravity="textTop"
app:iconPadding="0dp"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/button_read" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_read"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:enabled="false"
android:text="@string/read"
android:textAllCaps="false"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_favorite"
app:layout_constraintTop_toBottomOf="@id/info_layout"
tools:text="@string/_continue" />
<org.koitharu.kotatsu.base.ui.widgets.ChipsView <org.koitharu.kotatsu.base.ui.widgets.ChipsView
android:id="@+id/chips_tags" android:id="@+id/chips_tags"
android:layout_width="0dp" android:layout_width="0dp"
@@ -166,7 +132,7 @@
app:chipSpacingVertical="6dp" app:chipSpacingVertical="6dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button_read" /> app:layout_constraintTop_toBottomOf="@+id/info_layout" />
<TextView <TextView
android:id="@+id/textView_bookmarks" android:id="@+id/textView_bookmarks"

View File

@@ -18,4 +18,4 @@
android:title="@string/reverse" android:title="@string/reverse"
app:showAsAction="never" /> app:showAsAction="never" />
</menu> </menu>

View File

@@ -3,6 +3,20 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_favourite"
android:icon="@drawable/ic_heart_outline"
android:orderInCategory="10"
android:title="@string/add_to_favourites"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_share"
android:icon="?actionModeShareDrawable"
android:orderInCategory="15"
android:title="@string/share"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/action_save" android:id="@+id/action_save"
android:orderInCategory="40" android:orderInCategory="40"
@@ -41,4 +55,4 @@
android:title="@string/create_shortcut" android:title="@string/create_shortcut"
app:showAsAction="never" /> app:showAsAction="never" />
</menu> </menu>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:icon="?actionModeShareDrawable"
android:orderInCategory="15"
android:title="@string/share"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -72,6 +72,7 @@
<declare-styleable name="BottomSheetHeaderBar"> <declare-styleable name="BottomSheetHeaderBar">
<attr name="title" /> <attr name="title" />
<attr name="menu" /> <attr name="menu" />
<attr name="fitStatusBar" format="boolean" />
</declare-styleable> </declare-styleable>
</resources> </resources>

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<item name="toolbar" type="id" /> <item name="toolbar" type="id" />
<item name="container" type="id" /> <item name="container" type="id" />
<item name="action_leaks" type="id" /> <item name="action_leaks" type="id" />
<item name="fast_scroller" type="id" /> <item name="fast_scroller" type="id" />
</resources> <item name="group_branches" type="id" />
</resources>

View File

@@ -360,4 +360,5 @@
<string name="not_found_404">Content not found or removed</string> <string name="not_found_404">Content not found or removed</string>
<string name="incognito_mode">Incognito mode</string> <string name="incognito_mode">Incognito mode</string>
<string name="app_update_available_s">Application update available: %s</string> <string name="app_update_available_s">Application update available: %s</string>
<string name="no_chapters">No chapters</string>
</resources> </resources>