Compare commits

...
Author SHA1 Message Date
anasandClaude Opus 5 e284872eaa 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>
2026-10-07 16:10:21 +02:00
anasandClaude Opus 5 50bbe0b729 Correct the unofficial statement: separate, not unendorsed
The first wording claimed this app is "not affiliated with, endorsed by" the
owners of ganjoor.net and that they "bear no responsibility for it". That
overstates the distance: permission to build it was given. What is true, and
all that needed saying, is that this is a separate project and they do not run
or manage it. The denial of endorsement is gone from all three languages and
from both places in the README.

The author is named in full — Muhammad Anas Rashid — everywhere it appears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 15:12:11 +02:00
anasandClaude Opus 5 5d12a1c29c Merge 0.3.0: pinned poets, list view, and the unofficial-client statement
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 14:59:02 +02:00
7 changed files with 453 additions and 94 deletions

No files matched your search

+6 -7
View File
@@ -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,
)
}
}
+5 -1
View File
@@ -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>
+5 -1
View File
@@ -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>
+5 -1
View File
@@ -64,7 +64,7 @@
<string name="load_more">Load more</string>
<string name="done">Done</string>
<string name="about">About &amp; 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>