Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
270c28c9c9 | ||
|
|
3407f010c9 | ||
|
|
701b984f4b | ||
|
|
41d776a35d | ||
|
|
bcdad2e699 | ||
|
|
ce26676d4e | ||
|
|
488f48ad39 | ||
|
|
936fd3cf3b |
No files matched your search
@@ -16,7 +16,7 @@ val keystoreProperties = Properties().apply {
|
|||||||
|
|
||||||
// A delivered file should say what it is without being opened.
|
// A delivered file should say what it is without being opened.
|
||||||
base {
|
base {
|
||||||
archivesName = "ganjoor-0.3.0"
|
archivesName = "ganjoor-0.4.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -32,8 +32,8 @@ android {
|
|||||||
// Pre-1.0 while the app is still being shaped. versionCode only ever climbs: F-Droid
|
// Pre-1.0 while the app is still being shaped. versionCode only ever climbs: F-Droid
|
||||||
// refuses an update that does not, and one changelog file per code lives in
|
// refuses an update that does not, and one changelog file per code lives in
|
||||||
// fastlane/metadata/android/*/changelogs/.
|
// fastlane/metadata/android/*/changelogs/.
|
||||||
versionCode = 5
|
versionCode = 6
|
||||||
versionName = "0.3.0"
|
versionName = "0.4.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,14 @@ fun PoemScreen(
|
|||||||
val sidePanelOpen = LocalSidePanelOpen.current
|
val sidePanelOpen = LocalSidePanelOpen.current
|
||||||
LaunchedEffect(sidePanelOpen) { if (sidePanelOpen) tapped = null }
|
LaunchedEffect(sidePanelOpen) { if (sidePanelOpen) tapped = null }
|
||||||
|
|
||||||
|
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||||
|
// Whether the dictionary gets a column of its own is not a question about the window but
|
||||||
|
// about what is left of this page once the columns have taken theirs. On a book-style
|
||||||
|
// foldable held open in portrait the page is already down to ~450dp, and a panel beside it
|
||||||
|
// left the verse a couple of characters a line. Where it does not fit, the sheet is the
|
||||||
|
// better answer: it covers the foot of the poem but leaves the lines whole.
|
||||||
|
val roomForPanel = wide && maxWidth - DictionaryPanelWidth >= MinPageMeasure
|
||||||
|
|
||||||
Load(
|
Load(
|
||||||
key = fullUrl,
|
key = fullUrl,
|
||||||
block = { Ganjoor.poem(fullUrl) },
|
block = { Ganjoor.poem(fullUrl) },
|
||||||
@@ -169,20 +177,24 @@ fun PoemScreen(
|
|||||||
// couplet share a line, as ganjoor.net sets them on a desktop.
|
// couplet share a line, as ganjoor.net sets them on a desktop.
|
||||||
val side = if (wide) maxOf(20.dp, (maxWidth - 680.dp) / 2) else 20.dp
|
val side = if (wide) maxOf(20.dp, (maxWidth - 680.dp) / 2) else 20.dp
|
||||||
val sideBySide = wide && maxWidth - side * 2 >= 640.dp
|
val sideBySide = wide && maxWidth - side * 2 >= 640.dp
|
||||||
SelectionContainer {
|
Column(Modifier.fillMaxSize().padding(top = insets.calculateTopPadding())) {
|
||||||
|
// Above the text, not in it: as an item of the list the player was disposed the
|
||||||
|
// moment it scrolled off, which released the MediaPlayer and cut the reading off
|
||||||
|
// mid-line. Here it keeps playing, and stays in reach while you read further down.
|
||||||
|
RecitationPlayer(poem.id, Modifier.padding(start = side, end = side, top = 8.dp))
|
||||||
|
SelectionContainer(Modifier.weight(1f)) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(
|
||||||
start = side,
|
start = side,
|
||||||
end = side,
|
end = side,
|
||||||
top = insets.calculateTopPadding() + 8.dp,
|
top = 8.dp,
|
||||||
bottom = insets.calculateBottomPadding() + 32.dp,
|
bottom = insets.calculateBottomPadding() + 32.dp,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
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,
|
||||||
@@ -232,9 +244,10 @@ fun PoemScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (wide) {
|
if (roomForPanel) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = tapped != null,
|
visible = tapped != null,
|
||||||
enter = expandHorizontally() + fadeIn(),
|
enter = expandHorizontally() + fadeIn(),
|
||||||
@@ -252,7 +265,7 @@ fun PoemScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!wide) {
|
if (!roomForPanel) {
|
||||||
tapped?.let { tap ->
|
tapped?.let { tap ->
|
||||||
WordSheet(
|
WordSheet(
|
||||||
word = tap.word,
|
word = tap.word,
|
||||||
@@ -261,6 +274,7 @@ fun PoemScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The positions that make a line of verse; anything else (Single, Paragraph, Comment) is prose. */
|
/** The positions that make a line of verse; anything else (Single, Paragraph, Comment) is prose. */
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ganjoor.android.ui
|
|||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -15,11 +16,14 @@ import androidx.compose.material3.DropdownMenuItem
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
@@ -27,24 +31,40 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
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.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import com.ganjoor.android.R
|
import com.ganjoor.android.R
|
||||||
import com.ganjoor.android.data.Ganjoor
|
import com.ganjoor.android.data.Ganjoor
|
||||||
import com.ganjoor.android.data.Recitation
|
import com.ganjoor.android.data.Recitation
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
/** mm:ss, the only shape a reading's length ever needs. */
|
||||||
|
private fun clock(millis: Int): String {
|
||||||
|
val total = (millis / 1000).coerceAtLeast(0)
|
||||||
|
return "%d:%02d".format(total / 60, total % 60)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays a reading of the poem, streamed from Ganjoor.
|
* Plays a reading of the poem, streamed from Ganjoor.
|
||||||
*
|
*
|
||||||
* ponytail: the platform's MediaPlayer rather than ExoPlayer — one URL, play and pause, no
|
* Placed above the scrolling text rather than inside it, and deliberately so: as an item of the
|
||||||
* playlist or seeking to justify a media library. Nothing is cached, so this is the one part of
|
* LazyColumn it was disposed the moment it scrolled off, which released the MediaPlayer and cut
|
||||||
* the app that needs a connection; it simply doesn't appear when there is no reading or no
|
* the reading off mid-line. Pinned here it stays in composition for as long as the poem is open,
|
||||||
* network.
|
* and stays in reach — which is what a player is for while you are reading further down.
|
||||||
|
*
|
||||||
|
* ponytail: still the platform's MediaPlayer rather than ExoPlayer. One URL, and seeking within
|
||||||
|
* it, is not a media library's worth of work. 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
|
@Composable
|
||||||
fun RecitationPlayer(poemId: Int) {
|
fun RecitationPlayer(poemId: Int, modifier: Modifier = Modifier) {
|
||||||
val recitations by produceState(emptyList<Recitation>(), poemId) {
|
val recitations by produceState(emptyList<Recitation>(), poemId) {
|
||||||
value = Ganjoor.recitations(poemId)
|
value = Ganjoor.recitations(poemId)
|
||||||
}
|
}
|
||||||
@@ -54,6 +74,10 @@ fun RecitationPlayer(poemId: Int) {
|
|||||||
var playing by remember(poemId) { mutableStateOf(false) }
|
var playing by remember(poemId) { mutableStateOf(false) }
|
||||||
var loading by remember(poemId) { mutableStateOf(false) }
|
var loading by remember(poemId) { mutableStateOf(false) }
|
||||||
var picking by remember { mutableStateOf(false) }
|
var picking by remember { mutableStateOf(false) }
|
||||||
|
var duration by remember(poemId) { mutableIntStateOf(0) }
|
||||||
|
var position by remember(poemId) { mutableIntStateOf(0) }
|
||||||
|
// While a finger is on the slider the poll must not fight it for the handle.
|
||||||
|
var scrubbing by remember(poemId) { mutableStateOf<Float?>(null) }
|
||||||
|
|
||||||
val player = remember {
|
val player = remember {
|
||||||
MediaPlayer().apply {
|
MediaPlayer().apply {
|
||||||
@@ -65,7 +89,7 @@ fun RecitationPlayer(poemId: Int) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A reading left playing when the screen goes would keep the whole poem in memory.
|
// A reading left playing when the poem closes would keep the whole thing in memory.
|
||||||
DisposableEffect(player) { onDispose { runCatching { player.release() } } }
|
DisposableEffect(player) { onDispose { runCatching { player.release() } } }
|
||||||
|
|
||||||
val recitation = recitations.getOrNull(chosen) ?: return
|
val recitation = recitations.getOrNull(chosen) ?: return
|
||||||
@@ -74,16 +98,26 @@ fun RecitationPlayer(poemId: Int) {
|
|||||||
runCatching { player.reset() }
|
runCatching { player.reset() }
|
||||||
playing = false
|
playing = false
|
||||||
loading = false
|
loading = false
|
||||||
|
position = 0
|
||||||
|
duration = 0
|
||||||
onDispose { }
|
onDispose { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The handle follows the audio only while it is actually moving.
|
||||||
|
LaunchedEffect(playing) {
|
||||||
|
while (playing) {
|
||||||
|
runCatching { position = player.currentPosition }
|
||||||
|
delay(250)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun toggle() {
|
fun toggle() {
|
||||||
if (playing) {
|
if (playing) {
|
||||||
runCatching { player.pause() }
|
runCatching { player.pause() }
|
||||||
playing = false
|
playing = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (player.currentPosition > 0) {
|
if (duration > 0) {
|
||||||
runCatching { player.start() }.onSuccess { playing = true }
|
runCatching { player.start() }.onSuccess { playing = true }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,50 +125,96 @@ fun RecitationPlayer(poemId: Int) {
|
|||||||
runCatching {
|
runCatching {
|
||||||
player.reset()
|
player.reset()
|
||||||
player.setDataSource(recitation.mp3Url)
|
player.setDataSource(recitation.mp3Url)
|
||||||
player.setOnPreparedListener { it.start(); playing = true; loading = false }
|
player.setOnPreparedListener {
|
||||||
player.setOnCompletionListener { playing = false }
|
duration = it.duration
|
||||||
|
it.start()
|
||||||
|
playing = true
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
player.setOnCompletionListener { playing = false; position = duration }
|
||||||
player.setOnErrorListener { _, _, _ -> playing = false; loading = false; true }
|
player.setOnErrorListener { _, _, _ -> playing = false; loading = false; true }
|
||||||
player.prepareAsync()
|
player.prepareAsync()
|
||||||
}.onFailure { loading = false }
|
}.onFailure { loading = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Column(modifier.fillMaxWidth().padding(bottom = 8.dp)) {
|
||||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
IconButton(onClick = ::toggle) {
|
) {
|
||||||
when {
|
IconButton(onClick = ::toggle) {
|
||||||
loading -> CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
when {
|
||||||
// Core Material icons ship no pause glyph, and the extended set is 4 MB for one.
|
loading -> CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||||
playing -> Icon(painterResource(R.drawable.ic_pause), stringResource(R.string.pause))
|
// Core Material icons ship no pause glyph, and the extended set is 4 MB for one.
|
||||||
else -> Icon(Icons.Default.PlayArrow, stringResource(R.string.play_recitation))
|
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 },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextButton(
|
|
||||||
onClick = { if (recitations.size > 1) picking = true },
|
// Only once the length is known: a bar that cannot be dragged anywhere is furniture.
|
||||||
modifier = Modifier.weight(1f, fill = false),
|
if (duration > 0) {
|
||||||
) {
|
// Time runs left to right whatever the script, so the bar and its two times are laid
|
||||||
Text(
|
// out that way inside an otherwise right-to-left page.
|
||||||
text = recitation.audioArtist.ifBlank { stringResource(R.string.play_recitation) },
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
style = MaterialTheme.typography.labelLarge,
|
Row(
|
||||||
maxLines = 1,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
overflow = TextOverflow.Ellipsis,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
)
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
}
|
) {
|
||||||
if (recitations.size > 1) {
|
Text(
|
||||||
Text(
|
text = clock(scrubbing?.toInt() ?: position),
|
||||||
text = "${chosen + 1}/${recitations.size}",
|
style = MaterialTheme.typography.labelSmall,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
)
|
||||||
)
|
Slider(
|
||||||
}
|
value = scrubbing ?: position.toFloat().coerceIn(0f, duration.toFloat()),
|
||||||
DropdownMenu(expanded = picking, onDismissRequest = { picking = false }) {
|
onValueChange = { scrubbing = it },
|
||||||
recitations.forEachIndexed { index, item ->
|
onValueChangeFinished = {
|
||||||
DropdownMenuItem(
|
scrubbing?.let { target ->
|
||||||
text = { Text(item.audioArtist.ifBlank { item.audioTitle }) },
|
runCatching { player.seekTo(target.toInt()) }
|
||||||
onClick = { chosen = index; picking = false },
|
position = target.toInt()
|
||||||
)
|
}
|
||||||
|
scrubbing = null
|
||||||
|
},
|
||||||
|
valueRange = 0f..duration.toFloat(),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.semantics { contentDescription = "seek" },
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = clock(duration),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,13 @@ import com.ganjoor.android.ui.theme.readingStyle
|
|||||||
*/
|
*/
|
||||||
val DictionaryPanelWidth = 216.dp
|
val DictionaryPanelWidth = 216.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The narrowest the page may be left once the dictionary opens beside it. Below this the panel is
|
||||||
|
* not worth its room: on a book-style foldable held open in portrait the columns have already
|
||||||
|
* taken theirs, and a panel there left the verse a couple of characters a line.
|
||||||
|
*/
|
||||||
|
val MinPageMeasure = 400.dp
|
||||||
|
|
||||||
/** English prose inside an otherwise right-to-left sheet. */
|
/** English prose inside an otherwise right-to-left sheet. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun LeftToRight(content: @Composable () -> Unit) {
|
private fun LeftToRight(content: @Composable () -> Unit) {
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ On large screens, tapping a word opens **WordPanel** on the left instead of the
|
|||||||
- The poem moves over to make room, so nothing being read is covered.
|
- The poem moves over to make room, so nothing being read is covered.
|
||||||
- The tapped word stays highlighted in the verse (`secondary-container` / `on-secondary-container`), on phones too.
|
- The tapped word stays highlighted in the verse (`secondary-container` / `on-secondary-container`), on phones too.
|
||||||
- ✕ or Back closes the panel.
|
- ✕ or Back closes the panel.
|
||||||
|
- The panel only opens where the page keeps at least **400dp** beside it. On a book-style
|
||||||
|
foldable held open in portrait the columns have already taken their room, and a panel
|
||||||
|
there left the verse a couple of characters a line. Below that the sheet is used instead:
|
||||||
|
it covers the foot of the poem but leaves the lines whole.
|
||||||
|
|
||||||
## Reading settings
|
## Reading settings
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Tablets and unfolded foldables now read in columns: the poets on the right, then
|
||||||
|
a column for each level of the open book, and the poem in whatever room is left.
|
||||||
|
Each column scrolls on its own and narrows as you read, so the poem keeps the
|
||||||
|
middle. A button in the top bar folds them away.
|
||||||
|
|
||||||
|
Where there is room, a couplet's two hemistichs sit on one line, as Ganjoor sets
|
||||||
|
them on a desktop. Each couplet now sits in a soft card.
|
||||||
|
|
||||||
|
The dictionary and the reading settings open beside the poem rather than over
|
||||||
|
it, so nothing you are reading is covered.
|
||||||
|
|
||||||
|
Pages fill in where they load instead of blanking the screen, and Home and the
|
||||||
|
reading settings stay reachable while they do.
|
||||||
|
|
||||||
|
Tapping a word no longer carries the comma or question mark after it into the
|
||||||
|
lookup.
|
||||||
|
|
||||||
|
A reading keeps playing when you scroll down the poem, and a seek bar moves it
|
||||||
|
back and forth.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
تبلتها و گوشیهای تاشوی باز حالا ستونی خوانده میشوند: شاعران در سمت راست، سپس
|
||||||
|
یک ستون برای هر لایهٔ کتاب باز، و شعر در هر فضایی که میماند. هر ستون جداگانه
|
||||||
|
پیمایش میشود و با خواندن باریکتر میشود تا شعر میانه را نگه دارد. دکمهای در
|
||||||
|
نوار بالا آنها را جمع میکند.
|
||||||
|
|
||||||
|
هر جا جا باشد، دو مصرع یک بیت در یک سطر مینشینند، همانگونه که گنجور روی رایانه
|
||||||
|
میچیند. هر بیت اکنون در کارتی ملایم مینشیند.
|
||||||
|
|
||||||
|
لغتنامه و تنظیمات خواندن کنار شعر باز میشوند، نه رویش، تا چیزی از آنچه
|
||||||
|
میخوانید پوشیده نشود.
|
||||||
|
|
||||||
|
صفحهها همانجا که بار میشوند پر میشوند و صفحه سفید نمیماند؛ خانه و تنظیمات
|
||||||
|
خواندن در همان حال هم در دسترساند.
|
||||||
|
|
||||||
|
ضربه بر یک واژه دیگر ویرگول یا علامت پرسش پس از آن را به جستوجو نمیبرد.
|
||||||
|
|
||||||
|
خوانش با پیمایش شعر قطع نمیشود، و نواری تازه آن را پس و پیش میبرد.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
ٹیبلٹ اور کھلے فولڈ ایبل اب ستونوں میں پڑھے جاتے ہیں: شعرا دائیں طرف، پھر کھلی
|
||||||
|
کتاب کی ہر سطح کے لیے ایک ستون، اور باقی جگہ میں کلام۔ ہر ستون الگ سکرول ہوتا ہے
|
||||||
|
اور پڑھتے وقت تنگ ہو جاتا ہے تاکہ کلام درمیان میں رہے۔ اوپر کی پٹی کا ایک بٹن
|
||||||
|
انہیں سمیٹ دیتا ہے۔
|
||||||
|
|
||||||
|
جہاں گنجائش ہو، شعر کے دونوں مصرعے ایک ہی سطر میں آتے ہیں، جیسے گنجور کمپیوٹر پر
|
||||||
|
دکھاتا ہے۔ ہر شعر اب ایک ہلکے کارڈ میں بیٹھتا ہے۔
|
||||||
|
|
||||||
|
لغت نامہ اور مطالعے کی ترتیبات کلام کے ساتھ کھلتی ہیں، اُس پر نہیں، تاکہ جو آپ
|
||||||
|
پڑھ رہے ہیں وہ ڈھکا نہ جائے۔
|
||||||
|
|
||||||
|
صفحات وہیں بھرتے ہیں جہاں لوڈ ہوتے ہیں، خالی اسکرین نہیں آتی؛ ہوم اور مطالعے کی
|
||||||
|
ترتیبات اُس دوران بھی دستیاب رہتی ہیں۔
|
||||||
|
|
||||||
|
کسی لفظ پر ٹیپ اب اُس کے بعد والا کوما یا سوالیہ نشان ساتھ نہیں لے جاتی۔
|
||||||
|
|
||||||
|
سکرول کرنے پر تلاوت بند نہیں ہوتی، اور ایک نئی سیک بار اُسے آگے پیچھے کرتی ہے۔
|
||||||
Reference in new issue
Block a user