Download a poet from the list, and show when they are already here

A download control on each row in the list view: the arrow starts it, a spinner
replaces it while it runs and cancels on a tap, and a green tick says the poems
are already on the device. Only text is fetched — recitations stream when
played, and nothing about that changes.

The tick sits in a box the size of an IconButton although nothing about it is
tappable. A bare icon lands where the button's padding would have put it, so
the ticks and the arrows did not line up down the column.

The green is derived from the scheme's own luminance rather than fixed:
Material has no success colour, and one green goes muddy on sepia and glares on
OLED black.

Deleting stays on the downloads page, where the sizes are. A tap beside a
poet's name should not be the thing that throws their poems away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anas Rashid 2026-10-04 20:36:59 +02:00
parent a5cc0421b6
commit 929aee2a02
10 changed files with 82 additions and 3 deletions

View File

@ -55,6 +55,11 @@ import coil3.compose.AsyncImage
import java.text.Collator import java.text.Collator
import java.util.Locale import java.util.Locale
import com.ganjoor.android.R import com.ganjoor.android.R
import com.ganjoor.android.ui.theme.downloaded
import com.ganjoor.android.data.Offline
import com.ganjoor.android.data.Downloads
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material.icons.filled.CheckCircle
import com.ganjoor.android.data.Ganjoor import com.ganjoor.android.data.Ganjoor
import com.ganjoor.android.data.PoetRef import com.ganjoor.android.data.PoetRef
@ -249,6 +254,49 @@ private fun PinMark(modifier: Modifier = Modifier) {
) )
} }
/**
* Fetch a poet's poems, or say they are already here. Three states and no menu: downloading shows
* its progress and cancels on a tap, downloaded is a green tick, and anything else offers the
* download. Deleting stays on the downloads page, where the sizes are — a tap next to a poet's
* name should never be the thing that throws their poems away.
*/
@Composable
private fun DownloadAction(slug: String) {
val progress = Downloads.running[slug]
val saved = remember(slug, Downloads.revision) { Offline.isSaved(slug) }
when {
progress != null -> IconButton(onClick = { Downloads.cancel(slug) }) {
CircularProgressIndicator(
strokeWidth = 2.dp,
modifier = Modifier.size(18.dp),
)
}
// In a box the size of an IconButton, even though nothing here is tappable: a bare icon
// sits where the button's padding would have put it, so the ticks and the arrows would
// not line up down the column.
saved -> Box(
modifier = Modifier.size(48.dp),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = stringResource(R.string.downloaded_poet),
tint = MaterialTheme.colorScheme.downloaded,
)
}
else -> IconButton(onClick = { Downloads.start(slug) }) {
Icon(
painter = painterResource(R.drawable.ic_download),
contentDescription = stringResource(R.string.download),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
/** One poet as a row, for readers who would rather scan names than faces. */ /** One poet as a row, for readers who would rather scan names than faces. */
@Composable @Composable
private fun PoetRow(poet: PoetRef, pinned: Boolean, onClick: () -> Unit, onPin: () -> Unit) { private fun PoetRow(poet: PoetRef, pinned: Boolean, onClick: () -> Unit, onPin: () -> Unit) {
@ -271,7 +319,15 @@ private fun PoetRow(poet: PoetRef, pinned: Boolean, onClick: () -> Unit, onPin:
) )
} }
}, },
trailingContent = { if (pinned) PinMark() }, trailingContent = {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (pinned) PinMark()
DownloadAction(poet.slug)
}
},
modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onPin), modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onPin),
) )
} }

View File

@ -9,6 +9,7 @@ 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.luminance
import androidx.compose.ui.graphics.lerp 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
@ -202,3 +203,13 @@ fun GanjoorTheme(
content = content, content = content,
) )
} }
/**
* The green on a poet whose poems are already on the device.
*
* Material's palette has no success colour, and a fixed green goes muddy on the sepia schemes and
* glares on OLED black. Picking by the surface's luminance covers all five schemes without each
* of them having to declare one.
*/
val ColorScheme.downloaded: Color
get() = if (surface.luminance() < 0.5f) Color(0xFF81C784) else Color(0xFF2E7D32)

View File

@ -113,4 +113,5 @@
<string name="pin_hint">برای نگه‌داشتن یک شاعر در اینجا، انگشت را روی او نگه دارید</string> <string name="pin_hint">برای نگه‌داشتن یک شاعر در اینجا، انگشت را روی او نگه دارید</string>
<string name="view_grid">نمایش کارتی</string> <string name="view_grid">نمایش کارتی</string>
<string name="view_list">نمایش فهرستی</string> <string name="view_list">نمایش فهرستی</string>
<string name="downloaded_poet">پیش‌تر دریافت شده</string>
</resources> </resources>

View File

@ -113,4 +113,5 @@
<string name="pin_hint">کسی شاعر کو یہاں رکھنے کے لیے اُس پر دبا کر رکھیں</string> <string name="pin_hint">کسی شاعر کو یہاں رکھنے کے لیے اُس پر دبا کر رکھیں</string>
<string name="view_grid">کارڈ کی صورت</string> <string name="view_grid">کارڈ کی صورت</string>
<string name="view_list">فہرست کی صورت</string> <string name="view_list">فہرست کی صورت</string>
<string name="downloaded_poet">پہلے سے محفوظ ہے</string>
</resources> </resources>

