diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84121cf11..c22072304 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -145,6 +145,9 @@ + diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index d87ffe78d..ac50c83e7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -101,15 +101,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { } } - val readerPageSwitch: Set - get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS) + val isReaderVolumeButtonsEnabled: Boolean + get() = prefs.getBoolean(KEY_READER_VOLUME_BUTTONS, false) val isReaderZoomButtonsEnabled: Boolean get() = prefs.getBoolean(KEY_READER_ZOOM_BUTTONS, false) - val isReaderTapsAdaptive: Boolean - get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false) - val isReaderOptimizationEnabled: Boolean get() = prefs.getBoolean(KEY_READER_OPTIMIZE, false) @@ -453,7 +450,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { companion object { - const val PAGE_SWITCH_TAPS = "taps" const val PAGE_SWITCH_VOLUME_KEYS = "volume" const val TRACK_HISTORY = "history" @@ -476,8 +472,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_GRID_SIZE = "grid_size" const val KEY_REMOTE_SOURCES = "remote_sources" const val KEY_LOCAL_STORAGE = "local_storage" - const val KEY_READER_SWITCHERS = "reader_switchers" const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons" + const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons" const val KEY_TRACKER_ENABLED = "tracker_enabled" const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi" const val KEY_TRACK_SOURCES = "track_sources" @@ -530,7 +526,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_READER_BACKGROUND = "reader_background" const val KEY_READER_SCREEN_ON = "reader_screen_on" const val KEY_SHORTCUTS = "dynamic_shortcuts" - const val KEY_READER_TAPS_LTR = "reader_taps_ltr" + const val KEY_READER_TAP_ACTIONS = "reader_tap_actions" const val KEY_READER_OPTIMIZE = "reader_optimize" const val KEY_LOCAL_LIST_ORDER = "local_order" const val KEY_HISTORY_ORDER = "history_order" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/GridTouchHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/GridTouchHelper.kt deleted file mode 100644 index a5b49a0c0..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/GridTouchHelper.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.koitharu.kotatsu.core.util - -import android.content.Context -import android.view.GestureDetector -import android.view.MotionEvent -import kotlin.math.roundToInt - -class GridTouchHelper( - context: Context, - private val listener: OnGridTouchListener, -) : GestureDetector.SimpleOnGestureListener() { - - private val detector = GestureDetector(context, this) - private val width = context.resources.displayMetrics.widthPixels - private val height = context.resources.displayMetrics.heightPixels - private var isDispatching = false - - init { - detector.setIsLongpressEnabled(true) - detector.setOnDoubleTapListener(this) - } - - fun dispatchTouchEvent(event: MotionEvent) { - if (event.actionMasked == MotionEvent.ACTION_DOWN) { - isDispatching = listener.onProcessTouch(event.rawX.toInt(), event.rawY.toInt()) - } - detector.onTouchEvent(event) - } - - override fun onSingleTapConfirmed(event: MotionEvent): Boolean { - if (!isDispatching) { - return true - } - val xIndex = (event.rawX * 2f / width).roundToInt() - val yIndex = (event.rawY * 2f / height).roundToInt() - listener.onGridTouch( - when (xIndex) { - 0 -> AREA_LEFT - 1 -> { - when (yIndex) { - 0 -> AREA_TOP - 1 -> AREA_CENTER - 2 -> AREA_BOTTOM - else -> return false - } - } - - 2 -> AREA_RIGHT - else -> return false - }, - ) - return true - } - - override fun onLongPress(event: MotionEvent) { - super.onLongPress(event) - val xIndex = (event.rawX * 2f / width).roundToInt() - val yIndex = (event.rawY * 2f / height).roundToInt() - listener.onGridLongTouch( - when(xIndex) { - 1 -> { - when (yIndex) { - 1 -> AREA_CENTER - else -> -1 - } - } - else -> -1 - } - ) - } - - companion object { - - const val AREA_CENTER = 1 - const val AREA_LEFT = 2 - const val AREA_RIGHT = 3 - const val AREA_TOP = 4 - const val AREA_BOTTOM = 5 - } - - interface OnGridTouchListener { - - fun onGridTouch(area: Int) - - fun onGridLongTouch(area: Int) - - fun onProcessTouch(rawX: Int, rawY: Int): Boolean - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt new file mode 100644 index 000000000..d26b9fb7b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/data/TapGridSettings.kt @@ -0,0 +1,53 @@ +package org.koitharu.kotatsu.reader.data + +import android.content.Context +import androidx.core.content.edit +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import org.koitharu.kotatsu.core.util.ext.getEnumValue +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.core.util.ext.putEnumValue +import org.koitharu.kotatsu.reader.domain.TapGridArea +import org.koitharu.kotatsu.reader.ui.tapgrid.TapAction +import javax.inject.Inject + +class TapGridSettings @Inject constructor(@ApplicationContext context: Context) { + + private val prefs = context.getSharedPreferences("tap_grid", Context.MODE_PRIVATE) + + fun getTapAction(area: TapGridArea, isLongTap: Boolean): TapAction? { + val key = getPrefKey(area, isLongTap) + return if (!isLongTap && key !in prefs) { + getDefaultTapAction(area) + } else { + prefs.getEnumValue(key, TapAction::class.java) + } + } + + fun setTapAction(area: TapGridArea, isLongTap: Boolean, action: TapAction?) { + val key = getPrefKey(area, isLongTap) + prefs.edit { putEnumValue(key, action) } + } + + fun observe() = prefs.observe().flowOn(Dispatchers.IO) + + private fun getPrefKey(area: TapGridArea, isLongTap: Boolean): String = if (isLongTap) { + area.name + "_long" + } else { + area.name + } + + private fun getDefaultTapAction(area: TapGridArea): TapAction = when (area) { + TapGridArea.TOP_LEFT, + TapGridArea.TOP_CENTER, + TapGridArea.CENTER_LEFT, + TapGridArea.BOTTOM_LEFT -> TapAction.PAGE_PREV + + TapGridArea.CENTER -> TapAction.TOGGLE_UI + TapGridArea.TOP_RIGHT, + TapGridArea.CENTER_RIGHT, + TapGridArea.BOTTOM_CENTER, + TapGridArea.BOTTOM_RIGHT -> TapAction.PAGE_NEXT + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/TapGridArea.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/TapGridArea.kt new file mode 100644 index 000000000..4d51f36e0 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/TapGridArea.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.reader.domain + +enum class TapGridArea { + + TOP_LEFT, + TOP_CENTER, + TOP_RIGHT, + CENTER_LEFT, + CENTER, + CENTER_RIGHT, + BOTTOM_LEFT, + BOTTOM_CENTER, + BOTTOM_RIGHT; +} 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 c80aacf81..7c444d428 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 @@ -24,7 +24,6 @@ import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers @@ -41,10 +40,8 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.ui.BaseFullscreenActivity import org.koitharu.kotatsu.core.ui.util.MenuInvalidator import org.koitharu.kotatsu.core.ui.widgets.ZoomControl -import org.koitharu.kotatsu.core.util.GridTouchHelper import org.koitharu.kotatsu.core.util.IdlingDetector import org.koitharu.kotatsu.core.util.ShareHelper -import org.koitharu.kotatsu.core.util.ext.DIALOG_THEME_CENTERED import org.koitharu.kotatsu.core.util.ext.hasGlobalPoint import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled import org.koitharu.kotatsu.core.util.ext.isRtl @@ -57,9 +54,12 @@ import org.koitharu.kotatsu.databinding.ActivityReaderBinding import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.reader.data.TapGridSettings +import org.koitharu.kotatsu.reader.domain.TapGridArea import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState +import org.koitharu.kotatsu.reader.ui.tapgrid.TapGridDispatcher import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -68,7 +68,7 @@ import javax.inject.Inject class ReaderActivity : BaseFullscreenActivity(), ChaptersSheet.OnChapterChangeListener, - GridTouchHelper.OnGridTouchListener, + TapGridDispatcher.OnGridTouchListener, OnPageSelectListener, ReaderConfigSheet.Callback, ReaderControlDelegate.OnInteractionListener, @@ -79,6 +79,9 @@ class ReaderActivity : @Inject lateinit var settings: AppSettings + @Inject + lateinit var tapGridSettings: TapGridSettings + private val idlingDetector = IdlingDetector(TimeUnit.SECONDS.toMillis(10), this) private val viewModel: ReaderViewModel by viewModels() @@ -96,7 +99,7 @@ class ReaderActivity : lateinit var scrollTimerFactory: ScrollTimer.Factory private lateinit var scrollTimer: ScrollTimer - private lateinit var touchHelper: GridTouchHelper + private lateinit var touchHelper: TapGridDispatcher private lateinit var controlDelegate: ReaderControlDelegate private var gestureInsets: Insets = Insets.NONE private lateinit var readerManager: ReaderManager @@ -107,9 +110,9 @@ class ReaderActivity : setContentView(ActivityReaderBinding.inflate(layoutInflater)) readerManager = ReaderManager(supportFragmentManager, viewBinding.container) supportActionBar?.setDisplayHomeAsUpEnabled(true) - touchHelper = GridTouchHelper(this, this) + touchHelper = TapGridDispatcher(this, this) scrollTimer = scrollTimerFactory.create(this, this) - controlDelegate = ReaderControlDelegate(resources, settings, this, this) + controlDelegate = ReaderControlDelegate(resources, settings, tapGridSettings, this) viewBinding.slider.setLabelFormatter(PageLabelFormatter()) viewBinding.zoomControl.listener = this ReaderSliderListener(this, viewModel).attachToSlider(viewBinding.slider) @@ -202,12 +205,12 @@ class ReaderActivity : viewBinding.toolbarBottom.invalidateMenu() } - override fun onGridTouch(area: Int) { - controlDelegate.onGridTouch(area, viewBinding.container) + override fun onGridTouch(area: TapGridArea): Boolean { + return controlDelegate.onGridTouch(area) } - override fun onGridLongTouch(area: Int) { - controlDelegate.onGridLongTouch(area, viewBinding.container) + override fun onGridLongTouch(area: TapGridArea) { + controlDelegate.onGridLongTouch(area) } override fun onProcessTouch(rawX: Int, rawY: Int): Boolean { @@ -233,7 +236,7 @@ class ReaderActivity : } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - return controlDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event) + return controlDelegate.onKeyDown(keyCode) || super.onKeyDown(keyCode, event) } override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { @@ -334,6 +337,16 @@ class ReaderActivity : readerManager.currentReader?.switchPageBy(delta) } + override fun switchChapterBy(delta: Int) { + viewModel.switchChapterBy(delta) + } + + override fun openMenu() { + viewModel.saveCurrentState(readerManager.currentReader?.getCurrentState()) + val currentMode = readerManager.currentMode ?: return + ReaderConfigSheet.show(supportFragmentManager, currentMode) + } + override fun scrollBy(delta: Int, smooth: Boolean): Boolean { return readerManager.currentReader?.scrollBy(delta, smooth) ?: false } @@ -342,13 +355,6 @@ class ReaderActivity : setUiIsVisible(!viewBinding.appbarTop.isVisible) } - override fun viewDialog() { - MaterialAlertDialogBuilder(this, DIALOG_THEME_CENTERED) - .setMessage("Called dialog on long press") - .setPositiveButton(R.string.got_it, null) - .show() - } - override fun isReaderResumed(): Boolean { val reader = readerManager.currentReader ?: return false return reader.isResumed && supportFragmentManager.fragments.lastOrNull() === reader 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 d8fc27512..f2663d10f 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 @@ -1,92 +1,49 @@ package org.koitharu.kotatsu.reader.ui -import android.content.SharedPreferences import android.content.res.Resources import android.view.KeyEvent -import android.view.SoundEffectConstants -import android.view.View -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderMode -import org.koitharu.kotatsu.core.util.GridTouchHelper +import org.koitharu.kotatsu.reader.data.TapGridSettings +import org.koitharu.kotatsu.reader.domain.TapGridArea +import org.koitharu.kotatsu.reader.ui.tapgrid.TapAction class ReaderControlDelegate( resources: Resources, private val settings: AppSettings, + private val tapGridSettings: TapGridSettings, private val listener: OnInteractionListener, - owner: LifecycleOwner, -) : DefaultLifecycleObserver, SharedPreferences.OnSharedPreferenceChangeListener { +) { - private var isTapSwitchEnabled: Boolean = true - private var isVolumeKeysSwitchEnabled: Boolean = false - private var isReaderTapsAdaptive: Boolean = true private var minScrollDelta = resources.getDimensionPixelSize(R.dimen.reader_scroll_delta_min) - init { - owner.lifecycle.addObserver(this) - settings.subscribe(this) - updateSettings() + fun onGridTouch(area: TapGridArea): Boolean { + val action = tapGridSettings.getTapAction( + area = area, + isLongTap = false, + ) ?: return false + processAction(action) + return true } - override fun onDestroy(owner: LifecycleOwner) { - settings.unsubscribe(this) - owner.lifecycle.removeObserver(this) - super.onDestroy(owner) + fun onGridLongTouch(area: TapGridArea) { + val action = tapGridSettings.getTapAction( + area = area, + isLongTap = true, + ) ?: return + processAction(action) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { - updateSettings() - } - - fun onGridTouch(area: Int, view: View) { - when (area) { - GridTouchHelper.AREA_CENTER -> { - listener.toggleUiVisibility() - view.playSoundEffect(SoundEffectConstants.CLICK) - } - - GridTouchHelper.AREA_TOP -> if (isTapSwitchEnabled) { - listener.switchPageBy(-1) - view.playSoundEffect(SoundEffectConstants.NAVIGATION_UP) - } - - GridTouchHelper.AREA_LEFT -> if (isTapSwitchEnabled) { - listener.switchPageBy(if (isReaderTapsReversed()) 1 else -1) - view.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT) - } - - GridTouchHelper.AREA_BOTTOM -> if (isTapSwitchEnabled) { - listener.switchPageBy(1) - view.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN) - } - - GridTouchHelper.AREA_RIGHT -> if (isTapSwitchEnabled) { - listener.switchPageBy(if (isReaderTapsReversed()) -1 else 1) - view.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT) - } - } - } - - fun onGridLongTouch(area: Int, view: View) { - when (area) { - GridTouchHelper.AREA_CENTER -> { - listener.viewDialog() - view.playSoundEffect(SoundEffectConstants.CLICK) - } - } - } - - fun onKeyDown(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean = when (keyCode) { - KeyEvent.KEYCODE_VOLUME_UP -> if (isVolumeKeysSwitchEnabled) { + fun onKeyDown(keyCode: Int): Boolean = when (keyCode) { + KeyEvent.KEYCODE_VOLUME_UP -> if (settings.isReaderVolumeButtonsEnabled) { listener.switchPageBy(-1) true } else { false } - KeyEvent.KEYCODE_VOLUME_DOWN -> if (isVolumeKeysSwitchEnabled) { + KeyEvent.KEYCODE_VOLUME_DOWN -> if (settings.isReaderVolumeButtonsEnabled) { listener.switchPageBy(1) true } else { @@ -141,21 +98,23 @@ class ReaderControlDelegate( } fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent?): Boolean { - return ( - isVolumeKeysSwitchEnabled && - (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) - ) + return (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) + && settings.isReaderVolumeButtonsEnabled } - private fun updateSettings() { - val switch = settings.readerPageSwitch - isTapSwitchEnabled = AppSettings.PAGE_SWITCH_TAPS in switch - isVolumeKeysSwitchEnabled = AppSettings.PAGE_SWITCH_VOLUME_KEYS in switch - isReaderTapsAdaptive = settings.isReaderTapsAdaptive + private fun processAction(action: TapAction) { + when (action) { + TapAction.PAGE_NEXT -> listener.switchPageBy(1) + TapAction.PAGE_PREV -> listener.switchPageBy(-1) + TapAction.CHAPTER_NEXT -> listener.switchChapterBy(1) + TapAction.CHAPTER_PREV -> listener.switchChapterBy(-1) + TapAction.TOGGLE_UI -> listener.toggleUiVisibility() + TapAction.SHOW_MENU -> listener.openMenu() + } } private fun isReaderTapsReversed(): Boolean { - return isReaderTapsAdaptive && listener.readerMode == ReaderMode.REVERSED + return listener.readerMode == ReaderMode.REVERSED } interface OnInteractionListener { @@ -164,12 +123,14 @@ class ReaderControlDelegate( fun switchPageBy(delta: Int) - fun viewDialog() + fun switchChapterBy(delta: Int) fun scrollBy(delta: Int, smooth: Boolean): Boolean fun toggleUiVisibility() + fun openMenu() + fun isReaderResumed(): Boolean } } 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 8f1cf6a64..05399af96 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 @@ -262,6 +262,24 @@ class ReaderViewModel @Inject constructor( } } + fun switchChapterBy(delta: Int) { + val prevJob = loadingJob + loadingJob = launchLoadingJob(Dispatchers.Default) { + prevJob?.cancelAndJoin() + val currentChapterId = currentState.requireValue().chapterId + val allChapters = checkNotNull(manga).allChapters + var index = allChapters.indexOfFirst { x -> x.id == currentChapterId } + if (index < 0) { + return@launchLoadingJob + } + index += delta + val newChapterId = (allChapters.getOrNull(index) ?: return@launchLoadingJob).id + content.value = ReaderContent(emptyList(), null) + chaptersLoader.loadSingleChapter(newChapterId) + content.value = ReaderContent(chaptersLoader.snapshot(), ReaderState(newChapterId, 0, 0)) + } + } + @MainThread fun onCurrentPageChanged(lowerPos: Int, upperPos: Int) { val prevJob = stateChangeJob diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/tapgrid/TapAction.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/tapgrid/TapAction.kt new file mode 100644 index 000000000..6bcc9f7b7 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/tapgrid/TapAction.kt @@ -0,0 +1,17 @@ +package org.koitharu.kotatsu.reader.ui.tapgrid + +import androidx.annotation.StringRes +import org.koitharu.kotatsu.R + +enum class TapAction( + @StringRes val nameStringResId: Int, + val color: Int, +) { + + PAGE_NEXT(R.string.next_page, 0x8BFF00), + PAGE_PREV(R.string.prev_page, 0xFF4700), + CHAPTER_NEXT(R.string.next_chapter, 0x327E49), + CHAPTER_PREV(R.string.prev_chapter, 0x7E1218), + TOGGLE_UI(R.string.toggle_ui, 0x3D69C5), + SHOW_MENU(R.string.show_menu, 0xAA1AC5), +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/tapgrid/TapGridDispatcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/tapgrid/TapGridDispatcher.kt new file mode 100644 index 000000000..60108c372 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/tapgrid/TapGridDispatcher.kt @@ -0,0 +1,82 @@ +package org.koitharu.kotatsu.reader.ui.tapgrid + +import android.content.Context +import android.view.GestureDetector +import android.view.MotionEvent +import org.koitharu.kotatsu.reader.domain.TapGridArea +import kotlin.math.roundToInt + +class TapGridDispatcher( + context: Context, + private val listener: OnGridTouchListener, +) : GestureDetector.SimpleOnGestureListener() { + + private val detector = GestureDetector(context, this) + private val width = context.resources.displayMetrics.widthPixels + private val height = context.resources.displayMetrics.heightPixels + private var isDispatching = false + + init { + detector.setIsLongpressEnabled(true) + detector.setOnDoubleTapListener(this) + } + + fun dispatchTouchEvent(event: MotionEvent) { + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + isDispatching = listener.onProcessTouch(event.rawX.toInt(), event.rawY.toInt()) + } + detector.onTouchEvent(event) + } + + override fun onSingleTapConfirmed(event: MotionEvent): Boolean { + if (!isDispatching) { + return true + } + return listener.onGridTouch(getArea(event.rawX, event.rawY)) + } + + override fun onLongPress(event: MotionEvent) { + if (isDispatching) { + listener.onGridLongTouch(getArea(event.rawX, event.rawY)) + } + } + + private fun getArea(x: Float, y: Float): TapGridArea { + val xIndex = (x * 2f / width).roundToInt() + val yIndex = (y * 2f / height).roundToInt() + val area = when (xIndex) { + 0 -> when (yIndex) { // LEFT + 0 -> TapGridArea.TOP_LEFT + 1 -> TapGridArea.CENTER_LEFT + 2 -> TapGridArea.BOTTOM_LEFT + else -> null + } + + 1 -> when (yIndex) { // CENTER + 0 -> TapGridArea.TOP_CENTER + 1 -> TapGridArea.CENTER + 2 -> TapGridArea.BOTTOM_CENTER + else -> null + } + + 2 -> when (yIndex) { // RIGHT + 0 -> TapGridArea.TOP_RIGHT + 1 -> TapGridArea.CENTER_RIGHT + 2 -> TapGridArea.BOTTOM_RIGHT + else -> null + } + + else -> null + } + return checkNotNull(area) { "Invalid area ($xIndex, $yIndex)" } + } + + interface OnGridTouchListener { + + fun onGridTouch(area: TapGridArea): Boolean + + fun onGridLongTouch(area: TapGridArea) + + fun onProcessTouch(rawX: Int, rawY: Int): Boolean + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt index a605cb80f..411840007 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ReaderSettingsFragment.kt @@ -1,10 +1,10 @@ package org.koitharu.kotatsu.settings +import android.content.Intent import android.content.SharedPreferences import android.os.Bundle 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 @@ -16,7 +16,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat import org.koitharu.kotatsu.parsers.util.names -import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider +import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity @AndroidEntryPoint class ReaderSettingsFragment : @@ -26,7 +26,12 @@ class ReaderSettingsFragment : override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_reader) findPreference(AppSettings.KEY_READER_MODE)?.run { - entryValues = ReaderMode.entries.names() + entryValues = arrayOf( + ReaderMode.STANDARD.name, + ReaderMode.REVERSED.name, + ReaderMode.VERTICAL.name, + ReaderMode.WEBTOON.name, + ) setDefaultValueCompat(ReaderMode.STANDARD.name) } findPreference(AppSettings.KEY_READER_BACKGROUND)?.run { @@ -37,9 +42,6 @@ class ReaderSettingsFragment : entryValues = ReaderAnimation.entries.names() setDefaultValueCompat(ReaderAnimation.DEFAULT.name) } - findPreference(AppSettings.KEY_READER_SWITCHERS)?.run { - summaryProvider = MultiSummaryProvider(R.string.gestures_only) - } findPreference(AppSettings.KEY_ZOOM_MODE)?.run { entryValues = ZoomMode.entries.names() setDefaultValueCompat(ZoomMode.FIT_CENTER.name) @@ -57,6 +59,17 @@ class ReaderSettingsFragment : super.onDestroyView() } + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + AppSettings.KEY_READER_TAP_ACTIONS -> { + startActivity(Intent(preference.context, ReaderTapGridConfigActivity::class.java)) + true + } + + else -> super.onPreferenceTreeClick(preference) + } + } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { when (key) { AppSettings.KEY_READER_MODE -> updateReaderModeDependency() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/reader/ReaderTapGridConfigActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/reader/ReaderTapGridConfigActivity.kt new file mode 100644 index 000000000..b1c4d0848 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/reader/ReaderTapGridConfigActivity.kt @@ -0,0 +1,129 @@ +package org.koitharu.kotatsu.settings.reader + +import android.content.DialogInterface +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.Insets +import androidx.core.text.bold +import androidx.core.text.buildSpannedString +import androidx.core.view.updatePadding +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.findKeyByValue +import org.koitharu.kotatsu.core.util.ext.getThemeDrawable +import org.koitharu.kotatsu.core.util.ext.observe +import org.koitharu.kotatsu.databinding.ActivityReaderTapActionsBinding +import org.koitharu.kotatsu.reader.data.TapGridSettings +import org.koitharu.kotatsu.reader.domain.TapGridArea +import org.koitharu.kotatsu.reader.ui.tapgrid.TapAction +import java.util.EnumMap +import javax.inject.Inject +import com.google.android.material.R as materialR + +@AndroidEntryPoint +class ReaderTapGridConfigActivity : BaseActivity(), View.OnClickListener, + View.OnLongClickListener { + + @Inject + lateinit var tapGridSettings: TapGridSettings + + private val controls = EnumMap(TapGridArea::class.java) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(ActivityReaderTapActionsBinding.inflate(layoutInflater)) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + controls[TapGridArea.TOP_LEFT] = viewBinding.textViewTopLeft + controls[TapGridArea.TOP_CENTER] = viewBinding.textViewTopCenter + controls[TapGridArea.TOP_RIGHT] = viewBinding.textViewTopRight + controls[TapGridArea.CENTER_LEFT] = viewBinding.textViewCenterLeft + controls[TapGridArea.CENTER] = viewBinding.textViewCenter + controls[TapGridArea.CENTER_RIGHT] = viewBinding.textViewCenterRight + controls[TapGridArea.BOTTOM_LEFT] = viewBinding.textViewBottomLeft + controls[TapGridArea.BOTTOM_CENTER] = viewBinding.textViewBottomCenter + controls[TapGridArea.BOTTOM_RIGHT] = viewBinding.textViewBottomRight + + controls.forEach { (_, view) -> + view.setOnClickListener(this) + view.setOnLongClickListener(this) + } + updateValues() + tapGridSettings.observe().observe(this) { updateValues() } + } + + override fun onWindowInsetsChanged(insets: Insets) { + viewBinding.root.updatePadding( + left = insets.left, + top = insets.top, + right = insets.right, + bottom = insets.bottom, + ) + } + + override fun onClick(v: View) { + val area = controls.findKeyByValue(v) ?: return + showActionSelector(area, isLongTap = false) + } + + override fun onLongClick(v: View?): Boolean { + val area = controls.findKeyByValue(v) ?: return false + showActionSelector(area, isLongTap = true) + return true + } + + private fun updateValues() { + controls.forEach { (area, view) -> + view.text = buildSpannedString { + appendLine(getString(R.string.tap_action)) + bold { + appendLine(getTapActionText(area, isLongTap = false)) + } + appendLine() + appendLine(getString(R.string.long_tap_action)) + bold { + appendLine(getTapActionText(area, isLongTap = true)) + } + } + view.background = createBackground(tapGridSettings.getTapAction(area, false)) + } + } + + private fun getTapActionText(area: TapGridArea, isLongTap: Boolean): String { + return tapGridSettings.getTapAction(area, isLongTap)?.let { + getString(it.nameStringResId) + } ?: getString(R.string.none) + } + + private fun showActionSelector(area: TapGridArea, isLongTap: Boolean) { + val selectedItem = tapGridSettings.getTapAction(area, isLongTap)?.ordinal ?: -1 + val listener = DialogInterface.OnClickListener { dialog, which -> + tapGridSettings.setTapAction(area, isLongTap, TapAction.entries.getOrNull(which - 1)) + dialog.dismiss() + } + val names = arrayOfNulls(TapAction.entries.size + 1) + names[0] = getString(R.string.none) + TapAction.entries.forEachIndexed { index, action -> names[index + 1] = getString(action.nameStringResId) } + MaterialAlertDialogBuilder(this) + .setSingleChoiceItems(names, selectedItem + 1, listener) + .setTitle(if (isLongTap) R.string.long_tap_action else R.string.tap_action) + .setIcon(R.drawable.ic_tap) + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun createBackground(action: TapAction?): Drawable? { + val ripple = getThemeDrawable(materialR.attr.selectableItemBackground) + return if (action == null) { + ripple + } else { + LayerDrawable(arrayOf(ripple, ColorDrawable(ColorUtils.setAlphaComponent(action.color, 60)))) + } + } +} diff --git a/app/src/main/res/layout/activity_reader_tap_actions.xml b/app/src/main/res/layout/activity_reader_tap_actions.xml new file mode 100644 index 000000000..a559104f0 --- /dev/null +++ b/app/src/main/res/layout/activity_reader_tap_actions.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index d98a64d48..0abbd7895 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -5,10 +5,6 @@ @string/light @string/dark - - @string/taps_on_edges - @string/volume_buttons - @string/zoom_mode_fit_center @string/zoom_mode_fit_height @@ -43,6 +39,7 @@ @string/standard @string/right_to_left + @string/vertical @string/webtoon diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index 93ba5aeb4..ef192ce88 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -21,13 +21,6 @@ 1 2 - - taps - volume - - - taps - favourites history diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 094a3eb67..a921a8a8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -568,4 +568,17 @@ Vertical Last read Two pages + Show menu + Show/hide UI + Previous chapter + Next chapter + Previous page + Next page + Reader actions + Configure actions for tappable screen areas + Enable volume buttons + Use volume buttons for switching pages + Tap action + Long tap action + None diff --git a/app/src/main/res/xml/pref_reader.xml b/app/src/main/res/xml/pref_reader.xml index 06b4a4931..066d87d41 100644 --- a/app/src/main/res/xml/pref_reader.xml +++ b/app/src/main/res/xml/pref_reader.xml @@ -34,19 +34,18 @@ android:summary="@string/reader_zoom_buttons_summary" android:title="@string/reader_zoom_buttons" /> - + android:key="reader_volume_buttons" + android:summary="@string/switch_pages_volume_buttons_summary" + android:title="@string/switch_pages_volume_buttons" /> + android:title="@string/enhanced_colors" + app:allowDividerAbove="true" />