Configurable reader tap actions

This commit is contained in:
Koitharu
2024-01-27 18:06:40 +02:00
parent 72187e7da0
commit 6f7f3dc5e2
17 changed files with 615 additions and 218 deletions

View File

@@ -145,6 +145,9 @@
<data android:host="sync-settings" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity"
android:label="@string/reader_actions" />
<activity
android:name="org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity"
android:label="@string/local_manga_directories" />

View File

@@ -101,15 +101,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
val readerPageSwitch: Set<String>
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"

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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<ActivityReaderBinding>(),
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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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),
}

View File

@@ -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
}
}

View File

@@ -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<ListPreference>(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<ListPreference>(AppSettings.KEY_READER_BACKGROUND)?.run {
@@ -37,9 +42,6 @@ class ReaderSettingsFragment :
entryValues = ReaderAnimation.entries.names()
setDefaultValueCompat(ReaderAnimation.DEFAULT.name)
}
findPreference<MultiSelectListPreference>(AppSettings.KEY_READER_SWITCHERS)?.run {
summaryProvider = MultiSummaryProvider(R.string.gestures_only)
}
findPreference<ListPreference>(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()

View File

@@ -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<ActivityReaderTapActionsBinding>(), View.OnClickListener,
View.OnLongClickListener {
@Inject
lateinit var tapGridSettings: TapGridSettings
private val controls = EnumMap<TapGridArea, TextView>(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<String>(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))))
}
}
}

View File

@@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.33" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.67" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.33" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.67" />
<TextView
android:id="@+id/textView_top_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@id/guideline_top"
app:layout_constraintEnd_toStartOf="@id/guideline_left"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView_top_center"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@id/guideline_top"
app:layout_constraintEnd_toStartOf="@id/guideline_right"
app:layout_constraintStart_toEndOf="@id/guideline_left"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView_top_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@id/guideline_top"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline_right"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView_center_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@id/textView_bottom_left"
app:layout_constraintEnd_toStartOf="@id/guideline_left"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_top_left" />
<TextView
android:id="@+id/textView_center"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@id/textView_bottom_center"
app:layout_constraintEnd_toStartOf="@id/guideline_right"
app:layout_constraintStart_toEndOf="@id/guideline_left"
app:layout_constraintTop_toBottomOf="@id/textView_top_center" />
<TextView
android:id="@+id/textView_center_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toTopOf="@id/textView_bottom_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline_right"
app:layout_constraintTop_toBottomOf="@id/textView_top_right" />
<TextView
android:id="@+id/textView_bottom_left"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/guideline_left"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline_bottom" />
<TextView
android:id="@+id/textView_bottom_center"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/guideline_right"
app:layout_constraintStart_toEndOf="@id/guideline_left"
app:layout_constraintTop_toBottomOf="@id/guideline_bottom" />
<TextView
android:id="@+id/textView_bottom_right"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?selectableItemBackground"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/guideline_right"
app:layout_constraintTop_toBottomOf="@id/guideline_bottom" />
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:background="?android:divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_right"
app:layout_constraintStart_toStartOf="@id/guideline_right"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:background="?android:divider"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/guideline_left"
app:layout_constraintStart_toStartOf="@id/guideline_left"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?android:divider"
app:layout_constraintBottom_toBottomOf="@id/guideline_top"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_top" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?android:divider"
app:layout_constraintBottom_toBottomOf="@id/guideline_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline_bottom" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?android:divider"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@@ -5,10 +5,6 @@
<item>@string/light</item>
<item>@string/dark</item>
</string-array>
<string-array name="reader_switchers" translatable="false">
<item>@string/taps_on_edges</item>
<item>@string/volume_buttons</item>
</string-array>
<string-array name="zoom_modes" translatable="false">
<item>@string/zoom_mode_fit_center</item>
<item>@string/zoom_mode_fit_height</item>
@@ -43,6 +39,7 @@
<string-array name="reader_modes" translatable="false">
<item>@string/standard</item>
<item>@string/right_to_left</item>
<item>@string/vertical</item>
<item>@string/webtoon</item>
</string-array>
<string-array name="scrobbling_statuses" translatable="false">

View File

@@ -21,13 +21,6 @@
<item>1</item>
<item>2</item>
</string-array>
<string-array name="values_reader_switchers" translatable="false">
<item>taps</item>
<item>volume</item>
</string-array>
<string-array name="values_reader_switchers_default" translatable="false">
<item>taps</item>
</string-array>
<string-array name="values_track_sources" translatable="false">
<item>favourites</item>
<item>history</item>

View File

@@ -568,4 +568,17 @@
<string name="vertical">Vertical</string>
<string name="last_read">Last read</string>
<string name="two_pages">Two pages</string>
<string name="show_menu">Show menu</string>
<string name="toggle_ui">Show/hide UI</string>
<string name="prev_chapter">Previous chapter</string>
<string name="next_chapter">Next chapter</string>
<string name="prev_page">Previous page</string>
<string name="next_page">Next page</string>
<string name="reader_actions">Reader actions</string>
<string name="reader_actions_summary">Configure actions for tappable screen areas</string>
<string name="switch_pages_volume_buttons">Enable volume buttons</string>
<string name="switch_pages_volume_buttons_summary">Use volume buttons for switching pages</string>
<string name="tap_action">Tap action</string>
<string name="long_tap_action">Long tap action</string>
<string name="none">None</string>
</resources>

View File

@@ -34,19 +34,18 @@
android:summary="@string/reader_zoom_buttons_summary"
android:title="@string/reader_zoom_buttons" />
<MultiSelectListPreference
android:defaultValue="@array/values_reader_switchers_default"
android:entries="@array/reader_switchers"
android:entryValues="@array/values_reader_switchers"
android:key="reader_switchers"
android:title="@string/switch_pages"
<Preference
android:key="reader_tap_actions"
android:persistent="false"
android:summary="@string/reader_actions_summary"
android:title="@string/reader_actions"
app:allowDividerAbove="true" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="reader_taps_ltr"
android:summary="@string/reader_control_ltr_summary"
android:title="@string/reader_control_ltr" />
android:key="reader_volume_buttons"
android:summary="@string/switch_pages_volume_buttons_summary"
android:title="@string/switch_pages_volume_buttons" />
<ListPreference
android:entries="@array/reader_animation"
@@ -58,7 +57,8 @@
android:defaultValue="false"
android:key="enhanced_colors"
android:summary="@string/enhanced_colors_summary"
android:title="@string/enhanced_colors" />
android:title="@string/enhanced_colors"
app:allowDividerAbove="true" />
<SwitchPreferenceCompat
android:defaultValue="false"