View File

@ -118,4 +118,5 @@
<string name="pin_hint">Press and hold a poet to keep them here</string> <string name="pin_hint">Press and hold a poet to keep them here</string>
<string name="view_grid">Show as cards</string> <string name="view_grid">Show as cards</string>
<string name="view_list">Show as a list</string> <string name="view_list">Show as a list</string>
<string name="downloaded_poet">Already downloaded</string>
</resources> </resources>

View File

@ -6,3 +6,6 @@ alphabetical listing a tap away. Until you pin anything the shelf is simply
Ganjoor's order, so there is never an empty screen. Ganjoor's order, so there is never an empty screen.
Poets can be shown as cards or as a list, whichever reads better. Poets can be shown as cards or as a list, whichever reads better.
Each poet in the list view carries a download button, and a green tick once their
poems are on the device. Text only — recitations are still streamed when you play them.

View File

@ -6,3 +6,6 @@
صفحه‌ای خالی نمی‌بینید. صفحه‌ای خالی نمی‌بینید.
شاعران را می‌توان کارتی یا فهرستی دید، هرکدام که خواناتر است. شاعران را می‌توان کارتی یا فهرستی دید، هرکدام که خواناتر است.
هر شاعر در نمایش فهرستی یک دکمهٔ دریافت دارد، و پس از ذخیره‌شدن شعرهایش یک تیک سبز.
فقط متن؛ خوانش‌ها همچنان هنگام پخش از اینترنت می‌آیند.

View File

@ -7,3 +7,6 @@
سو خالی صفحہ کبھی نہیں ملتا۔ سو خالی صفحہ کبھی نہیں ملتا۔
شاعروں کو کارڈ کی صورت یا فہرست کی صورت، جو بہتر لگے، دیکھا جا سکتا ہے۔ شاعروں کو کارڈ کی صورت یا فہرست کی صورت، جو بہتر لگے، دیکھا جا سکتا ہے۔
فہرست کی صورت میں ہر شاعر کے ساتھ ڈاؤن لوڈ کا بٹن ہے، اور اُن کا کلام محفوظ ہو جانے پر
سبز نشان۔ صرف متن؛ تلاوتیں اب بھی سنتے وقت آن لائن چلتی ہیں۔

View File

@ -12,7 +12,7 @@ from source and signs with its own key, so these are for direct installation onl
| 0.2.0 | 2 | `d92445f` | Share a poem, couplet or selection; look a word up from any app's selection menu; the optional AI assistant. | | 0.2.0 | 2 | `d92445f` | Share a poem, couplet or selection; look a word up from any app's selection menu; the optional AI assistant. |
| 0.2.1 | 3 | `ec05989` | Assistant answers in the page rather than over it, a visible chevron for each couplet's actions, and answers remembered so scrolling doesn't re-ask. | | 0.2.1 | 3 | `ec05989` | Assistant answers in the page rather than over it, a visible chevron for each couplet's actions, and answers remembered so scrolling doesn't re-ask. |
| 0.2.2 | 4 | `d7568d1` | Credits Material Symbols for the glyphs, and states the terms around the optional assistant. | | 0.2.2 | 4 | `d7568d1` | Credits Material Symbols for the glyphs, and states the terms around the optional assistant. |
| 0.3.0 | 5 | `HEAD` | Pin poets to the home screen: three views (pinned, Ganjoor's order, alphabetical), as cards or a list. | | 0.3.0 | 5 | `HEAD` | Pin poets to the home screen: three views (pinned, Ganjoor's order, alphabetical), as cards or a list, with a download control on each row. |
## Checksums ## Checksums
@ -21,7 +21,7 @@ from source and signs with its own key, so these are for direct installation onl
43d78b773efca2b1aa2bf790c689b3086389d66c4ea0ffb72c9dba8bddb50770 ganjoor-0.2.0.apk 43d78b773efca2b1aa2bf790c689b3086389d66c4ea0ffb72c9dba8bddb50770 ganjoor-0.2.0.apk
5163554eb01845c131c31a9da7bdd748170bb9b5f82edaece754b4cf40b02b0c ganjoor-0.2.1.apk 5163554eb01845c131c31a9da7bdd748170bb9b5f82edaece754b4cf40b02b0c ganjoor-0.2.1.apk
e473269044710813d2cf4119de663fe180f887239df7bdb1ccee0239ef84fd3e ganjoor-0.2.2.apk e473269044710813d2cf4119de663fe180f887239df7bdb1ccee0239ef84fd3e ganjoor-0.2.2.apk
d9c49f109854793a0d96a3bd42dffff931dc37825dc49b4ed80cc4de1349ff72 ganjoor-0.3.0.apk 5ac991ab8fe8676f89f4bc09a1a5d7464e32b6f74d789e7f29038b85f2c64f4a ganjoor-0.3.0.apk
``` ```
Installing: `adb install -r releases/ganjoor-<version>.apk`. A phone holding a *debug* build has Installing: `adb install -r releases/ganjoor-<version>.apk`. A phone holding a *debug* build has

Binary file not shown.