Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/src/main/java/com/capyreader/app/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package com.capyreader.app

import android.content.Intent
import android.os.Bundle
import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo
import android.view.Menu
import androidx.activity.compose.setContent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.capyreader.app.keyboard.KeyboardShortcutManager
import com.capyreader.app.notifications.NotificationHelper
import com.capyreader.app.preferences.AppPreferences
import com.capyreader.app.ui.App
Expand Down Expand Up @@ -37,6 +41,23 @@ class MainActivity : BaseActivity() {
pendingArticleID = NotificationHelper.openFromIntent(intent, appPreferences = appPreferences)
}

override fun onProvideKeyboardShortcuts(
data: MutableList<KeyboardShortcutGroup>?,
menu: Menu?,
deviceId: Int
) {
super.onProvideKeyboardShortcuts(data, menu, deviceId)
val manager = get<KeyboardShortcutManager>()
val shortcuts = manager.effectiveBindings().flatMap { (action, keys) ->
keys.map { key ->
KeyboardShortcutInfo(getString(action.labelRes), key.keyCode, key.meta)
}
}
data?.add(
KeyboardShortcutGroup(getString(R.string.shortcuts_group_title), shortcuts)
)
}

private fun startDestination(): Route {
val appPreferences = get<AppPreferences>()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.capyreader.app.keyboard

import com.jocmp.capy.preferences.Preference
import com.jocmp.capy.preferences.getAndSet

class KeyboardShortcutManager(
private val overridesPreference: Preference<ShortcutOverrides>,
) {
fun resolve(keyCode: Int, meta: Int): ShortcutAction? {
val key = ShortcutKey(keyCode = keyCode, meta = meta)
val reverseLookup = buildReverseLookup()
return reverseLookup[key]
}

fun effectiveBindings(): Map<ShortcutAction, List<ShortcutKey>> {
val overrides = overridesPreference.get().bindings
return ShortcutAction.entries.associateWith { action ->
overrides[action] ?: action.defaultKeys
}
}

fun updateBinding(action: ShortcutAction, keys: List<ShortcutKey>) {
overridesPreference.getAndSet { current ->
current.copy(bindings = current.bindings + (action to keys))
}
}

fun resetBinding(action: ShortcutAction) {
overridesPreference.getAndSet { current ->
current.copy(bindings = current.bindings - action)
}
}

fun resetAll() {
overridesPreference.set(ShortcutOverrides())
}

fun findConflict(key: ShortcutKey, excludeAction: ShortcutAction): ShortcutAction? {
return effectiveBindings()
.filterKeys { it != excludeAction }
.entries
.firstOrNull { (_, keys) -> key in keys }
?.key
}

private fun buildReverseLookup(): Map<ShortcutKey, ShortcutAction> {
val bindings = effectiveBindings()
val lookup = mutableMapOf<ShortcutKey, ShortcutAction>()
bindings.forEach { (action, keys) ->
keys.forEach { key ->
lookup[key] = action
}
}
return lookup
}
}
85 changes: 85 additions & 0 deletions app/src/main/java/com/capyreader/app/keyboard/ShortcutAction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.capyreader.app.keyboard

import android.view.KeyEvent
import androidx.annotation.StringRes
import com.capyreader.app.R
import kotlinx.serialization.Serializable

@Serializable
enum class ShortcutAction(
@StringRes val labelRes: Int,
val defaultKeys: List<ShortcutKey>,
) {
NEXT_ARTICLE(
labelRes = R.string.shortcut_next_article,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_J),
),
),
PREVIOUS_ARTICLE(
labelRes = R.string.shortcut_previous_article,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_K),
),
),
TOGGLE_STAR(
labelRes = R.string.shortcut_toggle_star,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_S),
),
),
TOGGLE_READ(
labelRes = R.string.shortcut_toggle_read,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_M),
),
),
TOGGLE_FULL_CONTENT(
labelRes = R.string.shortcut_toggle_full_content,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_C),
),
),
OPEN_IN_BROWSER(
labelRes = R.string.shortcut_open_in_browser,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_V),
),
),
REFRESH(
labelRes = R.string.shortcut_refresh,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_R),
),
),
GO_BACK(
labelRes = R.string.shortcut_go_back,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_ESCAPE),
),
),
MARK_ALL_READ(
labelRes = R.string.shortcut_mark_all_read,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_A, meta = KeyEvent.META_SHIFT_ON),
),
),
FOCUS_SEARCH(
labelRes = R.string.shortcut_focus_search,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_SLASH),
),
),
TOGGLE_FULLSCREEN(
labelRes = R.string.shortcut_toggle_fullscreen,
defaultKeys = listOf(
ShortcutKey(KeyEvent.KEYCODE_F, meta = KeyEvent.META_SHIFT_ON),
),
);

fun isCharacterBound(): Boolean {
return defaultKeys.any { key ->
key.meta == 0 && key.keyCode in KeyEvent.KEYCODE_A..KeyEvent.KEYCODE_Z
}
}
}
66 changes: 66 additions & 0 deletions app/src/main/java/com/capyreader/app/keyboard/ShortcutKey.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.capyreader.app.keyboard

import android.view.KeyEvent
import kotlinx.serialization.Serializable

