Make OLED a switch on the dark themes, and stream recitations

OLED was a sixth theme, which meant choosing it threw away sepia night. It is
a switch now, applied to whichever dark theme is in use: the surfaces move
towards black rather than being replaced by it, so sepia night keeps its warmth
and only the background goes truly black. Text and accents are untouched.
Anyone who had picked the old Black theme comes out as Dark with the switch on.

Recitations: Ganjoor hosts readings of the poems — twenty of Hafez's first
ghazal alone — and the poem screen now plays them. Streamed, never stored,
because a dozen readings of one ghazal would dwarf the poems themselves, which
also makes this the one part of the app that genuinely needs a connection; it
doesn't appear without one. The platform's MediaPlayer rather than ExoPlayer:
one URL, play and pause, nothing to justify a media library.

Pronunciations were laid out as a flowing row, which put Latin IPA and Urdu
spelling in the same right-to-left run — the chips reordered and each label
ended up under someone else's value. They are one per line now, in a single
direction.

Verified on an API 36 emulator: sepia night with the switch on is black with
warm cards; the player shows play on arrival with no network call, and streams
on press.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anas Rashid 2026-10-04 18:05:21 +02:00
parent 0df736fd89
commit 0d083b6638
13 changed files with 291 additions and 61 deletions

View File

@ -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 classical verse readable — `افتاد` is only findable as `افتادن`, and Wiktionary ships 149,589
form→lemma pairs that make that possible. 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. See [`tools/README.md`](tools/README.md) to rebuild it, and for the licensing of each source.
## Where the poems come from ## Where the poems come from

View File

@ -46,7 +46,7 @@ class MainActivity : ComponentActivity() {
val systemInDark = resources.configuration.uiMode and val systemInDark = resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
window.setBackgroundDrawable( window.setBackgroundDrawable(
windowBackground(settings.value.theme, systemInDark).toDrawable() windowBackground(settings.value.theme, systemInDark, settings.value.oled).toDrawable()
) )
enableEdgeToEdge() enableEdgeToEdge()
@ -62,7 +62,7 @@ class MainActivity : ComponentActivity() {
// navigates right-to-left whichever UI language is selected. // navigates right-to-left whichever UI language is selected.
LocalLayoutDirection provides LayoutDirection.Rtl, LocalLayoutDirection provides LayoutDirection.Rtl,
) { ) {
GanjoorTheme(settings.value.theme, settings.value.language) { GanjoorTheme(settings.value.theme, settings.value.language, settings.value.oled) {
GanjoorApp() GanjoorApp()
} }
} }

View File

@ -145,6 +145,18 @@ private data class LiveCat(val poems: List<LivePoem> = emptyList())
@Serializable @Serializable
private data class LivePoem(val id: Int = 0, val excerpt: String? = null) 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 } private val liveJson = Json { ignoreUnknownKeys = true }
@Serializable @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. * 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 * Re-running it is cheap: anything already on disk is skipped, which is also how a download

View File

