Update reader actions bar

This commit is contained in:
Koitharu
2025-03-03 14:03:47 +02:00
parent 5d91e20844
commit 09590cfab0
18 changed files with 500 additions and 425 deletions

View File

@@ -170,7 +170,7 @@ private fun SpannableStringBuilder.appendTagsSummary(filter: MangaListFilter) {
}
}
fun MangaChapter.getLocalizedTitle(resources: Resources): String {
fun MangaChapter.getLocalizedTitle(resources: Resources, index: Int = -1): String {
title?.let {
if (it.isNotBlank()) {
return it
@@ -181,6 +181,12 @@ fun MangaChapter.getLocalizedTitle(resources: Resources): String {
return when {
num != null && vol != null -> resources.getString(R.string.chapter_volume_number, vol, num)
num != null -> resources.getString(R.string.chapter_number, num)
else -> resources.getString(R.string.unnamed_chapter) // TODO fallback to manga title + index
index > 0 -> resources.getString(
R.string.chapters_time_pattern,
resources.getString(R.string.unnamed_chapter),
index.toString(),
)
else -> resources.getString(R.string.unnamed_chapter)
}
}

View File

@@ -33,7 +33,7 @@ data class MangaDetails(
val coverUrl: String?
get() = manga.largeCoverUrl
.ifNullOrEmpty { manga.largeCoverUrl }
.ifNullOrEmpty { manga.coverUrl }
.ifNullOrEmpty { localManga?.manga?.coverUrl }
?.nullIfEmpty()

View File

@@ -69,7 +69,6 @@ data class ChapterListItem(
}
}
private fun buildDescription(): String {
val joiner = StringJoiner("")
chapter.numberString()?.let {

View File

@@ -0,0 +1,239 @@
package org.koitharu.kotatsu.reader.ui
import android.content.Context
import android.content.SharedPreferences
import android.database.ContentObserver
import android.provider.Settings
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.annotation.AttrRes
import androidx.annotation.StringRes
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderControl
import org.koitharu.kotatsu.core.util.ext.isRtl
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.databinding.LayoutReaderActionsBinding
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES
import org.koitharu.kotatsu.reader.ui.ReaderControlDelegate.OnInteractionListener
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class ReaderActionsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr),
View.OnClickListener,
SharedPreferences.OnSharedPreferenceChangeListener,
Slider.OnChangeListener,
Slider.OnSliderTouchListener {
@Inject
lateinit var settings: AppSettings
private val binding = LayoutReaderActionsBinding.inflate(LayoutInflater.from(context), this)
private val rotationObserver = object : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
updateRotationButton()
}
}
private var isSliderChanged = false
private var isSliderTracking = false
var isSliderEnabled: Boolean
get() = binding.slider.isEnabled
set(value) {
binding.slider.isEnabled = value
binding.slider.setThumbVisible(value)
}
var isNextEnabled: Boolean
get() = binding.buttonNext.isEnabled
set(value) {
binding.buttonNext.isEnabled = value
}
var isPrevEnabled: Boolean
get() = binding.buttonPrev.isEnabled
set(value) {
binding.buttonPrev.isEnabled = value
}
var listener: OnInteractionListener? = null
init {
orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
binding.buttonNext.initAction()
binding.buttonPrev.initAction()
binding.buttonSave.initAction()
binding.buttonOptions.initAction()
binding.buttonScreenRotation.initAction()
binding.buttonPagesThumbs.initAction()
binding.slider.setLabelFormatter(PageLabelFormatter())
binding.slider.addOnChangeListener(this)
binding.slider.addOnSliderTouchListener(this)
updateControlsVisibility()
updatePagesSheetButton()
updateRotationButton()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
settings.subscribe(this)
context.contentResolver.registerContentObserver(
Settings.System.CONTENT_URI, true, rotationObserver,
)
}
override fun onDetachedFromWindow() {
settings.unsubscribe(this)
context.contentResolver.unregisterContentObserver(rotationObserver)
super.onDetachedFromWindow()
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_prev -> listener?.switchChapterBy(-1)
R.id.button_next -> listener?.switchChapterBy(1)
R.id.button_save -> listener?.onSavePageClick()
R.id.button_pages_thumbs -> AppRouter.from(this)?.showChapterPagesSheet()
R.id.button_screen_rotation -> listener?.toggleScreenOrientation()
R.id.button_options -> listener?.openMenu()
}
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) {
if (isSliderTracking) {
isSliderChanged = true
} else {
listener?.switchPageTo(value.toInt())
}
}
}
override fun onStartTrackingTouch(slider: Slider) {
isSliderChanged = false
isSliderTracking = true
}
override fun onStopTrackingTouch(slider: Slider) {
isSliderTracking = false
if (isSliderChanged) {
listener?.switchPageTo(slider.value.toInt())
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_READER_CONTROLS -> updateControlsVisibility()
AppSettings.KEY_PAGES_TAB,
AppSettings.KEY_DETAILS_TAB,
AppSettings.KEY_DETAILS_LAST_TAB -> updatePagesSheetButton()
}
}
fun setSliderValue(value: Int, max: Int) {
binding.slider.valueTo = max.toFloat()
binding.slider.setValueRounded(value.toFloat())
}
fun setSliderReversed(reversed: Boolean) {
binding.slider.isRtl = reversed != isRtl
}
private fun updateControlsVisibility() {
val controls = settings.readerControls
binding.buttonPrev.isVisible = ReaderControl.PREV_CHAPTER in controls
binding.buttonNext.isVisible = ReaderControl.NEXT_CHAPTER in controls
binding.buttonPagesThumbs.isVisible = ReaderControl.PAGES_SHEET in controls
binding.buttonScreenRotation.isVisible = ReaderControl.SCREEN_ROTATION in controls
binding.buttonSave.isVisible = ReaderControl.SAVE_PAGE in controls
binding.slider.isVisible = ReaderControl.SLIDER in controls
adjustLayoutParams()
}
private fun updatePagesSheetButton() {
val isPagesMode = settings.defaultDetailsTab == TAB_PAGES
val button = binding.buttonPagesThumbs
button.setIconResource(
if (isPagesMode) R.drawable.ic_grid else R.drawable.ic_list,
)
button.setTitle(
if (isPagesMode) R.string.pages else R.string.chapters,
)
}
private fun adjustLayoutParams() {
val isSliderVisible = binding.slider.isVisible
repeat(childCount) { i ->
val child = getChildAt(i)
if (child is FrameLayout) {
child.updateLayoutParams<LayoutParams> {
width = if (isSliderVisible) LayoutParams.WRAP_CONTENT else 0
weight = if (isSliderVisible) 0f else 1f
}
}
}
}
private fun updateRotationButton() {
val button = binding.buttonScreenRotation
when {
!button.isVisible -> return
isAutoRotationEnabled() -> {
button.setTitle(R.string.lock_screen_rotation)
button.setIconResource(R.drawable.ic_screen_rotation_lock)
}
else -> {
button.setTitle(R.string.rotate_screen)
button.setIconResource(R.drawable.ic_screen_rotation)
}
}
}
private fun Button.initAction() {
setOnClickListener(this@ReaderActionsView)
ViewCompat.setTooltipText(this, contentDescription)
}
private fun Button.setTitle(@StringRes titleResId: Int) {
val text = resources.getString(titleResId)
contentDescription = text
ViewCompat.setTooltipText(this, text)
}
private fun isAutoRotationEnabled(): Boolean = Settings.System.getInt(
context.contentResolver,
Settings.System.ACCELEROMETER_ROTATION,
0,
) == 1
private fun Slider.setThumbVisible(visible: Boolean) {
thumbWidth = if (visible) {
resources.getDimensionPixelSize(materialR.dimen.m3_comp_slider_active_handle_width)
} else {
0
}
thumbHeight = if (visible) {
resources.getDimensionPixelSize(materialR.dimen.m3_comp_slider_active_handle_height)
} else {
0
}
}
}

View File

@@ -10,11 +10,10 @@ import android.view.Gravity
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.ViewGroup
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.MenuHost
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
@@ -35,7 +34,6 @@ import org.koitharu.kotatsu.core.exceptions.resolve.DialogErrorObserver
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderControl
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
@@ -43,11 +41,9 @@ import org.koitharu.kotatsu.core.ui.widgets.ZoomControl
import org.koitharu.kotatsu.core.util.IdlingDetector
import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.isRtl
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.postDelayed
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.zipWithPrevious
import org.koitharu.kotatsu.databinding.ActivityReaderBinding
import org.koitharu.kotatsu.details.ui.pager.pages.PagesSavedObserver
@@ -100,9 +96,6 @@ class ReaderActivity :
scrollTimer.isEnabled = value
}
private val secondaryMenuHost: MenuHost
get() = viewBinding.toolbarBottom ?: this
private lateinit var scrollTimer: ScrollTimer
private lateinit var pageSaveHelper: PageSaveHelper
private lateinit var touchHelper: TapGridDispatcher
@@ -120,13 +113,11 @@ class ReaderActivity :
scrollTimer = scrollTimerFactory.create(this, this)
pageSaveHelper = pageSaveHelperFactory.create(this)
controlDelegate = ReaderControlDelegate(resources, settings, tapGridSettings, this)
viewBinding.slider.setLabelFormatter(PageLabelFormatter())
viewBinding.zoomControl.listener = this
ReaderSliderListener(viewModel, this).attachToSlider(viewBinding.slider)
viewBinding.actionsView.listener = this
idlingDetector.bindToLifecycle(this)
viewBinding.buttonPrev.setOnClickListener(controlDelegate)
viewBinding.buttonNext.setOnClickListener(controlDelegate)
ViewCompat.setOnApplyWindowInsetsListener(viewBinding.root, this)
screenOrientationHelper.applySettings()
viewModel.onError.observeEvent(
this,
@@ -150,17 +141,13 @@ class ReaderActivity :
viewModel.content.observe(this) {
onLoadingStateChanged(viewModel.isLoading.value)
}
viewModel.readerControls.observe(this, ::onReaderControlsChanged)
viewModel.isKeepScreenOnEnabled.observe(this, this::setKeepScreenOn)
viewModel.isInfoBarTransparent.observe(this) { viewBinding.infoBar.drawBackground = !it }
viewModel.isInfoBarEnabled.observe(this, ::onReaderBarChanged)
viewModel.isBookmarkAdded.observe(this, MenuInvalidator(this))
val bottomMenuInvalidator = MenuInvalidator(secondaryMenuHost)
viewModel.isPagesSheetEnabled.observe(this, bottomMenuInvalidator)
screenOrientationHelper.observeAutoOrientation().observe(this, bottomMenuInvalidator)
viewModel.onShowToast.observeEvent(this) { msgId ->
Snackbar.make(viewBinding.container, msgId, Snackbar.LENGTH_SHORT)
.setAnchorView(viewBinding.appbarBottom)
.setAnchorView(viewBinding.toolbarDocked)
.show()
}
viewModel.readerSettings.observe(this) {
@@ -169,10 +156,7 @@ class ReaderActivity :
viewModel.isZoomControlsEnabled.observe(this) {
viewBinding.zoomControl.isVisible = it
}
addMenuProvider(ReaderMenuTopProvider(viewModel))
secondaryMenuHost.addMenuProvider(
ReaderMenuBottomProvider(this, readerManager, screenOrientationHelper, this, viewModel),
)
addMenuProvider(ReaderMenuProvider(viewModel))
}
override fun getParentActivityIntent(): Intent? {
@@ -215,7 +199,7 @@ class ReaderActivity :
if (viewBinding.appbarTop.isVisible) {
lifecycle.postDelayed(TimeUnit.SECONDS.toMillis(1), hideUiRunnable)
}
viewBinding.slider.isRtl = mode == ReaderMode.REVERSED
viewBinding.actionsView.setSliderReversed(mode == ReaderMode.REVERSED)
}
private fun onLoadingStateChanged(isLoading: Boolean) {
@@ -226,7 +210,6 @@ class ReaderActivity :
} else {
viewBinding.toastView.hide()
}
secondaryMenuHost.invalidateMenu()
invalidateOptionsMenu()
}
@@ -247,7 +230,7 @@ class ReaderActivity :
rawX >= viewBinding.root.width - gestureInsets.right ||
rawY >= viewBinding.root.height - gestureInsets.bottom ||
viewBinding.appbarTop.hasGlobalPoint(rawX, rawY) ||
viewBinding.appbarBottom?.hasGlobalPoint(rawX, rawY) == true
viewBinding.toolbarDocked?.hasGlobalPoint(rawX, rawY) == true
) {
false
} else {
@@ -263,7 +246,7 @@ class ReaderActivity :
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return controlDelegate.onKeyDown(keyCode) || super.onKeyDown(keyCode, event)
return controlDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
@@ -307,13 +290,6 @@ class ReaderActivity :
}
}
private fun onReaderControlsChanged(controls: Set<ReaderControl>) = with(viewBinding) {
buttonPrev.isVisible = ReaderControl.PREV_CHAPTER in controls
buttonNext.isVisible = ReaderControl.NEXT_CHAPTER in controls
slider.isVisible = ReaderControl.SLIDER in controls
secondaryMenuHost.invalidateMenu()
}
private fun setUiIsVisible(isUiVisible: Boolean) {
if (viewBinding.appbarTop.isVisible != isUiVisible) {
if (isAnimationsEnabled) {
@@ -321,12 +297,12 @@ class ReaderActivity :
.setOrdering(TransitionSet.ORDERING_TOGETHER)
.addTransition(Slide(Gravity.TOP).addTarget(viewBinding.appbarTop))
.addTransition(Fade().addTarget(viewBinding.infoBar))
.addTransition(Slide(Gravity.BOTTOM).addTarget(viewBinding.appbarBottom))
.addTransition(Slide(Gravity.BOTTOM).addTarget(viewBinding.toolbarDocked))
TransitionManager.beginDelayedTransition(viewBinding.root, transition)
}
val isFullscreen = settings.isReaderFullscreenEnabled
viewBinding.appbarTop.isVisible = isUiVisible
viewBinding.appbarBottom?.isVisible = isUiVisible
viewBinding.toolbarDocked?.isVisible = isUiVisible
viewBinding.infoBar.isGone = isUiVisible || (!viewModel.isInfoBarEnabled.value)
viewBinding.infoBar.isTimeVisible = isFullscreen
systemUiController.setSystemUiVisible(isUiVisible || !isFullscreen)
@@ -341,10 +317,12 @@ class ReaderActivity :
right = systemBars.right,
left = systemBars.left,
)
viewBinding.appbarBottom?.updateLayoutParams<MarginLayoutParams> {
bottomMargin = systemBars.bottom + topMargin
rightMargin = systemBars.right + topMargin
leftMargin = systemBars.left + topMargin
if (viewBinding.toolbarDocked != null) {
viewBinding.actionsView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = systemBars.bottom
rightMargin = systemBars.right
leftMargin = systemBars.left
}
}
viewBinding.infoBar.updatePadding(
top = systemBars.top,
@@ -385,6 +363,28 @@ class ReaderActivity :
viewModel.saveCurrentPage(pageSaveHelper)
}
override fun toggleScreenOrientation() {
if (screenOrientationHelper.toggleScreenOrientation()) {
Snackbar.make(
viewBinding.container,
if (screenOrientationHelper.isLocked) {
R.string.screen_rotation_locked
} else {
R.string.screen_rotation_unlocked
},
Snackbar.LENGTH_SHORT,
).setAnchorView(viewBinding.toolbarDocked)
.show()
}
}
override fun switchPageTo(index: Int) {
val pages = viewModel.getCurrentChapterPages()
val page = pages?.getOrNull(index) ?: return
val chapterId = viewModel.getCurrentState()?.chapterId ?: return
onPageSelected(ReaderPage(page, index, chapterId))
}
private fun onReaderBarChanged(isBarEnabled: Boolean) {
viewBinding.infoBar.isVisible = isBarEnabled && viewBinding.appbarTop.isGone
}
@@ -395,27 +395,29 @@ class ReaderActivity :
viewBinding.infoBar.update(uiState)
if (uiState == null) {
supportActionBar?.subtitle = null
viewBinding.layoutSlider.isVisible = false
viewBinding.actionsView.setSliderValue(0, 1)
viewBinding.actionsView.isSliderEnabled = false
return
}
val chapterTitle = uiState.getChapterTitle(resources)
supportActionBar?.subtitle = when {
uiState.incognito -> getString(R.string.incognito_mode)
else -> uiState.chapterName
else -> chapterTitle
}
if (uiState.chapterName != previous?.chapterName && !uiState.chapterName.isNullOrEmpty()) {
viewBinding.toastView.showTemporary(uiState.chapterName, TOAST_DURATION)
if (chapterTitle != previous?.getChapterTitle(resources) && chapterTitle.isNotEmpty()) {
viewBinding.toastView.showTemporary(chapterTitle, TOAST_DURATION)
}
if (uiState.isSliderAvailable()) {
viewBinding.slider.valueTo = uiState.totalPages.toFloat() - 1
viewBinding.slider.setValueRounded(uiState.currentPage.toFloat())
viewBinding.actionsView.setSliderValue(
value = uiState.currentPage,
max = uiState.totalPages - 1,
)
} else {
viewBinding.slider.valueTo = 1f
viewBinding.slider.value = 0f
viewBinding.actionsView.setSliderValue(0, 1)
}
viewBinding.slider.isEnabled = uiState.isSliderAvailable()
viewBinding.buttonNext.isEnabled = uiState.hasNextChapter()
viewBinding.buttonPrev.isEnabled = uiState.hasPreviousChapter()
viewBinding.layoutSlider.isVisible = true
viewBinding.actionsView.isSliderEnabled = uiState.isSliderAvailable()
viewBinding.actionsView.isNextEnabled = uiState.hasNextChapter()
viewBinding.actionsView.isPrevEnabled = uiState.hasPreviousChapter()
}
companion object {

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.reader.data.TapGridSettings
import org.koitharu.kotatsu.reader.domain.TapGridArea
import org.koitharu.kotatsu.reader.ui.tapgrid.TapAction
import kotlin.math.sign
class ReaderControlDelegate(
resources: Resources,
@@ -43,77 +44,48 @@ class ReaderControlDelegate(
processAction(action)
}
fun onKeyDown(keyCode: Int): Boolean = when (keyCode) {
fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_NAVIGATE_NEXT,
KeyEvent.KEYCODE_SPACE -> switchBy(1, event, false)
KeyEvent.KEYCODE_R -> {
listener.switchPageBy(1)
true
}
KeyEvent.KEYCODE_PAGE_DOWN -> switchBy(1, event, false)
KeyEvent.KEYCODE_L -> {
listener.switchPageBy(-1)
true
}
KeyEvent.KEYCODE_VOLUME_UP -> if (settings.isReaderVolumeButtonsEnabled) {
listener.switchPageBy(-1)
true
} else {
false
}
KeyEvent.KEYCODE_NAVIGATE_PREVIOUS -> switchBy(-1, event, false)
KeyEvent.KEYCODE_PAGE_UP -> switchBy(-1, event, false)
KeyEvent.KEYCODE_VOLUME_DOWN -> if (settings.isReaderVolumeButtonsEnabled) {
listener.switchPageBy(1)
true
} else {
false
}
KeyEvent.KEYCODE_R -> switchBy(1, null, false)
KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_PAGE_DOWN,
-> {
listener.switchPageBy(1)
true
}
KeyEvent.KEYCODE_L -> switchBy(-1, null, false)
KeyEvent.KEYCODE_DPAD_RIGHT -> {
listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1)
true
}
KeyEvent.KEYCODE_PAGE_UP,
-> {
listener.switchPageBy(-1)
true
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1)
true
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
listener.toggleUiVisibility()
true
}
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP -> {
if (!listener.scrollBy(-minScrollDelta, smooth = true)) {
listener.switchPageBy(-1)
KeyEvent.KEYCODE_VOLUME_UP -> if (settings.isReaderVolumeButtonsEnabled) {
switchBy(-1, event, false)
} else {
return false
}
true
}
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (!listener.scrollBy(minScrollDelta, smooth = true)) {
listener.switchPageBy(1)
KeyEvent.KEYCODE_VOLUME_DOWN -> if (settings.isReaderVolumeButtonsEnabled) {
switchBy(1, event, false)
} else {
return false
}
true
}
else -> false
KeyEvent.KEYCODE_DPAD_RIGHT -> switchByRelative(-1, event)
KeyEvent.KEYCODE_DPAD_LEFT -> switchByRelative(1, event)
KeyEvent.KEYCODE_DPAD_CENTER -> listener.toggleUiVisibility()
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP,
KeyEvent.KEYCODE_DPAD_UP -> switchBy(-1, event, true)
KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN,
KeyEvent.KEYCODE_DPAD_DOWN -> switchBy(1, event, true)
else -> return false
}
return true
}
fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean {
@@ -136,12 +108,30 @@ class ReaderControlDelegate(
return settings.isReaderControlAlwaysLTR && listener.readerMode == ReaderMode.REVERSED
}
private fun switchBy(delta: Int, event: KeyEvent?, scroll: Boolean) {
if (event?.isCtrlPressed == true) {
listener.switchChapterBy(delta)
} else if (scroll) {
if (!listener.scrollBy(minScrollDelta * delta.sign, smooth = true)) {
listener.switchPageBy(delta)
}
} else {
listener.switchPageBy(delta)
}
}
private fun switchByRelative(delta: Int, event: KeyEvent?) {
return switchBy(if (isReaderTapsReversed()) -delta else delta, event, scroll = false)
}
interface OnInteractionListener {
val readerMode: ReaderMode?
fun switchPageBy(delta: Int)
fun switchPageTo(index: Int)
fun switchChapterBy(delta: Int)
fun scrollBy(delta: Int, smooth: Boolean): Boolean
@@ -150,6 +140,10 @@ class ReaderControlDelegate(
fun openMenu()
fun onSavePageClick()
fun toggleScreenOrientation()
fun isReaderResumed(): Boolean
}
}

View File

@@ -1,111 +0,0 @@
package org.koitharu.kotatsu.reader.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import androidx.core.view.MenuProvider
import androidx.fragment.app.FragmentActivity
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.prefs.ReaderControl
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
class ReaderMenuBottomProvider(
private val activity: FragmentActivity,
private val readerManager: ReaderManager,
private val screenOrientationHelper: ScreenOrientationHelper,
private val configCallback: ReaderConfigSheet.Callback,
private val viewModel: ReaderViewModel,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_reader_bottom, menu)
onPrepareMenu(menu) // fix, not called in toolbar
}
override fun onPrepareMenu(menu: Menu) {
val readerControls = viewModel.readerControls.value
val hasPages = viewModel.content.value.pages.isNotEmpty()
val isPagesSheetEnabled = hasPages && ReaderControl.PAGES_SHEET in readerControls
menu.findItem(R.id.action_pages_thumbs).run {
isVisible = isPagesSheetEnabled
if (isPagesSheetEnabled) {
setIcon(if (viewModel.isPagesSheetEnabled.value) R.drawable.ic_grid else R.drawable.ic_list)
}
}
menu.findItem(R.id.action_screen_rotation).run {
isVisible = ReaderControl.SCREEN_ROTATION in readerControls
when {
!isVisible -> Unit
!screenOrientationHelper.isAutoRotationEnabled -> {
setTitle(R.string.rotate_screen)
setIcon(R.drawable.ic_screen_rotation)
}
else -> {
setTitle(R.string.lock_screen_rotation)
setIcon(R.drawable.ic_screen_rotation_lock)
}
}
}
menu.findItem(R.id.action_save_page)?.run {
isVisible = hasPages && ReaderControl.SAVE_PAGE in readerControls
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_screen_rotation -> {
toggleScreenRotation()
true
}
R.id.action_save_page -> {
configCallback.onSavePageClick()
true
}
R.id.action_pages_thumbs -> {
activity.router.showChapterPagesSheet()
true
}
R.id.action_options -> {
viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState())
val currentMode = readerManager.currentMode ?: return false
activity.router.showReaderConfigSheet(currentMode)
true
}
R.id.action_bookmark -> {
if (viewModel.isBookmarkAdded.value) {
viewModel.removeBookmark()
} else {
viewModel.addBookmark()
}
true
}
else -> false
}
}
private fun toggleScreenRotation() = with(screenOrientationHelper) {
if (isAutoRotationEnabled) {
val newValue = !isLocked
isLocked = newValue
Toast.makeText(
activity,
if (newValue) {
R.string.screen_rotation_locked
} else {
R.string.screen_rotation_unlocked
},
Toast.LENGTH_SHORT,
).show()
} else {
isLandscape = !isLandscape
}
}
}

