From 09590cfab0bb75939cb64cd9353868f12b82c25a Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 3 Mar 2025 14:03:47 +0200 Subject: [PATCH] Update reader actions bar --- .../org/koitharu/kotatsu/core/model/Manga.kt | 10 +- .../kotatsu/details/data/MangaDetails.kt | 2 +- .../details/ui/model/ChapterListItem.kt | 1 - .../kotatsu/reader/ui/ReaderActionsView.kt | 239 ++++++++++++++++++ .../kotatsu/reader/ui/ReaderActivity.kt | 102 ++++---- .../reader/ui/ReaderControlDelegate.kt | 116 ++++----- .../reader/ui/ReaderMenuBottomProvider.kt | 111 -------- ...nuTopProvider.kt => ReaderMenuProvider.kt} | 4 +- .../kotatsu/reader/ui/ReaderSliderListener.kt | 47 ---- .../kotatsu/reader/ui/ReaderViewModel.kt | 20 +- .../reader/ui/ScreenOrientationHelper.kt | 13 +- .../kotatsu/reader/ui/pager/ReaderUiState.kt | 16 +- .../layout-w600dp-land/activity_reader.xml | 42 +-- app/src/main/res/layout/activity_reader.xml | 58 +---- .../main/res/layout/layout_reader_actions.xml | 101 ++++++++ .../{opt_reader_top.xml => opt_reader.xml} | 0 app/src/main/res/menu/opt_reader_bottom.xml | 39 --- app/src/main/res/values/styles.xml | 4 + 18 files changed, 500 insertions(+), 425 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActionsView.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuBottomProvider.kt rename app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/{ReaderMenuTopProvider.kt => ReaderMenuProvider.kt} (93%) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt create mode 100644 app/src/main/res/layout/layout_reader_actions.xml rename app/src/main/res/menu/{opt_reader_top.xml => opt_reader.xml} (100%) delete mode 100644 app/src/main/res/menu/opt_reader_bottom.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 7ef2c4702..9afce24e4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -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) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/data/MangaDetails.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/data/MangaDetails.kt index 45a4cad93..8e7f25864 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/data/MangaDetails.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/data/MangaDetails.kt @@ -33,7 +33,7 @@ data class MangaDetails( val coverUrl: String? get() = manga.largeCoverUrl - .ifNullOrEmpty { manga.largeCoverUrl } + .ifNullOrEmpty { manga.coverUrl } .ifNullOrEmpty { localManga?.manga?.coverUrl } ?.nullIfEmpty() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt index 14dd9f0aa..5b3a09fb7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/model/ChapterListItem.kt @@ -69,7 +69,6 @@ data class ChapterListItem( } } - private fun buildDescription(): String { val joiner = StringJoiner(" • ") chapter.numberString()?.let { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActionsView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActionsView.kt new file mode 100644 index 000000000..c748eaab2 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActionsView.kt @@ -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 { + 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 + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt index beea52cb3..e81eb0823 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActivity.kt @@ -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) = 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 { - bottomMargin = systemBars.bottom + topMargin - rightMargin = systemBars.right + topMargin - leftMargin = systemBars.left + topMargin + if (viewBinding.toolbarDocked != null) { + viewBinding.actionsView.updateLayoutParams { + 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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt index 2076a50b5..4827a7a40 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderControlDelegate.kt @@ -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 } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuBottomProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuBottomProvider.kt deleted file mode 100644 index ace7fa17d..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuBottomProvider.kt +++ /dev/null @@ -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 - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuTopProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuProvider.kt similarity index 93% rename from app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuTopProvider.kt rename to app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuProvider.kt index aa66de0eb..04a3b4522 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuTopProvider.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderMenuProvider.kt @@ -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) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt deleted file mode 100644 index 5bfc74953..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderSliderListener.kt +++ /dev/null @@ -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)) - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 00dc714ea..2610754db 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ScreenOrientationHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ScreenOrientationHelper.kt index 6dc370090..620671d8b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ScreenOrientationHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ScreenOrientationHelper.kt @@ -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 + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt index 151a3ee0c..b62236cbf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/pager/ReaderUiState.kt @@ -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) } diff --git a/app/src/main/res/layout-w600dp-land/activity_reader.xml b/app/src/main/res/layout-w600dp-land/activity_reader.xml index cbfbe99be..cf4959497 100644 --- a/app/src/main/res/layout-w600dp-land/activity_reader.xml +++ b/app/src/main/res/layout-w600dp-land/activity_reader.xml @@ -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"> - - - - - - - - - + android:layout_gravity="center_vertical|end" /> diff --git a/app/src/main/res/layout/activity_reader.xml b/app/src/main/res/layout/activity_reader.xml index 0a8fdf3da..7eda5b92c 100644 --- a/app/src/main/res/layout/activity_reader.xml +++ b/app/src/main/res/layout/activity_reader.xml @@ -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" /> - - + android:minHeight="?actionBarSize" /> - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/opt_reader_top.xml b/app/src/main/res/menu/opt_reader.xml similarity index 100% rename from app/src/main/res/menu/opt_reader_top.xml rename to app/src/main/res/menu/opt_reader.xml diff --git a/app/src/main/res/menu/opt_reader_bottom.xml b/app/src/main/res/menu/opt_reader_bottom.xml deleted file mode 100644 index 76f1ae34f..000000000 --- a/app/src/main/res/menu/opt_reader_bottom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f0683e1ba..c8b86d4b6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -111,6 +111,10 @@ 42dp + +