@ -126,6 +126,7 @@ fun PoemScreen(
item { item {
Column(Modifier.padding(bottom = 12.dp)) { Column(Modifier.padding(bottom = 12.dp)) {
Breadcrumbs(poem.fullTitle, poem.fullUrl.ifBlank { fullUrl }, onCategory) Breadcrumbs(poem.fullTitle, poem.fullUrl.ifBlank { fullUrl }, onCategory)
RecitationPlayer(poem.id)
poem.metre?.rhythm?.let { rhythm -> poem.metre?.rhythm?.let { rhythm ->
Text( Text(
text = rhythm, text = rhythm,

View File

@ -78,6 +78,15 @@ fun ReadingSettingsSheet(onDismiss: () -> Unit) {
settings.update { it.copy(theme = mode) } 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) Label(R.string.font)
Chips(ReadingFont.entries, prefs.font, { stringResource(it.label) }) { font -> Chips(ReadingFont.entries, prefs.font, { stringResource(it.label) }) { font ->
settings.update { it.copy(font = font) } settings.update { it.copy(font = font) }

View 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 },
)
}
}
}
}

View File

@ -18,9 +18,6 @@ enum class ThemeMode(@StringRes val label: Int) {
Dark(R.string.theme_dark), Dark(R.string.theme_dark),
Sepia(R.string.theme_sepia), Sepia(R.string.theme_sepia),
SepiaDark(R.string.theme_sepia_dark), 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) { enum class ReadingFont(@StringRes val label: Int) {
@ -58,6 +55,8 @@ data class Prefs(
val showSummaries: Boolean = false, val showSummaries: Boolean = false,
val language: Language = Language.Fa, val language: Language = Language.Fa,
val offline: Boolean = false, val offline: Boolean = false,
/** True black backgrounds, applied to whichever dark theme is in use. */
val oled: Boolean = false,
val poetSort: PoetSort = PoetSort.Default, val poetSort: PoetSort = PoetSort.Default,
) )
@ -74,6 +73,8 @@ class Settings(context: Context) {
showSummaries = prefs.getBoolean("showSummaries", false), showSummaries = prefs.getBoolean("showSummaries", false),
language = languageOrDefault(prefs.getString(KEY_LANGUAGE, null)), language = languageOrDefault(prefs.getString(KEY_LANGUAGE, null)),
offline = prefs.getBoolean("offline", false), 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), poetSort = enumOrDefault(prefs.getString("poetSort", null), PoetSort.Default),
) )
) )
@ -91,6 +92,7 @@ class Settings(context: Context) {
putBoolean("showSummaries", p.showSummaries) putBoolean("showSummaries", p.showSummaries)
putString(KEY_LANGUAGE, p.language.tag) putString(KEY_LANGUAGE, p.language.tag)
putBoolean("offline", p.offline) putBoolean("offline", p.offline)
putBoolean("oled", p.oled)
putString("poetSort", p.poetSort.name) putString("poetSort", p.poetSort.name)
} }
} }

View File

