Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,16 @@ private fun listItemColors(
val defaults = ListItemDefaults.colors()
val isMonochrome = LocalAppTheme.current == AppTheme.MONOCHROME
val dimColors = read && !isMonochrome
val isListFocused = LocalListFocused.current

val containerColor = when {
selected && isListFocused -> colorScheme.primaryContainer
selected -> colorScheme.surfaceVariant
else -> defaults.containerColor
}

return ListItemDefaults.colors(
containerColor = if (selected) colorScheme.surfaceVariant else defaults.containerColor,
containerColor = containerColor,
headlineColor = if (dimColors) defaults.disabledContentColor else defaults.headlineColor,
supportingColor = if (dimColors) defaults.disabledContentColor else defaults.supportingTextColor
)
Expand Down
113 changes: 100 additions & 13 deletions app/src/main/java/com/capyreader/app/ui/articles/ArticleScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,20 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.foundation.focusable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import com.capyreader.app.ui.articles.detail.ArticleShortcut
import com.capyreader.app.ui.articles.detail.articleKeyboardHandler
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
Expand Down Expand Up @@ -192,6 +197,8 @@ fun ArticleScreen(
val scrollBehavior = pinnedScrollBehavior()
var media by rememberSaveable(saver = Media.Saver) { mutableStateOf(null) }
val focusManager = LocalFocusManager.current
val listFocusRequester = remember { FocusRequester() }
val detailFocusRequester = remember { FocusRequester() }
val openUpdatePasswordDialog = {
viewModel.dismissUnauthorizedMessage()
setUpdatePasswordDialogOpen(true)
Expand Down Expand Up @@ -480,9 +487,68 @@ fun ArticleScreen(
listPane = {
val keyboardManager = LocalSoftwareKeyboardController.current
val markReadPosition = LocalMarkAllReadButtonPosition.current
val isListFocused =
scaffoldNavigator.currentDestination?.pane == ListDetailPaneScaffoldRole.List

val currentArticleIndex = remember(article?.id, articles.itemCount) {
if (article == null) -1
else articles.itemSnapshotList.indexOfFirst { it?.id == article.id }
}

val onListShortcut = { shortcut: ArticleShortcut ->
when (shortcut) {
ArticleShortcut.NextArticle -> {
val nextIndex = if (currentArticleIndex < 0) 0 else currentArticleIndex + 1
if (nextIndex < articles.itemCount) {
articles[nextIndex]?.let { setArticle(it.id) }
}
}
ArticleShortcut.PreviousArticle -> {
val prevIndex = currentArticleIndex - 1
if (prevIndex >= 0) {
articles[prevIndex]?.let { setArticle(it.id) }
}
}
ArticleShortcut.OpenInBrowser -> {
article?.url?.let { url ->
linkOpener.open(url.toString().toUri())
}
}
ArticleShortcut.ToggleStar -> {
if (article != null) {
viewModel.toggleArticleStar()
}
}
ArticleShortcut.ToggleRead -> {
if (article != null) {
viewModel.toggleArticleRead()
}
}
ArticleShortcut.ToggleFullContent -> {
if (article != null) {
fullContent.fetch()
}
}
ArticleShortcut.FocusDetail -> {
coroutineScope.launch {
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
detailFocusRequester.requestFocus()
}
}
ArticleShortcut.FocusList -> {}
else -> {}
}
Unit
}

Scaffold(
modifier = Modifier
.focusRequester(listFocusRequester)
.focusable()
.articleKeyboardHandler(
isDetailPaneFocused = false,
onShortcut = onListShortcut,
)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.nestedScroll(object : NestedScrollConnection {
override fun onPostScroll(
Expand Down Expand Up @@ -565,19 +631,23 @@ fun ArticleScreen(
},
) {
key(filter, articles.itemCount) {
ArticleList(
articles = articles,
selectedArticleKey = article?.id,
listState = listState,
enableMarkReadOnScroll = enableMarkReadOnScroll,
refreshingAll = viewModel.refreshingAll,
onMarkAllRead = { range ->
onMarkAllRead(range)
},
onSelect = { articleID ->
selectArticle(articleID)
},
)
CompositionLocalProvider(
LocalListFocused provides isListFocused
) {
ArticleList(
articles = articles,
selectedArticleKey = article?.id,
listState = listState,
enableMarkReadOnScroll = enableMarkReadOnScroll,
refreshingAll = viewModel.refreshingAll,
onMarkAllRead = { range ->
onMarkAllRead(range)
},
onSelect = { articleID ->
selectArticle(articleID)
},
)
}
}
}
}
Expand All @@ -594,6 +664,9 @@ fun ArticleScreen(
CapyPlaceholder()
}
} else if (article != null) {
val isDetailPaneFocused =
scaffoldNavigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail

ArticleView(
article = article,
articles = articles,
Expand All @@ -603,6 +676,20 @@ fun ArticleScreen(
onToggleRead = viewModel::toggleArticleRead,
onToggleStar = viewModel::toggleArticleStar,
enableBackHandler = media == null,
isDetailPaneFocused = isDetailPaneFocused,
onFocusList = {
coroutineScope.launch {
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.List)
listFocusRequester.requestFocus()
}
},
focusRequester = detailFocusRequester,
onFocusDetail = {
coroutineScope.launch {
scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail)
detailFocusRequester.requestFocus()
}
},
onSelectMedia = { media = it },
onSelectArticle = { articleID ->
setArticle(articleID)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.capyreader.app.ui.articles

import androidx.compose.runtime.compositionLocalOf

val LocalListFocused = compositionLocalOf { false }
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.capyreader.app.ui.articles.detail

import android.view.KeyEvent
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onKeyEvent

fun Modifier.articleKeyboardHandler(
isDetailPaneFocused: Boolean,
onShortcut: (ArticleShortcut) -> Unit,
): Modifier = onKeyEvent { event ->
if (event.nativeKeyEvent.action != KeyEvent.ACTION_DOWN) {
return@onKeyEvent false
}

val shortcut = mapKeyToShortcut(
keyCode = event.nativeKeyEvent.keyCode,
isDetailPaneFocused = isDetailPaneFocused,
) ?: return@onKeyEvent false

onShortcut(shortcut)
true
}

private fun mapKeyToShortcut(keyCode: Int, isDetailPaneFocused: Boolean): ArticleShortcut? {
return when (keyCode) {
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_DPAD_DOWN -> {
if (isDetailPaneFocused) ArticleShortcut.ScrollDown else ArticleShortcut.NextArticle
}
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_DPAD_UP -> {
if (isDetailPaneFocused) ArticleShortcut.ScrollUp else ArticleShortcut.PreviousArticle
}
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_DPAD_LEFT -> ArticleShortcut.FocusList
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_DPAD_RIGHT -> ArticleShortcut.FocusDetail
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_DPAD_CENTER -> ArticleShortcut.OpenInBrowser
KeyEvent.KEYCODE_S -> ArticleShortcut.ToggleStar
KeyEvent.KEYCODE_M -> ArticleShortcut.ToggleRead
KeyEvent.KEYCODE_C -> ArticleShortcut.ToggleFullContent
KeyEvent.KEYCODE_ESCAPE -> ArticleShortcut.GoBack
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import kotlin.math.roundToInt
fun ArticleReader(
article: Article,
onSelectMedia: (media: Media) -> Unit,
onScrollStateReady: (ScrollState, maxHeight: Float) -> Unit = { _, _ -> },
) {
val (shareLink, setShareLink) = rememberSaveableShareLink()
val linkOpener = LocalLinkOpener.current
Expand All @@ -62,7 +63,12 @@ fun ArticleReader(
)
}
} else {
ScrollableWebView(webViewState, article, showImages)
ScrollableWebView(
webViewState = webViewState,
article = article,
showImages = showImages,
onScrollStateReady = onScrollStateReady,
)
}

ArticleStyleListener(webView = webViewState.webView)
Expand All @@ -78,14 +84,23 @@ fun ArticleReader(
}

@Composable
fun ScrollableWebView(webViewState: WebViewState, article: Article, showImages: Boolean) {
fun ScrollableWebView(
webViewState: WebViewState,
article: Article,
showImages: Boolean,
onScrollStateReady: (ScrollState, maxHeight: Float) -> Unit = { _, _ -> },
) {
var maxHeight by remember { mutableFloatStateOf(0f) }
val scrollState = rememberSaveable(article.id, saver = ScrollState.Saver) {
ScrollState(initial = 0)
}

var lastScrollYPercent by rememberSaveable(article.id) { mutableFloatStateOf(0f) }

LaunchedEffect(scrollState, maxHeight) {
onScrollStateReady(scrollState, maxHeight)
}

CornerTapGestureScroll(
maxArticleHeight = maxHeight,
scrollState = scrollState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.capyreader.app.ui.articles.detail

sealed class ArticleShortcut {
data object NextArticle : ArticleShortcut()
data object PreviousArticle : ArticleShortcut()
data object OpenInBrowser : ArticleShortcut()
data object ToggleStar : ArticleShortcut()
data object ToggleRead : ArticleShortcut()
data object ToggleFullContent : ArticleShortcut()
data object GoBack : ArticleShortcut()
data object ScrollDown : ArticleShortcut()
data object ScrollUp : ArticleShortcut()
data object FocusList : ArticleShortcut()
data object FocusDetail : ArticleShortcut()
}
Loading