Dictionary coverage, pronunciation, and chapter order #1
@ -77,6 +77,15 @@ then with an affix stripped, then the parts of a ZWNJ compound. The lemma step i
|
||||
classical verse readable — `افتاد` is only findable as `افتادن`, and Wiktionary ships 149,589
|
||||
form→lemma pairs that make that possible.
|
||||
|
||||
Each entry carries its pronunciation where Wiktionary has one — 101,306 of them, tagged with the
|
||||
variety, Classical Persian first, because a word in a 14th-century ghazal was not said the way
|
||||
Tehran says it now. Urdu Wiktionary adds the vowelled spelling and the syllable split in Urdu
|
||||
script. When nothing matches at all, the sheet offers near words ranked by shared letters.
|
||||
|
||||
Poems that Ganjoor has recordings for show a play button, streamed rather than stored: a famous
|
||||
ghazal often has a dozen readings, and downloading them would dwarf the poems. It is the one
|
||||
part of the app that needs a connection, and it simply doesn't appear without one.
|
||||
|
||||
See [`tools/README.md`](tools/README.md) to rebuild it, and for the licensing of each source.
|
||||
|
||||
## Where the poems come from
|
||||
|
||||
@ -46,7 +46,7 @@ class MainActivity : ComponentActivity() {
|
||||
val systemInDark = resources.configuration.uiMode and
|
||||
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||
window.setBackgroundDrawable(
|
||||
windowBackground(settings.value.theme, systemInDark).toDrawable()
|
||||
windowBackground(settings.value.theme, systemInDark, settings.value.oled).toDrawable()
|
||||
)
|
||||
|
||||
enableEdgeToEdge()
|
||||
@ -62,7 +62,7 @@ class MainActivity : ComponentActivity() {
|
||||
// navigates right-to-left whichever UI language is selected.
|
||||
LocalLayoutDirection provides LayoutDirection.Rtl,
|
||||
) {
|
||||
GanjoorTheme(settings.value.theme, settings.value.language) {
|
||||
GanjoorTheme(settings.value.theme, settings.value.language, settings.value.oled) {
|
||||
GanjoorApp()
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,6 +145,18 @@ private data class LiveCat(val poems: List<LivePoem> = emptyList())
|
||||
@Serializable
|
||||
private data class LivePoem(val id: Int = 0, val excerpt: String? = null)
|
||||
|
||||
/** A reading of a whole poem, hosted by Ganjoor. */
|
||||
@Serializable
|
||||
data class Recitation(
|
||||
val id: Int = 0,
|
||||
val audioTitle: String = "",
|
||||
val audioArtist: String = "",
|
||||
val mp3Url: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class LivePoemRecitations(val recitations: List<Recitation> = emptyList())
|
||||
|
||||
private val liveJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Serializable
|
||||
@ -355,6 +367,27 @@ object Ganjoor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Readings of a poem, by the people who recorded them for ganjoor.net.
|
||||
*
|
||||
* Streamed, never stored: the files are a few hundred kilobytes each and there are often a
|
||||
* dozen readings of a famous ghazal, so downloading them all would dwarf the poems. Offline
|
||||
* mode therefore has none of this, which is honest — a recording is the one thing here that
|
||||
* genuinely needs the network.
|
||||
*/
|
||||
suspend fun recitations(poemId: Int): List<Recitation> = withContext(Dispatchers.IO) {
|
||||
if (offline || poemId == 0) return@withContext emptyList()
|
||||
val url = liveBase.newBuilder()
|
||||
.addPathSegments("api/ganjoor/poem/$poemId")
|
||||
.addQueryParameter("recitations", "true")
|
||||
.addQueryParameter("verseDetails", "false")
|
||||
.build()
|
||||
runCatching {
|
||||
liveJson.decodeFromString<LivePoemRecitations>(fetch(url)).recitations
|
||||
.filter { it.mp3Url.isNotBlank() }
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a poet's whole tree — biography, every category and every poem — for offline reading.
|
||||
* Re-running it is cheap: anything already on disk is skipped, which is also how a download
|
||||
|
||||
@ -126,6 +126,7 @@ fun PoemScreen(
|
||||
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,
|
||||
|
||||
@ -78,6 +78,15 @@ fun ReadingSettingsSheet(onDismiss: () -> Unit) {
|
||||
settings.update { it.copy(theme = mode) }
|
||||
}
|
||||
|
||||
// Only means anything on a dark theme, so it sits with them and says so.
|
||||
Toggle(
|
||||
title = R.string.oled,
|
||||
note = R.string.oled_note,
|
||||
checked = prefs.oled,
|
||||
) { on ->
|
||||
settings.update { it.copy(oled = on) }
|
||||
}
|
||||
|
||||
Label(R.string.font)
|
||||
Chips(ReadingFont.entries, prefs.font, { stringResource(it.label) }) { font ->
|
||||
settings.update { it.copy(font = font) }
|
||||
|
||||
141
app/src/main/java/com/ganjoor/android/ui/RecitationPlayer.kt
Normal file
141
app/src/main/java/com/ganjoor/android/ui/RecitationPlayer.kt
Normal file
@ -0,0 +1,141 @@
|
||||
package com.ganjoor.android.ui
|
||||
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
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.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ganjoor.android.R
|
||||
import com.ganjoor.android.data.Ganjoor
|
||||
import com.ganjoor.android.data.Recitation
|
||||
|
||||
/**
|
||||
* Plays a reading of the poem, streamed from Ganjoor.
|
||||
*
|
||||
* ponytail: the platform's MediaPlayer rather than ExoPlayer — one URL, play and pause, no
|
||||
* playlist or seeking to justify a media library. Nothing is cached, so this is the one part of
|
||||
* the app that needs a connection; it simply doesn't appear when there is no reading or no
|
||||
* network.
|
||||
*/
|
||||
@Composable
|
||||
fun RecitationPlayer(poemId: Int) {
|
||||
val recitations by produceState(emptyList<Recitation>(), poemId) {
|
||||
value = Ganjoor.recitations(poemId)
|
||||
}
|
||||
if (recitations.isEmpty()) return
|
||||
|
||||
var chosen by remember(poemId) { mutableIntStateOf(0) }
|
||||
var playing by remember(poemId) { mutableStateOf(false) }
|
||||
var loading by remember(poemId) { mutableStateOf(false) }
|
||||
var picking by remember { mutableStateOf(false) }
|
||||
|
||||
val player = remember {
|
||||
MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
// A reading left playing when the screen goes would keep the whole poem in memory.
|
||||
DisposableEffect(player) { onDispose { runCatching { player.release() } } }
|
||||
|
||||
val recitation = recitations.getOrNull(chosen) ?: return
|
||||
// Changing reader stops whatever was playing, so the two never overlap.
|
||||
DisposableEffect(recitation.mp3Url) {
|
||||
runCatching { player.reset() }
|
||||
playing = false
|
||||
loading = false
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
fun toggle() {
|
||||
if (playing) {
|
||||
runCatching { player.pause() }
|
||||
playing = false
|
||||
return
|
||||
}
|
||||
if (player.currentPosition > 0) {
|
||||
runCatching { player.start() }.onSuccess { playing = true }
|
||||
return
|
||||
}
|
||||
loading = true
|
||||
runCatching {
|
||||
player.reset()
|
||||
player.setDataSource(recitation.mp3Url)
|
||||
player.setOnPreparedListener { it.start(); playing = true; loading = false }
|
||||
player.setOnCompletionListener { playing = false }
|
||||
player.setOnErrorListener { _, _, _ -> playing = false; loading = false; true }
|
||||
player.prepareAsync()
|
||||
}.onFailure { loading = false }
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
IconButton(onClick = ::toggle) {
|
||||
when {
|
||||
loading -> CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
// Core Material icons ship no pause glyph, and the extended set is 4 MB for one.
|
||||
playing -> Icon(painterResource(R.drawable.ic_pause), stringResource(R.string.pause))
|
||||
else -> Icon(Icons.Default.PlayArrow, stringResource(R.string.play_recitation))
|
||||
}
|
||||
}
|
||||
TextButton(
|
||||
onClick = { if (recitations.size > 1) picking = true },
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
) {
|
||||
Text(
|
||||
text = recitation.audioArtist.ifBlank { stringResource(R.string.play_recitation) },
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (recitations.size > 1) {
|
||||
Text(
|
||||
text = "${chosen + 1}/${recitations.size}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = picking, onDismissRequest = { picking = false }) {
|
||||
recitations.forEachIndexed { index, item ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(item.audioArtist.ifBlank { item.audioTitle }) },
|
||||
onClick = { chosen = index; picking = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,9 +18,6 @@ enum class ThemeMode(@StringRes val label: Int) {
|
||||
Dark(R.string.theme_dark),
|
||||
Sepia(R.string.theme_sepia),
|
||||
SepiaDark(R.string.theme_sepia_dark),
|
||||
|
||||
/** Pure black, so OLED panels can switch the pixels off entirely. */
|
||||
Black(R.string.theme_black),
|
||||
}
|
||||
|
||||
enum class ReadingFont(@StringRes val label: Int) {
|
||||
@ -58,6 +55,8 @@ data class Prefs(
|
||||
val showSummaries: Boolean = false,
|
||||
val language: Language = Language.Fa,
|
||||
val offline: Boolean = false,
|
||||
/** True black backgrounds, applied to whichever dark theme is in use. */
|
||||
val oled: Boolean = false,
|
||||
val poetSort: PoetSort = PoetSort.Default,
|
||||
)
|
||||
|
||||
@ -74,6 +73,8 @@ class Settings(context: Context) {
|
||||
showSummaries = prefs.getBoolean("showSummaries", false),
|
||||
language = languageOrDefault(prefs.getString(KEY_LANGUAGE, null)),
|
||||
offline = prefs.getBoolean("offline", false),
|
||||
// "Black" used to be a sixth theme; it is a flag on the dark ones now.
|
||||
oled = prefs.getBoolean("oled", prefs.getString("theme", null) == "Black"),
|
||||
poetSort = enumOrDefault(prefs.getString("poetSort", null), PoetSort.Default),
|
||||
)
|
||||
)
|
||||
@ -91,6 +92,7 @@ class Settings(context: Context) {
|
||||
putBoolean("showSummaries", p.showSummaries)
|
||||
putString(KEY_LANGUAGE, p.language.tag)
|
||||
putBoolean("offline", p.offline)
|
||||
putBoolean("oled", p.oled)
|
||||
putString("poetSort", p.poetSort.name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.ganjoor.android.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@ -22,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -93,19 +95,29 @@ fun WordSheet(word: String, onDismiss: () -> Unit) {
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (sounds.isNotEmpty()) {
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
sounds.forEach { sound ->
|
||||
Column {
|
||||
// IPA is Latin-script, so it reads left to right whatever the UI does
|
||||
LeftToRight {
|
||||
Text(sound.text, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
if (sound.label.isNotBlank()) {
|
||||
// One direction for the whole block, and one pronunciation per line. Flowing
|
||||
// them side by side put Latin IPA and Urdu spelling in the same right-to-left
|
||||
// run, which reordered the chips and left each label under someone else's value.
|
||||
LeftToRight {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
sounds.forEach { sound ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = sound.label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
text = sound.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (sound.label.isNotBlank()) {
|
||||
Text(
|
||||
text = sound.label,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,14 @@ package com.ganjoor.android.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
@ -66,36 +68,6 @@ private val DarkScheme = darkColorScheme(
|
||||
outline = Color(0xFF899393),
|
||||
)
|
||||
|
||||
/**
|
||||
* True black, for OLED panels: a black pixel is an unlit pixel, so a night-time reading session
|
||||
* costs noticeably less battery than the regular dark theme's dark grey. Surfaces step up in
|
||||
* near-black greys so cards and sheets stay distinguishable without lighting the whole screen.
|
||||
*/
|
||||
private val BlackScheme = darkColorScheme(
|
||||
primary = Color(0xFF80D4DA),
|
||||
onPrimary = Color(0xFF00363A),
|
||||
primaryContainer = Color(0xFF004F53),
|
||||
onPrimaryContainer = Color(0xFF9CF1F6),
|
||||
secondary = Color(0xFFFFB873),
|
||||
onSecondary = Color(0xFF4A2800),
|
||||
secondaryContainer = Color(0xFF693C00),
|
||||
onSecondaryContainer = Color(0xFFFFDCBE),
|
||||
background = Color(0xFF000000),
|
||||
onBackground = Color(0xFFE3E3E3),
|
||||
surface = Color(0xFF000000),
|
||||
onSurface = Color(0xFFE3E3E3),
|
||||
surfaceVariant = Color(0xFF1C1C1C),
|
||||
onSurfaceVariant = Color(0xFFBDBDBD),
|
||||
outline = Color(0xFF6E6E6E),
|
||||
surfaceBright = Color(0xFF262626),
|
||||
surfaceDim = Color(0xFF000000),
|
||||
surfaceContainerLowest = Color(0xFF000000),
|
||||
surfaceContainerLow = Color(0xFF0A0A0A),
|
||||
surfaceContainer = Color(0xFF101010),
|
||||
surfaceContainerHigh = Color(0xFF1A1A1A),
|
||||
surfaceContainerHighest = Color(0xFF242424),
|
||||
)
|
||||
|
||||
// Aged paper, for long reading sessions.
|
||||
private val SepiaScheme = lightColorScheme(
|
||||
primary = Color(0xFF7A4E24),
|
||||
@ -153,28 +125,61 @@ private val SepiaDarkScheme = darkColorScheme(
|
||||
* so the gap between the window appearing and the first frame matches the theme instead of
|
||||
* flashing the platform's white. A static XML theme can't express sepia or OLED, hence this.
|
||||
*/
|
||||
fun windowBackground(mode: ThemeMode, systemInDark: Boolean): Int = when (mode) {
|
||||
ThemeMode.Light -> LightScheme
|
||||
ThemeMode.Dark -> DarkScheme
|
||||
ThemeMode.Sepia -> SepiaScheme
|
||||
ThemeMode.SepiaDark -> SepiaDarkScheme
|
||||
ThemeMode.Black -> BlackScheme
|
||||
ThemeMode.System -> if (systemInDark) DarkScheme else LightScheme
|
||||
}.background.toArgb()
|
||||
fun windowBackground(mode: ThemeMode, systemInDark: Boolean, oled: Boolean): Int {
|
||||
val dark = when (mode) {
|
||||
ThemeMode.System -> systemInDark
|
||||
ThemeMode.Light, ThemeMode.Sepia -> false
|
||||
ThemeMode.Dark, ThemeMode.SepiaDark -> true
|
||||
}
|
||||
if (oled && dark) return Color.Black.toArgb()
|
||||
return when (mode) {
|
||||
ThemeMode.Light -> LightScheme
|
||||
ThemeMode.Dark -> DarkScheme
|
||||
ThemeMode.Sepia -> SepiaScheme
|
||||
ThemeMode.SepiaDark -> SepiaDarkScheme
|
||||
ThemeMode.System -> if (systemInDark) DarkScheme else LightScheme
|
||||
}.background.toArgb()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a dark scheme to true black for OLED panels, where an unlit pixel costs no power.
|
||||
*
|
||||
* Only the surfaces move, and they move towards black rather than being replaced by it, so sepia
|
||||
* night keeps its warmth instead of turning into the grey dark theme. Text and accents are left
|
||||
* exactly as they were.
|
||||
*/
|
||||
private fun ColorScheme.asOled(): ColorScheme = copy(
|
||||
background = Color.Black,
|
||||
surface = Color.Black,
|
||||
surfaceDim = Color.Black,
|
||||
surfaceContainerLowest = Color.Black,
|
||||
surfaceContainerLow = lerp(surfaceContainerLow, Color.Black, 0.80f),
|
||||
surfaceContainer = lerp(surfaceContainer, Color.Black, 0.74f),
|
||||
surfaceContainerHigh = lerp(surfaceContainerHigh, Color.Black, 0.64f),
|
||||
surfaceContainerHighest = lerp(surfaceContainerHighest, Color.Black, 0.54f),
|
||||
surfaceBright = lerp(surfaceBright, Color.Black, 0.46f),
|
||||
surfaceVariant = lerp(surfaceVariant, Color.Black, 0.52f),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GanjoorTheme(mode: ThemeMode, language: Language, content: @Composable () -> Unit) {
|
||||
fun GanjoorTheme(
|
||||
mode: ThemeMode,
|
||||
language: Language,
|
||||
oled: Boolean,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val dark = when (mode) {
|
||||
ThemeMode.System -> isSystemInDarkTheme()
|
||||
ThemeMode.Light, ThemeMode.Sepia -> false
|
||||
ThemeMode.Dark, ThemeMode.SepiaDark, ThemeMode.Black -> true
|
||||
ThemeMode.Dark, ThemeMode.SepiaDark -> true
|
||||
}
|
||||
val scheme = when (mode) {
|
||||
val chosen = when (mode) {
|
||||
ThemeMode.Sepia -> SepiaScheme
|
||||
ThemeMode.SepiaDark -> SepiaDarkScheme
|
||||
ThemeMode.Black -> BlackScheme
|
||||
else -> if (dark) DarkScheme else LightScheme
|
||||
}
|
||||
// Only dark schemes have anything to gain from it.
|
||||
val scheme = if (oled && dark) chosen.asOled() else chosen
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
|
||||
9
app/src/main/res/drawable/ic_pause.xml
Normal file
9
app/src/main/res/drawable/ic_pause.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19h4V5H6v14zM14,5v14h4V5h-4z" />
|
||||
</vector>
|
||||
@ -58,7 +58,6 @@
|
||||
<string name="search_needs_connection">جستوجو به اینترنت نیاز دارد. حالت برونخط را خاموش کنید.</string>
|
||||
<string name="load_more">بیشتر</string>
|
||||
<string name="done">تمام</string>
|
||||
<string name="theme_black">سیاه (OLED)</string>
|
||||
<string name="about">درباره و پروانهها</string>
|
||||
<string name="about_intro">گنجور برای اندروید نرمافزار آزاد است. شعرها، قلمها و همهٔ کتابخانههایی که این برنامه بر آنها ساخته شده در زیر آمدهاند؛ برای خواندن متن کامل پروانه روی هر مورد بزنید.</string>
|
||||
<string name="no_definition">برای این واژه مدخلی یافت نشد.</string>
|
||||
@ -69,4 +68,8 @@
|
||||
<string name="source_urwiktionary">ویکیواژهٔ اردو — اردو به اردو (CC BY-SA 3.0)</string>
|
||||
<string name="did_you_mean">واژههای نزدیک</string>
|
||||
<string name="source_wiktionary_ar">ویکیواژه — عربی به انگلیسی (CC BY-SA 3.0)</string>
|
||||
<string name="oled">پسزمینهٔ سیاه (OLED)</string>
|
||||
<string name="oled_note">روی پوستههای تاریک اعمال میشود و در نمایشگر OLED باتری کمتری میبرد</string>
|
||||
<string name="play_recitation">پخش خوانش</string>
|
||||
<string name="pause">توقف</string>
|
||||
</resources>
|
||||
|
||||
@ -58,7 +58,6 @@
|
||||
<string name="search_needs_connection">تلاش کے لیے انٹرنیٹ درکار ہے۔ آف لائن موڈ بند کریں۔</string>
|
||||
<string name="load_more">مزید</string>
|
||||
<string name="done">مکمل</string>
|
||||
<string name="theme_black">سیاہ (OLED)</string>
|
||||
<string name="about">تعارف اور لائسنس</string>
|
||||
<string name="about_intro">گنجور فار اینڈرائیڈ آزاد سافٹ ویئر ہے۔ کلام، فونٹس اور تمام لائبریریاں ذیل میں درج ہیں؛ مکمل لائسنس پڑھنے کے لیے کسی اندراج پر ٹیپ کریں۔</string>
|
||||
<string name="no_definition">اس لفظ کا کوئی اندراج نہیں ملا۔</string>
|
||||
@ -69,4 +68,8 @@
|
||||
<string name="source_urwiktionary">اردو ویکی لغت — اردو سے اردو (CC BY-SA 3.0)</string>
|
||||
<string name="did_you_mean">ملتے جلتے الفاظ</string>
|
||||
<string name="source_wiktionary_ar">ویکی لغت — عربی سے انگریزی (CC BY-SA 3.0)</string>
|
||||
<string name="oled">سیاہ پس منظر (OLED)</string>
|
||||
<string name="oled_note">تاریک تھیمز پر لاگو ہوتا ہے؛ OLED اسکرین پر بیٹری بچاتا ہے</string>
|
||||
<string name="play_recitation">قرات سنیں</string>
|
||||
<string name="pause">وقفہ</string>
|
||||
</resources>
|
||||
|
||||
@ -63,7 +63,6 @@
|
||||
<string name="search_needs_connection">Search needs a connection. Turn off offline mode to use it.</string>
|
||||
<string name="load_more">Load more</string>
|
||||
<string name="done">Done</string>
|
||||
<string name="theme_black">Black (OLED)</string>
|
||||
<string name="about">About & licences</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>
|
||||
@ -74,4 +73,8 @@
|
||||
<string name="source_urwiktionary">Urdu Wiktionary — Urdu to Urdu (CC BY-SA 3.0)</string>
|
||||
<string name="did_you_mean">Similar words</string>
|
||||
<string name="source_wiktionary_ar">Wiktionary — Arabic to English (CC BY-SA 3.0)</string>
|
||||
<string name="oled">Black backgrounds (OLED)</string>
|
||||
<string name="oled_note">Applies to the dark themes; saves power on OLED screens</string>
|
||||
<string name="play_recitation">Play recitation</string>
|
||||
<string name="pause">Pause</string>
|
||||
</resources>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user