@ -2,6 +2,7 @@ package com.ganjoor.android.ui
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -22,6 +23,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -93,19 +95,29 @@ fun WordSheet(word: String, onDismiss: () -> Unit) {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
if (sounds.isNotEmpty()) { if (sounds.isNotEmpty()) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) { // One direction for the whole block, and one pronunciation per line. Flowing
sounds.forEach { sound -> // them side by side put Latin IPA and Urdu spelling in the same right-to-left
Column { // run, which reordered the chips and left each label under someone else's value.
// IPA is Latin-script, so it reads left to right whatever the UI does LeftToRight {
LeftToRight { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(sound.text, style = MaterialTheme.typography.bodyMedium) sounds.forEach { sound ->
} Row(
if (sound.label.isNotBlank()) { modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text( Text(
text = sound.label, text = sound.text,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f, fill = false),
) )
if (sound.label.isNotBlank()) {
Text(
text = sound.label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} }
} }
} }

View File

@ -2,12 +2,14 @@ package com.ganjoor.android.ui.theme
import android.app.Activity import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.drawable.toDrawable
@ -66,36 +68,6 @@ private val DarkScheme = darkColorScheme(
outline = Color(0xFF899393), 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. // Aged paper, for long reading sessions.
private val SepiaScheme = lightColorScheme( private val SepiaScheme = lightColorScheme(
primary = Color(0xFF7A4E24), 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 * 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. * 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) { fun windowBackground(mode: ThemeMode, systemInDark: Boolean, oled: Boolean): Int {
ThemeMode.Light -> LightScheme val dark = when (mode) {
ThemeMode.Dark -> DarkScheme ThemeMode.System -> systemInDark
ThemeMode.Sepia -> SepiaScheme ThemeMode.Light, ThemeMode.Sepia -> false
ThemeMode.SepiaDark -> SepiaDarkScheme ThemeMode.Dark, ThemeMode.SepiaDark -> true
ThemeMode.Black -> BlackScheme }
ThemeMode.System -> if (systemInDark) DarkScheme else LightScheme if (oled && dark) return Color.Black.toArgb()
}.background.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 @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) { val dark = when (mode) {
ThemeMode.System -> isSystemInDarkTheme() ThemeMode.System -> isSystemInDarkTheme()
ThemeMode.Light, ThemeMode.Sepia -> false 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.Sepia -> SepiaScheme
ThemeMode.SepiaDark -> SepiaDarkScheme ThemeMode.SepiaDark -> SepiaDarkScheme
ThemeMode.Black -> BlackScheme
else -> if (dark) DarkScheme else LightScheme 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 val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {

View 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>

View File

@ -58,7 +58,6 @@
<string name="search_needs_connection">جست‌وجو به اینترنت نیاز دارد. حالت برون‌خط را خاموش کنید.</string> <string name="search_needs_connection">جست‌وجو به اینترنت نیاز دارد. حالت برون‌خط را خاموش کنید.</string>
<string name="load_more">بیشتر</string> <string name="load_more">بیشتر</string>
<string name="done">تمام</string> <string name="done">تمام</string>
<string name="theme_black">سیاه (OLED)</string>
<string name="about">درباره و پروانه‌ها</string> <string name="about">درباره و پروانه‌ها</string>
<string name="about_intro">گنجور برای اندروید نرم‌افزار آزاد است. شعرها، قلم‌ها و همهٔ کتابخانه‌هایی که این برنامه بر آن‌ها ساخته شده در زیر آمده‌اند؛ برای خواندن متن کامل پروانه روی هر مورد بزنید.</string> <string name="about_intro">گنجور برای اندروید نرم‌افزار آزاد است. شعرها، قلم‌ها و همهٔ کتابخانه‌هایی که این برنامه بر آن‌ها ساخته شده در زیر آمده‌اند؛ برای خواندن متن کامل پروانه روی هر مورد بزنید.</string>
<string name="no_definition">برای این واژه مدخلی یافت نشد.</string> <string name="no_definition">برای این واژه مدخلی یافت نشد.</string>
@ -69,4 +68,8 @@
<string name="source_urwiktionary">ویکی‌واژهٔ اردو — اردو به اردو (CC BY-SA 3.0)</string> <string name="source_urwiktionary">ویکی‌واژهٔ اردو — اردو به اردو (CC BY-SA 3.0)</string>
<string name="did_you_mean">واژه‌های نزدیک</string> <string name="did_you_mean">واژه‌های نزدیک</string>
<string name="source_wiktionary_ar">ویکی‌واژه — عربی به انگلیسی (CC BY-SA 3.0)</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> </resources>

View File

@ -58,7 +58,6 @@
<string name="search_needs_connection">تلاش کے لیے انٹرنیٹ درکار ہے۔ آف لائن موڈ بند کریں۔</string> <string name="search_needs_connection">تلاش کے لیے انٹرنیٹ درکار ہے۔ آف لائن موڈ بند کریں۔</string>
<string name="load_more">مزید</string> <string name="load_more">مزید</string>
<string name="done">مکمل</string> <string name="done">مکمل</string>
<string name="theme_black">سیاہ (OLED)</string>
<string name="about">تعارف اور لائسنس</string> <string name="about">تعارف اور لائسنس</string>
<string name="about_intro">گنجور فار اینڈرائیڈ آزاد سافٹ ویئر ہے۔ کلام، فونٹس اور تمام لائبریریاں ذیل میں درج ہیں؛ مکمل لائسنس پڑھنے کے لیے کسی اندراج پر ٹیپ کریں۔</string> <string name="about_intro">گنجور فار اینڈرائیڈ آزاد سافٹ ویئر ہے۔ کلام، فونٹس اور تمام لائبریریاں ذیل میں درج ہیں؛ مکمل لائسنس پڑھنے کے لیے کسی اندراج پر ٹیپ کریں۔</string>
<string name="no_definition">اس لفظ کا کوئی اندراج نہیں ملا۔</string> <string name="no_definition">اس لفظ کا کوئی اندراج نہیں ملا۔</string>
@ -69,4 +68,8 @@
<string name="source_urwiktionary">اردو ویکی لغت — اردو سے اردو (CC BY-SA 3.0)</string> <string name="source_urwiktionary">اردو ویکی لغت — اردو سے اردو (CC BY-SA 3.0)</string>
<string name="did_you_mean">ملتے جلتے الفاظ</string> <string name="did_you_mean">ملتے جلتے الفاظ</string>
<string name="source_wiktionary_ar">ویکی لغت — عربی سے انگریزی (CC BY-SA 3.0)</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> </resources>

View File

@ -63,7 +63,6 @@
<string name="search_needs_connection">Search needs a connection. Turn off offline mode to use it.</string> <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="load_more">Load more</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="theme_black">Black (OLED)</string>
<string name="about">About &amp; licences</string> <string name="about">About &amp; 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="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="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="source_urwiktionary">Urdu Wiktionary — Urdu to Urdu (CC BY-SA 3.0)</string>
<string name="did_you_mean">Similar words</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="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> </resources>