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:
parent
0df736fd89
commit
0d083b6638
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) }
|
||||||
|
|||||||
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),
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
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="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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 & licences</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="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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user