Give the reader a wide-screen shape: measure, contents, dictionary beside it
Three changes, all behind one breakpoint at 840dp — Material's "expanded" width, which is a tablet in landscape or a foldable opened out. Below it nothing moves. The measure. A couplet's two hemistichs are set to the start and end of the line, which on a phone reads as one unit and on a tablet flings them to opposite edges with a hand's width of nothing between. The poem is now held to 680dp and centred, so the halves of a line stay within one glance of each other. On a phone the constraint never binds. Contents. PoemTree puts the poem's place in the poet's work beside it — poet » book » section » poem — with the path to the open poem already unfolded and scrolled to, because a book of 495 ghazals otherwise opens at ghazal 1. Branches load as they open. Tapping a category folds it rather than navigating: the section's own page lists the same poems, so opening it would only cost the reader their place. The dictionary. On a wide screen it is a column beside the poem rather than a sheet over it, so the line the word came from stays in view while its meaning is read, and it holds its word until closed. The sheet and the pane share one lookup and one footer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
50bbe0b729
commit
e284872eaa
@ -4,14 +4,19 @@ import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
@ -20,10 +25,12 @@ import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.FavoriteBorder
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
@ -38,6 +45,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@ -115,83 +123,188 @@ fun PoemScreen(
|
||||
)
|
||||
}
|
||||
) { insets ->
|
||||
// Free-form selection for copying any span; tapping a word looks it up, and the
|
||||
// per-couplet actions save a passage with the reference attached, which a raw copy
|
||||
// would lose.
|
||||
//
|
||||
// The system selection menu gets its own entries from ProcessTextActivity, not from
|
||||
// here: Compose 1.10 stopped routing SelectionContainer through LocalTextToolbar, so
|
||||
// a custom TextToolbar is never asked to show. ACTION_PROCESS_TEXT goes round that,
|
||||
// and reaches every other app's selection menu as a side effect.
|
||||
SelectionContainer {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 20.dp,
|
||||
end = 20.dp,
|
||||
top = insets.calculateTopPadding() + 8.dp,
|
||||
bottom = insets.calculateBottomPadding() + 32.dp,
|
||||
),
|
||||
) {
|
||||
item {
|
||||
Column(Modifier.padding(bottom = 12.dp)) {
|
||||
Breadcrumbs(poem.fullTitle, poem.fullUrl.ifBlank { fullUrl }, onCategory)
|
||||
RecitationPlayer(poem.id)
|
||||
poem.metre?.rhythm?.let { rhythm ->
|
||||
Text(
|
||||
text = rhythm,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
// Narrow and wide are the same reader in different containers. On a phone the poem
|
||||
// has the window to itself and the dictionary arrives as a sheet over it; given the
|
||||
// width of a tablet or an opened foldable, the poem's place in the poet's work sits
|
||||
// on one side of it and the dictionary on the other, and nothing covers the verse.
|
||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
val wide = maxWidth >= WideScreen
|
||||
|
||||
val body: @Composable (Modifier) -> Unit = { modifier ->
|
||||
// Free-form selection for copying any span; tapping a word looks it up, and
|
||||
// the per-couplet actions save a passage with the reference attached, which a
|
||||
// raw copy would lose.
|
||||
//
|
||||
// The system selection menu gets its own entries from ProcessTextActivity,
|
||||
// not from here: Compose 1.10 stopped routing SelectionContainer through
|
||||
// LocalTextToolbar, so a custom TextToolbar is never asked to show.
|
||||
// ACTION_PROCESS_TEXT goes round that, and reaches every other app's
|
||||
// selection menu as a side effect.
|
||||
SelectionContainer(modifier) {
|
||||
PoemBody(
|
||||
poem = poem,
|
||||
fullUrl = fullUrl,
|
||||
couplets = couplets,
|
||||
style = style,
|
||||
showSummaries = prefs.showSummaries,
|
||||
siblings = siblings,
|
||||
here = here,
|
||||
insets = insets,
|
||||
onPoem = onPoem,
|
||||
onCategory = onCategory,
|
||||
onWord = { word, passage -> tapped = word to passage },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (wide) {
|
||||
Row(Modifier.fillMaxSize()) {
|
||||
// First child, so in this right-to-left layout it lands on the right.
|
||||
PoemTree(
|
||||
poemUrl = fullUrl,
|
||||
onPoem = onPoem,
|
||||
contentPadding = insets,
|
||||
modifier = Modifier.width(300.dp).fillMaxHeight(),
|
||||
)
|
||||
VerticalDivider()
|
||||
body(Modifier.weight(1f))
|
||||
tapped?.let { (word, passage) ->
|
||||
VerticalDivider()
|
||||
DictionaryPane(
|
||||
word = word,
|
||||
passage = passage,
|
||||
onClose = { tapped = null },
|
||||
insets = insets,
|
||||
modifier = Modifier.width(360.dp).fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(couplets) { couplet ->
|
||||
Couplet(
|
||||
couplet = couplet,
|
||||
style = style,
|
||||
showSummaries = prefs.showSummaries,
|
||||
source = Bookmark(fullUrl, poem.title, poem.fullTitle),
|
||||
onWord = { word, passage -> tapped = word to passage },
|
||||
)
|
||||
}
|
||||
|
||||
if (prefs.showSummaries) {
|
||||
poem.poemSummary?.takeIf { it.isNotBlank() }?.let { summary ->
|
||||
item { PoemSummary(summary) }
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SiblingNav(
|
||||
previous = if (here > 0) siblings[here - 1] else null,
|
||||
next = if (here >= 0) siblings.getOrNull(here + 1) else null,
|
||||
onPoem = onPoem,
|
||||
)
|
||||
}
|
||||
|
||||
poem.sourceName?.takeIf { it.isNotBlank() }?.let { source ->
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.source, source),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
} else {
|
||||
body(Modifier)
|
||||
tapped?.let { (word, passage) ->
|
||||
WordSheet(
|
||||
word = word,
|
||||
passage = passage,
|
||||
onDismiss = { tapped = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The poem itself, held to a reading measure. Left to fill a tablet, a couplet's two hemistichs
|
||||
* are flung to opposite edges of the window with a hand's width of nothing between them, and the
|
||||
* eye has to cross the whole screen between halves of one line.
|
||||
*/
|
||||
@Composable
|
||||
private fun PoemBody(
|
||||
poem: Poem,
|
||||
fullUrl: String,
|
||||
couplets: List<List<Verse>>,
|
||||
style: androidx.compose.ui.text.TextStyle,
|
||||
showSummaries: Boolean,
|
||||
siblings: List<PoemRef>,
|
||||
here: Int,
|
||||
insets: PaddingValues,
|
||||
onPoem: (String) -> Unit,
|
||||
onCategory: (String) -> Unit,
|
||||
onWord: (String, Bookmark) -> Unit,
|
||||
) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.widthIn(max = ReadingMeasure).fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
start = 20.dp,
|
||||
end = 20.dp,
|
||||
top = insets.calculateTopPadding() + 8.dp,
|
||||
bottom = insets.calculateBottomPadding() + 32.dp,
|
||||
),
|
||||
) {
|
||||
item {
|
||||
Column(Modifier.padding(bottom = 12.dp)) {
|
||||
Breadcrumbs(poem.fullTitle, poem.fullUrl.ifBlank { fullUrl }, onCategory)
|
||||
RecitationPlayer(poem.id)
|
||||
poem.metre?.rhythm?.let { rhythm ->
|
||||
Text(
|
||||
text = rhythm,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(couplets) { couplet ->
|
||||
Couplet(
|
||||
couplet = couplet,
|
||||
style = style,
|
||||
showSummaries = showSummaries,
|
||||
source = Bookmark(fullUrl, poem.title, poem.fullTitle),
|
||||
onWord = onWord,
|
||||
)
|
||||
}
|
||||
|
||||
if (showSummaries) {
|
||||
poem.poemSummary?.takeIf { it.isNotBlank() }?.let { summary ->
|
||||
item { PoemSummary(summary) }
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SiblingNav(
|
||||
previous = if (here > 0) siblings[here - 1] else null,
|
||||
next = if (here >= 0) siblings.getOrNull(here + 1) else null,
|
||||
onPoem = onPoem,
|
||||
)
|
||||
}
|
||||
|
||||
poem.sourceName?.takeIf { it.isNotBlank() }?.let { source ->
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.source, source),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tapped?.let { (word, passage) ->
|
||||
WordSheet(
|
||||
word = word,
|
||||
passage = passage,
|
||||
onDismiss = { tapped = null },
|
||||
)
|
||||
/**
|
||||
* The dictionary as a column beside the poem rather than a sheet over it, so the line the word
|
||||
* came from stays in view while its meaning is read. Closeable, because it holds its word until
|
||||
* dismissed — unlike the sheet, which a tap outside takes away.
|
||||
*/
|
||||
@Composable
|
||||
private fun DictionaryPane(
|
||||
word: String,
|
||||
passage: Bookmark,
|
||||
onClose: () -> Unit,
|
||||
insets: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier.padding(top = insets.calculateTopPadding())) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 20.dp, end = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.dictionary),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(Icons.Default.Close, stringResource(R.string.close))
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
WordLookup(word) { PassageFooter(passage) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
226
app/src/main/java/com/ganjoor/android/ui/PoemTree.kt
Normal file
226
app/src/main/java/com/ganjoor/android/ui/PoemTree.kt
Normal file
@ -0,0 +1,226 @@
|
||||
package com.ganjoor.android.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ganjoor.android.R
|
||||
import com.ganjoor.android.data.Category
|
||||
import com.ganjoor.android.data.Ganjoor
|
||||
|
||||
/**
|
||||
* Below this, the window is a phone: one column, and the dictionary arrives as a sheet over the
|
||||
* poem. At or above it there is room to put the poem's place in the poet's work beside it, and
|
||||
* the dictionary beside that, without squeezing the measure the poetry is set to.
|
||||
*
|
||||
* 840dp is Material's "expanded" width — a tablet in landscape, or a foldable opened out.
|
||||
*/
|
||||
val WideScreen = 840.dp
|
||||
|
||||
/** The reading measure. A line of verse set across a whole tablet is a line nobody can follow. */
|
||||
val ReadingMeasure = 680.dp
|
||||
|
||||
/**
|
||||
* One row of the contents tree. [expanded] is null for a poem, which has nothing to open;
|
||||
* [url] is null for the placeholder shown while a branch is still loading.
|
||||
*/
|
||||
private data class TreeRow(
|
||||
val label: String,
|
||||
val url: String?,
|
||||
val depth: Int,
|
||||
val expanded: Boolean?,
|
||||
val current: Boolean = false,
|
||||
)
|
||||
|
||||
/** Every category above [poemUrl]: `/hafez/ghazal/sh1` -> `/hafez`, `/hafez/ghazal`. */
|
||||
private fun ancestorUrls(poemUrl: String): List<String> {
|
||||
val segments = poemUrl.trim('/').split('/').filter { it.isNotEmpty() }
|
||||
if (segments.size <= 1) return emptyList()
|
||||
return (1 until segments.size).map { "/" + segments.take(it).joinToString("/") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the loaded part of the tree into rows. A branch that is open but not yet fetched
|
||||
* contributes a spinner rather than nothing, so an expanding node never looks broken.
|
||||
*/
|
||||
private fun buildRows(
|
||||
root: String,
|
||||
loaded: Map<String, Category>,
|
||||
expanded: Set<String>,
|
||||
current: String,
|
||||
): List<TreeRow> {
|
||||
val rows = mutableListOf<TreeRow>()
|
||||
|
||||
fun walk(url: String, depth: Int) {
|
||||
val category = loaded[url]
|
||||
if (category == null) {
|
||||
rows += TreeRow(label = "", url = null, depth = depth, expanded = null)
|
||||
return
|
||||
}
|
||||
category.childCats.forEach { child ->
|
||||
val open = child.fullUrl in expanded
|
||||
rows += TreeRow(child.title, child.fullUrl, depth, open)
|
||||
if (open) walk(child.fullUrl, depth + 1)
|
||||
}
|
||||
category.poems.forEach { poem ->
|
||||
rows += TreeRow(poem.title, poem.fullUrl, depth, null, poem.fullUrl == current)
|
||||
}
|
||||
}
|
||||
|
||||
walk(root, 0)
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Where this poem sits in its poet's work — poet » book » section » poem — as a tree beside the
|
||||
* reader, with the path to the open poem already unfolded and scrolled to.
|
||||
*
|
||||
* Tapping a category folds it open or shut rather than navigating: a section's own page lists the
|
||||
* same poems this tree already shows, so opening it would only cost the reader their place. The
|
||||
* breadcrumbs above the poem are the way out to a category page.
|
||||
*
|
||||
* ponytail: branches are fetched as they open and kept only while the screen lives. The disk
|
||||
* cache makes re-opening cheap, and a poet's whole tree is far too much to hold eagerly.
|
||||
*/
|
||||
@Composable
|
||||
fun PoemTree(
|
||||
poemUrl: String,
|
||||
onPoem: (String) -> Unit,
|
||||
contentPadding: PaddingValues,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val ancestors = remember(poemUrl) { ancestorUrls(poemUrl) }
|
||||
if (ancestors.isEmpty()) return
|
||||
val root = ancestors.first()
|
||||
|
||||
val loaded = remember(poemUrl) { mutableStateMapOf<String, Category>() }
|
||||
var expanded by remember(poemUrl) { mutableStateOf(ancestors.toSet()) }
|
||||
var unreachable by remember(poemUrl) { mutableStateOf(emptySet<String>()) }
|
||||
|
||||
LaunchedEffect(poemUrl, expanded) {
|
||||
expanded.filterNot { it in loaded || it in unreachable }.forEach { url ->
|
||||
runCatching { Ganjoor.category(url) }
|
||||
.onSuccess { loaded[url] = it }
|
||||
// A branch that will not load stays shut rather than retrying on every
|
||||
// recomposition; the poem itself is already on screen and still readable.
|
||||
.onFailure { unreachable = unreachable + url }
|
||||
}
|
||||
}
|
||||
|
||||
val rows = remember(loaded.size, expanded, poemUrl) {
|
||||
buildRows(root, loaded, expanded, poemUrl)
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
var settled by remember(poemUrl) { mutableStateOf(false) }
|
||||
// Once, when the open poem's row first exists: a book of 495 ghazals opens far from the one
|
||||
// being read, and a tree that starts at ghazal 1 is a tree nobody scrolls.
|
||||
LaunchedEffect(rows) {
|
||||
if (settled) return@LaunchedEffect
|
||||
val index = rows.indexOfFirst { it.current }
|
||||
if (index >= 0) {
|
||||
listState.scrollToItem(index + 1)
|
||||
settled = true
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(modifier = modifier, state = listState, contentPadding = contentPadding) {
|
||||
item {
|
||||
Text(
|
||||
text = loaded[root]?.title ?: stringResource(R.string.contents),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp, bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
items(rows) { row ->
|
||||
TreeRowItem(
|
||||
row = row,
|
||||
onToggle = {
|
||||
expanded = if (row.url in expanded) expanded - row.url!! else expanded + row.url!!
|
||||
},
|
||||
onPoem = onPoem,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TreeRowItem(row: TreeRow, onToggle: () -> Unit, onPoem: (String) -> Unit) {
|
||||
// Indent by depth, on top of the row's own padding, so nesting reads at a glance.
|
||||
val indent = (row.depth * 14).dp
|
||||
|
||||
if (row.url == null) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = indent + 12.dp, top = 8.dp, bottom = 8.dp),
|
||||
) {
|
||||
CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val isCategory = row.expanded != null
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { if (isCategory) onToggle() else onPoem(row.url) }
|
||||
.padding(start = indent + 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
if (isCategory) {
|
||||
Icon(
|
||||
// Down when open, and pointing along the text when shut — the chevron is
|
||||
// auto-mirrored, so it turns the right way in a right-to-left layout.
|
||||
imageVector = if (row.expanded == true) Icons.Default.KeyboardArrowDown
|
||||
else Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||
contentDescription = stringResource(
|
||||
if (row.expanded == true) R.string.collapse else R.string.expand
|
||||
),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = row.label,
|
||||
style = if (isCategory) MaterialTheme.typography.bodyMedium
|
||||
else MaterialTheme.typography.bodySmall,
|
||||
fontWeight = if (row.current) FontWeight.Bold else null,
|
||||
color = when {
|
||||
row.current -> MaterialTheme.colorScheme.primary
|
||||
isCategory -> MaterialTheme.colorScheme.onSurface
|
||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,24 +78,33 @@ fun WordSheet(word: String, onDismiss: () -> Unit, passage: Bookmark? = null) {
|
||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
||||
// Inside the lookup's own scroll, not after it: the lookup scrolls, so anything placed
|
||||
// below it is pushed past the bottom of the sheet with no way to reach it.
|
||||
WordLookup(word) {
|
||||
if (passage == null) return@WordLookup
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.this_couplet),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
PassageActions(passage)
|
||||
if (LocalAssistant.current.serverReady) {
|
||||
AssistantInline(
|
||||
prompt = "explain",
|
||||
text = passage.excerpt.orEmpty(),
|
||||
label = R.string.assistant_explain,
|
||||
instruction = R.string.assistant_ask_prompt,
|
||||
)
|
||||
}
|
||||
}
|
||||
WordLookup(word) { PassageFooter(passage) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The couplet the word was tapped in, with its own actions, under the definitions.
|
||||
*
|
||||
* Shared by the phone's sheet and the wide-screen dictionary pane, which show the same lookup in
|
||||
* two different containers.
|
||||
*/
|
||||
@Composable
|
||||
internal fun ColumnScope.PassageFooter(passage: Bookmark?) {
|
||||
if (passage == null) return
|
||||
HorizontalDivider(modifier = Modifier.padding(top = 8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.this_couplet),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
PassageActions(passage)
|
||||
if (LocalAssistant.current.serverReady) {
|
||||
AssistantInline(
|
||||
prompt = "explain",
|
||||
text = passage.excerpt.orEmpty(),
|
||||
label = R.string.assistant_explain,
|
||||
instruction = R.string.assistant_ask_prompt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -115,4 +115,8 @@
|
||||
<string name="view_grid">نمایش کارتی</string>
|
||||
<string name="view_list">نمایش فهرستی</string>
|
||||
<string name="downloaded_poet">پیشتر دریافت شده</string>
|
||||
<string name="close">بستن</string>
|
||||
<string name="contents">فهرست</string>
|
||||
<string name="expand">گشودن</string>
|
||||
<string name="collapse">بستن شاخه</string>
|
||||
</resources>
|
||||
|
||||
@ -115,4 +115,8 @@
|
||||
<string name="view_grid">کارڈ کی صورت</string>
|
||||
<string name="view_list">فہرست کی صورت</string>
|
||||
<string name="downloaded_poet">پہلے سے محفوظ ہے</string>
|
||||
<string name="close">بند کریں</string>
|
||||
<string name="contents">فہرست</string>
|
||||
<string name="expand">کھولیں</string>
|
||||
<string name="collapse">بند کریں</string>
|
||||
</resources>
|
||||
|
||||
@ -120,4 +120,8 @@
|
||||
<string name="view_grid">Show as cards</string>
|
||||
<string name="view_list">Show as a list</string>
|
||||
<string name="downloaded_poet">Already downloaded</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="contents">Contents</string>
|
||||
<string name="expand">Expand</string>
|
||||
<string name="collapse">Collapse</string>
|
||||
</resources>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user