Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e284872eaa |
No files matched your search
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user