Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e284872eaa | ||
|
|
50bbe0b729 | ||
|
|
5d12a1c29c |
No files matched your search
@@ -5,9 +5,8 @@
|
||||
> **Unofficial.** This is an independent, unofficial Android client for
|
||||
> [ganjoor.net](https://ganjoor.net). It is a **separate project** from ganjoor.net and its
|
||||
> GitHub repositories ([`ganjoor/ganjoor`](https://github.com/ganjoor),
|
||||
> `GanjoorService`, and the rest), built and maintained by **Anas Rashid**. It is not
|
||||
> affiliated with, endorsed by, or managed by the owners of ganjoor.net, and they bear no
|
||||
> responsibility for it. Please direct any issue with this app
|
||||
> `GanjoorService`, and the rest), built and maintained by **Muhammad Anas Rashid**. The owners of
|
||||
> ganjoor.net do not run or manage it. Please direct any issue with this app
|
||||
> [here](https://github.com/anas-rashid/ganjoorandroid/issues) — never to the Ganjoor project.
|
||||
|
||||
An Android reader for [Ganjoor](https://ganjoor.net), the open archive of Persian poetry —
|
||||
@@ -238,7 +237,7 @@ The two things worth knowing up front: the **data set** carries no licence file,
|
||||
**GanjoorService** is GPL-3.0 but none of its code is used here — only data over HTTPS — so this
|
||||
app is not a derivative work of it.
|
||||
|
||||
This app is an **unofficial, independent** client, maintained by **Anas Rashid**. It is not
|
||||
affiliated with, endorsed by, or managed by ganjoor.net or its maintainers, and it is a wholly
|
||||
separate project from the Ganjoor GitHub repositories. The same statement is shown in the app
|
||||
itself, at **Reading settings → About & licences**.
|
||||
This app is an **unofficial, independent** client, built and maintained by **Muhammad Anas Rashid**. It is
|
||||
a wholly separate project from ganjoor.net and the Ganjoor GitHub repositories, and the owners of
|
||||
ganjoor.net do not run or manage it. The same statement is shown in the app itself, at **Reading
|
||||
settings → About & licences**.
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<string name="load_more">بیشتر</string>
|
||||
<string name="done">تمام</string>
|
||||
<string name="about">درباره و پروانهها</string>
|
||||
<string name="about_unofficial">غیررسمی. «گنجور برای اندروید» یک کارخواه اندرویدیِ مستقل و غیررسمی برای ganjoor.net است. این برنامه پروژهای جدا از ganjoor.net و مخزنهای گیتهاب آن است و بهدست انس رشید ساخته و نگهداری میشود. هیچ وابستگی، تأیید یا مدیریتی از سوی صاحبان ganjoor.net ندارد و آنان هیچ مسئولیتی در قبال آن بر عهده ندارند.</string>
|
||||
<string name="about_unofficial">غیررسمی. «گنجور برای اندروید» یک کارخواه اندرویدیِ مستقل و غیررسمی برای ganjoor.net است. این برنامه پروژهای جدا از ganjoor.net و مخزنهای گیتهاب آن است و بهدست محمد انس رشید ساخته و نگهداری میشود. صاحبان ganjoor.net آن را اداره یا مدیریت نمیکنند.</string>
|
||||
<string name="about_intro">گنجور برای اندروید نرمافزار آزاد است. شعرها، قلمها و همهٔ کتابخانههایی که این برنامه بر آنها ساخته شده در زیر آمدهاند؛ برای خواندن متن کامل پروانه روی هر مورد بزنید.</string>
|
||||
<string name="no_definition">برای این واژه مدخلی یافت نشد.</string>
|
||||
<string name="source_wiktionary">ویکیواژه — فارسی به انگلیسی (CC BY-SA 3.0)</string>
|
||||
@@ -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>
|
||||
@@ -59,7 +59,7 @@
|
||||
<string name="load_more">مزید</string>
|
||||
<string name="done">مکمل</string>
|
||||
<string name="about">تعارف اور لائسنس</string>
|
||||
<string name="about_unofficial">غیر سرکاری۔ ”گنجور فار اینڈرائیڈ“ ganjoor.net کا ایک خودمختار اور غیر سرکاری اینڈرائیڈ کلائنٹ ہے۔ یہ ganjoor.net اور اس کی گِٹ ہب ریپازٹریز سے بالکل الگ منصوبہ ہے، جسے انس رشید نے بنایا اور وہی اسے سنبھالتے ہیں۔ اسے ganjoor.net کے مالکان کی نہ تائید حاصل ہے، نہ وہ اسے چلاتے ہیں، اور نہ ہی اس کے ذمہ دار ہیں۔</string>
|
||||
<string name="about_unofficial">غیر سرکاری۔ ”گنجور فار اینڈرائیڈ“ ganjoor.net کا ایک خودمختار اور غیر سرکاری اینڈرائیڈ کلائنٹ ہے۔ یہ ganjoor.net اور اس کی گِٹ ہب ریپازٹریز سے الگ منصوبہ ہے، جسے محمد انس رشید نے بنایا اور وہی اسے سنبھالتے ہیں۔ ganjoor.net کے مالکان اسے نہ چلاتے ہیں اور نہ اس کا انتظام کرتے ہیں۔</string>
|
||||
<string name="about_intro">گنجور فار اینڈرائیڈ آزاد سافٹ ویئر ہے۔ کلام، فونٹس اور تمام لائبریریاں ذیل میں درج ہیں؛ مکمل لائسنس پڑھنے کے لیے کسی اندراج پر ٹیپ کریں۔</string>
|
||||
<string name="no_definition">اس لفظ کا کوئی اندراج نہیں ملا۔</string>
|
||||
<string name="source_wiktionary">ویکی لغت — فارسی سے انگریزی (CC BY-SA 3.0)</string>
|
||||
@@ -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>
|
||||
@@ -64,7 +64,7 @@
|
||||
<string name="load_more">Load more</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="about">About & licences</string>
|
||||
<string name="about_unofficial">Unofficial. Ganjoor for Android is an independent, unofficial Android client for ganjoor.net. It is a separate project from ganjoor.net and its GitHub repositories, built and maintained by Anas Rashid. It is not affiliated with, endorsed by, or managed by the owners of ganjoor.net, who bear no responsibility for it.</string>
|
||||
<string name="about_unofficial">Unofficial. Ganjoor for Android is an independent, unofficial Android client for ganjoor.net. It is a separate project from ganjoor.net and its GitHub repositories, built and maintained by Muhammad Anas Rashid. The owners of ganjoor.net do not run or manage it.</string>
|
||||
<string name="about_intro">Ganjoor for Android is free software. The poems, the fonts and every library it is built on are credited below; tap an entry to read its full licence.</string>
|
||||
<string name="no_definition">No entry for this word.</string>
|
||||
<string name="source_wiktionary">Wiktionary — Persian to English (CC BY-SA 3.0)</string>
|
||||
@@ -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