diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 2bcd23609..38963f65d 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -3,6 +3,9 @@
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 1368b79fc..cd538ea26 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -84,7 +84,7 @@ afterEvaluate {
}
}
dependencies {
- implementation('com.github.nv95:kotatsu-parsers:7588617316') {
+ implementation('com.github.KotatsuApp:kotatsu-parsers:7588617316') {
exclude group: 'org.json', module: 'json'
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
index 66d9ed580..b8f2cf0e8 100644
--- a/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/widgets/BottomSheetHeaderBar.kt
@@ -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()
+ 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) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt
new file mode 100644
index 000000000..d40cb7195
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersBottomSheetMediator.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
index 87f6e8fcf..2ea3aadaa 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt
@@ -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(),
OnListItemClickListener,
- AdapterView.OnItemSelectedListener,
- MenuItem.OnActionExpandListener,
- SearchView.OnQueryTextListener,
- ListSelectionController.Callback {
+ ListSelectionController.Callback2 {
private val viewModel by activityViewModels()
@@ -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()
@@ -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 {
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) {
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
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt
new file mode 100644
index 000000000..d9ca082ee
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersMenuProvider.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
index 1d146e96a..4537fca8b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
@@ -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(),
- 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 {
+ 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) {
- 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 {
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
index f9d17a06c..b60e41679 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsFragment.kt
@@ -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
- }
- }
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
new file mode 100644
index 000000000..561ebbb36
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsMenuProvider.kt
@@ -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) {
+ 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()
+ }
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
index ff3cbe7ea..b28863d9b 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt
@@ -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> = 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 = combine(
delegate.manga,
isLoading.asFlow(),
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt
index 0ff2fbc94..469ae6514 100644
--- a/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/adapter/ChaptersSelectionDecoration.kt
@@ -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)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt
new file mode 100644
index 000000000..a3fd7f9fa
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/model/HistoryInfo.kt
@@ -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,
+ )
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
index a8f0744bd..27b1bdd6a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
@@ -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) {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
index f96d728c7..be2c5f7e9 100644
--- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryDao.kt
@@ -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
@@ -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
diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
index 4c0d23b81..fe9c19d19 100644
--- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt
@@ -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
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt
index 39f4de6c5..0f1ab7c74 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt
@@ -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
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt
index 04018b113..ea4132ac5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FragmentExt.kt
@@ -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)
}
diff --git a/app/src/main/res/layout-w600dp/fragment_details.xml b/app/src/main/res/layout-w600dp/fragment_details.xml
index 05271da72..36e2064a4 100644
--- a/app/src/main/res/layout-w600dp/fragment_details.xml
+++ b/app/src/main/res/layout-w600dp/fragment_details.xml
@@ -107,47 +107,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
-
-
-
-
-
-
-
-
+ android:background="@drawable/m3_tabs_background"
+ android:theme="?attr/actionBarTheme"
+ app:layout_scrollFlags="noScroll"
+ tools:ignore="PrivateResource"
+ tools:menu="@menu/opt_details">
-
+ 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"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/appbar"
+ tools:enabled="true"
+ tools:icon="@drawable/ic_read" />
@@ -44,12 +56,64 @@
android:name="org.koitharu.kotatsu.details.ui.DetailsFragment"
android:layout_width="0dp"
android:layout_height="0dp"
+ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
+ app:layout_constraintWidth_percent="0.5"
tools:layout="@layout/fragment_details" />
+
+
+
+
+
+
+
+
-
-
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout-w720dp/fragment_chapters.xml b/app/src/main/res/layout-w720dp/fragment_chapters.xml
deleted file mode 100644
index a148e92db..000000000
--- a/app/src/main/res/layout-w720dp/fragment_chapters.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/activity_details.xml
index 56b04b77e..42d944b1b 100644
--- a/app/src/main/res/layout/activity_details.xml
+++ b/app/src/main/res/layout/activity_details.xml
@@ -1,5 +1,5 @@
-
-
+ android:stateListAnimator="@null"
+ app:elevation="0dp"
+ app:liftOnScroll="false">
+ app:layout_scrollFlags="noScroll"
+ tools:ignore="PrivateResource" />
-
+
-
-
-
-
-
+ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
+ tools:layout="@layout/fragment_details" />
-
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ app:behavior_hideable="false"
+ app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
+ app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.BottomSheet">
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_chapters.xml b/app/src/main/res/layout/fragment_chapters.xml
index d0c4734ea..0cc0a7bd5 100644
--- a/app/src/main/res/layout/fragment_chapters.xml
+++ b/app/src/main/res/layout/fragment_chapters.xml
@@ -1,31 +1,15 @@
-
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml
index 09f271553..85e2448e2 100644
--- a/app/src/main/res/layout/fragment_details.xml
+++ b/app/src/main/res/layout/fragment_details.xml
@@ -46,8 +46,8 @@
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
- android:maxLines="5"
android:ellipsize="end"
+ android:maxLines="5"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
@@ -121,40 +121,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_header" />
-
-
-
-
+ app:layout_constraintTop_toBottomOf="@+id/info_layout" />
-
\ No newline at end of file
+
diff --git a/app/src/main/res/menu/opt_details.xml b/app/src/main/res/menu/opt_details.xml
index d6cc9be85..7f5d3445f 100644
--- a/app/src/main/res/menu/opt_details.xml
+++ b/app/src/main/res/menu/opt_details.xml
@@ -3,6 +3,20 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
+
+
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/res/menu/opt_details_info.xml b/app/src/main/res/menu/opt_details_info.xml
deleted file mode 100644
index 1464db172..000000000
--- a/app/src/main/res/menu/opt_details_info.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 22a1984e7..bddae4f97 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -72,6 +72,7 @@
+
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index c0143cbb7..cc780e28f 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -1,7 +1,8 @@
-
-
-
+
+
+
-
\ No newline at end of file
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5a9c5c17b..4907a48b9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -360,4 +360,5 @@
Content not found or removed
Incognito mode
Application update available: %s
+ No chapters