View File

@@ -6,12 +6,12 @@ import android.view.MenuItem
import androidx.core.view.MenuProvider
import org.koitharu.kotatsu.R
class ReaderMenuTopProvider(
class ReaderMenuProvider(
private val viewModel: ReaderViewModel,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_reader_top, menu)
menuInflater.inflate(R.menu.opt_reader, menu)
}
override fun onPrepareMenu(menu: Menu) {

View File

@@ -1,47 +0,0 @@
package org.koitharu.kotatsu.reader.ui
import com.google.android.material.slider.Slider
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
class ReaderSliderListener(
private val viewModel: ReaderViewModel,
private val callback: ReaderNavigationCallback,
) : Slider.OnChangeListener, Slider.OnSliderTouchListener {
private var isChanged = false
private var isTracking = false
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) {
if (isTracking) {
isChanged = true
} else {
switchPageToIndex(value.toInt())
}
}
}
override fun onStartTrackingTouch(slider: Slider) {
isChanged = false
isTracking = true
}
override fun onStopTrackingTouch(slider: Slider) {
isTracking = false
if (isChanged) {
switchPageToIndex(slider.value.toInt())
}
}
fun attachToSlider(slider: Slider) {
slider.addOnChangeListener(this)
slider.addOnSliderTouchListener(this)
}
private fun switchPageToIndex(index: Int) {
val pages = viewModel.getCurrentChapterPages()
val page = pages?.getOrNull(index) ?: return
val chapterId = viewModel.getCurrentState()?.chapterId ?: return
callback.onPageSelected(ReaderPage(page, index, chapterId))
}
}