@Serializable
data class ShortcutKey(
val keyCode: Int,
val meta: Int = 0,
) {
fun label(): String {
val parts = mutableListOf<String>()

if (meta and KeyEvent.META_CTRL_ON != 0) parts.add("Ctrl")
if (meta and KeyEvent.META_ALT_ON != 0) parts.add("Alt")
if (meta and KeyEvent.META_SHIFT_ON != 0) parts.add("Shift")
if (meta and KeyEvent.META_META_ON != 0) parts.add("Meta")

parts.add(keyCodeLabel(keyCode))

return parts.joinToString("+")
}

companion object {
private fun keyCodeLabel(keyCode: Int): String {
return when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> "Up"
KeyEvent.KEYCODE_DPAD_DOWN -> "Down"
KeyEvent.KEYCODE_DPAD_LEFT -> "Left"
KeyEvent.KEYCODE_DPAD_RIGHT -> "Right"
KeyEvent.KEYCODE_ENTER -> "Enter"
KeyEvent.KEYCODE_SPACE -> "Space"
KeyEvent.KEYCODE_ESCAPE -> "Esc"
KeyEvent.KEYCODE_DEL -> "Backspace"
KeyEvent.KEYCODE_FORWARD_DEL -> "Delete"
KeyEvent.KEYCODE_TAB -> "Tab"
else -> KeyEvent.keyCodeToString(keyCode)
.removePrefix("KEYCODE_")
.lowercase()
.replaceFirstChar { it.uppercase() }
}
}

fun isModifierOnly(keyCode: Int): Boolean {
return keyCode in listOf(
KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_SHIFT_RIGHT,
KeyEvent.KEYCODE_CTRL_LEFT,
KeyEvent.KEYCODE_CTRL_RIGHT,
KeyEvent.KEYCODE_ALT_LEFT,
KeyEvent.KEYCODE_ALT_RIGHT,
KeyEvent.KEYCODE_META_LEFT,
KeyEvent.KEYCODE_META_RIGHT,
)
}

fun metaState(event: KeyEvent): Int {
var meta = 0
if (event.isCtrlPressed) meta = meta or KeyEvent.META_CTRL_ON
if (event.isAltPressed) meta = meta or KeyEvent.META_ALT_ON
if (event.isShiftPressed) meta = meta or KeyEvent.META_SHIFT_ON
if (event.isMetaPressed) meta = meta or KeyEvent.META_META_ON
return meta
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.capyreader.app.keyboard

import kotlinx.serialization.Serializable

@Serializable
data class ShortcutOverrides(
val bindings: Map<ShortcutAction, List<ShortcutKey>> = emptyMap()
)
15 changes: 15 additions & 0 deletions app/src/main/java/com/capyreader/app/preferences/AppPreferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import androidx.preference.PreferenceManager
import com.capyreader.app.common.FeedGroup
import com.capyreader.app.common.ImagePreview
import com.capyreader.app.keyboard.ShortcutOverrides
import com.capyreader.app.refresher.RefreshInterval
import com.capyreader.app.ui.articles.ArticleListFontScale
import com.capyreader.app.ui.articles.DefaultPaneExpansionIndex
Expand Down Expand Up @@ -82,6 +83,20 @@ class AppPreferences(context: Context) {
val badgeStyle: Preference<BadgeStyle>
get() = preferenceStore.getEnum("badge_style", BadgeStyle.default)

val shortcutOverrides: Preference<ShortcutOverrides>
get() = preferenceStore.getObject(
key = "keyboard_shortcut_overrides",
defaultValue = ShortcutOverrides(),
serializer = { Json.encodeToString(it) },
deserializer = {
try {
Json.decodeFromString(it)
} catch (e: Throwable) {
ShortcutOverrides()
}
}
)

fun clearAll() {
preferenceStore.clearAll()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fun ArticleList(
onMarkAllRead: (range: MarkRead) -> Unit = {},
refreshingAll: Boolean,
dimReadArticles: Boolean = true,
keyboardNavigated: Boolean = false,
) {
val articleOptions = rememberArticleOptions().copy(
dim = dimReadArticles,
Expand All @@ -58,6 +59,7 @@ fun ArticleList(
article = item,
index = index,
selected = selectedArticleKey == item.id,
keyboardNavigated = keyboardNavigated,
onSelect = {
onSelect(it)
},
Expand Down
16 changes: 15 additions & 1 deletion app/src/main/java/com/capyreader/app/ui/articles/ArticleRow.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.capyreader.app.ui.articles
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand Down Expand Up @@ -95,6 +96,7 @@ fun ArticleRow(
onMarkAllRead: (range: MarkRead) -> Unit = {},
currentTime: LocalDateTime,
options: ArticleRowOptions = ArticleRowOptions(),
keyboardNavigated: Boolean = false,
) {
val imageURL = article.imageURL
val isMonochrome = LocalAppTheme.current.value == AppTheme.MONOCHROME
Expand Down Expand Up @@ -125,6 +127,7 @@ fun ArticleRow(
onClick = { onSelect(article.id) },
onLongClick = openArticleMenu,
article = article,
showBorder = selected && keyboardNavigated,
) {
ArticleListItem(
headlineContent = {
Expand Down Expand Up @@ -415,11 +418,22 @@ private fun ArticleBox(
onClick: () -> Unit,
onLongClick: () -> Unit,
article: Article,
showBorder: Boolean,
content: @Composable () -> Unit
) {
ArticleRowSwipeBox(article) {
Box(
val borderModifier = if (showBorder) {
Modifier.border(
width = 2.dp,
color = colorScheme.primary,
shape = RoundedCornerShape(4.dp),
)
} else {
Modifier
}

Box(
borderModifier
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
Expand Down
Loading
Loading