View File

@@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
@@ -43,7 +42,6 @@ import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.history.data.HistoryRepository
@@ -120,8 +118,6 @@ class ReaderViewModel @Inject constructor(
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
}
val isPagesSheetEnabled = observeIsPagesSheetEnabled()
val content = MutableStateFlow(ReaderContent(emptyList(), null))
val pageAnimation = settings.observeAsStateFlow(
@@ -136,12 +132,6 @@ class ReaderViewModel @Inject constructor(
valueProducer = { isReaderBarEnabled },
)
val readerControls = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_READER_CONTROLS,
valueProducer = { readerControls },
)
val isInfoBarTransparent = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_READER_BAR_TRANSPARENT,
@@ -449,9 +439,8 @@ class ReaderViewModel @Inject constructor(
val chapterIndex = m.chapters[chapter.branch]?.indexOfFirst { it.id == chapter.id } ?: -1
val newState = ReaderUiState(
mangaName = m.toManga().title,
branch = chapter.branch,
chapterName = chapter.name,
chapterNumber = chapterIndex + 1,
chapter = chapter,
chapterIndex = chapterIndex,
chaptersTotal = m.chapters[chapter.branch].sizeOrZero(),
totalPages = chaptersLoader.getPagesCount(chapter.id),
currentPage = state.page,
@@ -493,11 +482,6 @@ class ReaderViewModel @Inject constructor(
valueProducer = { isReaderZoomButtonsEnabled },
)
private fun observeIsPagesSheetEnabled() = settings.observe()
.filter { it == AppSettings.KEY_PAGES_TAB || it == AppSettings.KEY_DETAILS_TAB || it == AppSettings.KEY_DETAILS_LAST_TAB }
.map { settings.defaultDetailsTab == TAB_PAGES }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.defaultDetailsTab == TAB_PAGES)
private suspend fun getStateFromIntent(manga: Manga): ReaderState {
val history = historyRepository.getOne(manga)
val preselectedBranch = selectedBranch.value

View File

@@ -19,7 +19,7 @@ import javax.inject.Inject
@ActivityScoped
class ScreenOrientationHelper @Inject constructor(
private val activity: Activity,
settings: AppSettings,
private val settings: AppSettings,
) {
val isAutoRotationEnabled: Boolean
@@ -49,7 +49,7 @@ class ScreenOrientationHelper @Inject constructor(
}
}
init {
fun applySettings() {
if (activity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
// https://developer.android.com/reference/android/R.attr.html#screenOrientation
activity.requestedOrientation = settings.readerScreenOrientation
@@ -72,4 +72,13 @@ class ScreenOrientationHelper @Inject constructor(
emit(isAutoRotationEnabled)
}.distinctUntilChanged()
.conflate()
fun toggleScreenOrientation(): Boolean = if (isAutoRotationEnabled) {
val newValue = !isLocked
isLocked = newValue
true
} else {
isLandscape = !isLandscape
false
}
}

View File

@@ -1,10 +1,13 @@
package org.koitharu.kotatsu.reader.ui.pager
import android.content.res.Resources
import org.koitharu.kotatsu.core.model.getLocalizedTitle
import org.koitharu.kotatsu.parsers.model.MangaChapter
data class ReaderUiState(
val mangaName: String?,
val branch: String?,
val chapterName: String?,
val chapterNumber: Int,
val chapter: MangaChapter,
val chapterIndex: Int,
val chaptersTotal: Int,
val currentPage: Int,
val totalPages: Int,
@@ -12,9 +15,14 @@ data class ReaderUiState(
val incognito: Boolean,
) {
val chapterNumber: Int
get() = chapterIndex + 1
fun hasNextChapter(): Boolean = chapterNumber < chaptersTotal
fun hasPreviousChapter(): Boolean = chapterNumber > 1
fun hasPreviousChapter(): Boolean = chapterIndex > 0
fun isSliderAvailable(): Boolean = totalPages > 1 && currentPage < totalPages
fun getChapterTitle(resources: Resources) = chapter.getLocalizedTitle(resources, chapterIndex)
}

View File

@@ -49,45 +49,13 @@
android:elevation="@dimen/m3_card_elevated_elevation"
app:elevation="@dimen/m3_card_elevated_elevation"
app:popupTheme="@style/ThemeOverlay.Kotatsu"
tools:menu="@menu/opt_reader_top">
tools:menu="@menu/opt_reader">
<LinearLayout
android:id="@+id/layout_slider"
android:layout_width="wrap_content"
<org.koitharu.kotatsu.reader.ui.ReaderActionsView
android:id="@+id/actionsView"
android:layout_width="400dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="2dp"
android:gravity="center_vertical|end">
<ImageButton
android:id="@+id/button_prev"
style="?actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/prev_chapter"
android:src="@drawable/ic_prev"
android:tooltipText="@string/prev_chapter" />
<com.google.android.material.slider.Slider
android:id="@+id/slider"
android:layout_width="260dp"
android:layout_height="wrap_content"
android:stepSize="1.0"
android:valueFrom="0"
app:labelBehavior="floating"
tools:value="6"
tools:valueTo="20" />
<ImageButton
android:id="@+id/button_next"
style="?actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/next_chapter"
android:src="@drawable/ic_next"
android:tooltipText="@string/next_chapter" />
</LinearLayout>
android:layout_gravity="center_vertical|end" />
</com.google.android.material.appbar.MaterialToolbar>

View File

@@ -49,68 +49,26 @@
android:elevation="@dimen/m3_card_elevated_elevation"
app:elevation="@dimen/m3_card_elevated_elevation"
app:popupTheme="@style/ThemeOverlay.Kotatsu"
tools:menu="@menu/opt_reader_top" />
tools:menu="@menu/opt_reader" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/appbar_bottom"
style="?materialCardViewElevatedStyle"
<com.google.android.material.dockedtoolbar.DockedToolbarLayout
android:id="@+id/toolbar_docked"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="8dp"
android:fitsSystemWindows="false"
app:cardBackgroundColor="?colorSurfaceContainer"
app:layout_insetEdge="bottom">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_bottom"
<org.koitharu.kotatsu.reader.ui.ReaderActionsView
android:id="@+id/actionsView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:menu="@menu/opt_reader_bottom">
android:minHeight="?actionBarSize" />
<LinearLayout
android:id="@+id/layout_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="2dp"
android:gravity="center_vertical|end">
<ImageButton
android:id="@+id/button_prev"
style="?actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/prev_chapter"
android:src="@drawable/ic_prev"
android:tooltipText="@string/prev_chapter" />
<com.google.android.material.slider.Slider
android:id="@+id/slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:stepSize="1.0"
android:valueFrom="0"
app:labelBehavior="floating"
tools:value="6"
tools:valueTo="20" />
<ImageButton
android:id="@+id/button_next"
style="?actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/next_chapter"
android:src="@drawable/ic_next"
android:tooltipText="@string/next_chapter" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.card.MaterialCardView>
</com.google.android.material.dockedtoolbar.DockedToolbarLayout>
<org.koitharu.kotatsu.reader.ui.ReaderToastView
android:id="@+id/toastView"

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
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"
tools:layout_height="wrap_content"
tools:layout_width="match_parent"
tools:orientation="horizontal"
tools:parentTag="android.widget.LinearLayout">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_prev"
style="@style/Widget.Kotatsu.IconButton.Action"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/prev_chapter"
app:icon="@drawable/ic_prev" />
</FrameLayout>
<com.google.android.material.slider.Slider
android:id="@+id/slider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:stepSize="1.0"
android:valueFrom="0"
android:visibility="visible"
app:labelBehavior="floating"
tools:value="6"
tools:valueTo="20" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_next"
style="@style/Widget.Kotatsu.IconButton.Action"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/next_chapter"
app:icon="@drawable/ic_next" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_save"
style="@style/Widget.Kotatsu.IconButton.Action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/save_page"
app:icon="@drawable/ic_save" />
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_screen_rotation"
style="@style/Widget.Kotatsu.IconButton.Action"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/screen_orientation"
app:icon="@drawable/ic_screen_rotation" />
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_pages_thumbs"
style="@style/Widget.Kotatsu.IconButton.Action"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/pages"
app:icon="@drawable/ic_grid" />
</FrameLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_options"
style="@style/Widget.Kotatsu.IconButton.Action"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/options"
app:icon="@drawable/abc_ic_menu_overflow_material" />
</FrameLayout>
</merge>

View File

@@ -1,39 +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"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/action_save_page"
android:icon="@drawable/ic_save"
android:title="@string/save_page"
android:visible="false"
app:showAsAction="always"
tools:visible="true" />
<item
android:id="@+id/action_screen_rotation"
android:icon="@drawable/ic_screen_rotation"
android:title="@string/screen_orientation"
android:visible="false"
app:showAsAction="always"
tools:visible="true" />
<item
android:id="@+id/action_pages_thumbs"
android:icon="@drawable/ic_grid"
android:title="@string/pages"
android:visible="false"
app:showAsAction="always"
tools:visible="true" />
<item
android:id="@+id/action_options"
android:icon="@drawable/abc_ic_menu_overflow_material"
android:title="@string/options"
app:showAsAction="always"
tools:visible="true" />
</menu>

View File

@@ -111,6 +111,10 @@
<item name="android:minHeight">42dp</item>
</style>
<style name="Widget.Kotatsu.IconButton.Action" parent="Widget.Material3.Button.IconButton">
<item name="iconTint">?colorControlNormal</item>
</style>
<style name="Widget.Kotatsu.ToggleButton" parent="Widget.Material3.Button.OutlinedButton">
<item name="android:checkable">true</item>
<item name="android:textAlignment">textStart</item>