Navigate the poem tree instead of the visit history
Breadcrumbs: a poem's path (poet » book » section) is now built from its own fullTitle and fullUrl and every ancestor is tappable, so the trail is the same whether the poem was opened by browsing, from the saved list, from a breadcrumb, or by reading on from the previous poem. Back: browsing is a tree, so Back climbs it — poem to section to book to poet to home — rather than retracing however you arrived. Each screen derives its own parent from its URL and navigation keeps the stack flat, which is what stops Back from walking forward again into the page you just left. System Back and the top-bar arrow do the same thing. The exception is a poem opened from the saved list: that was reached from a list, not a shelf, so Back returns to the list, and reading on through the divan keeps that origin. Plus a Home button on every screen below the poet list. Verified on an API 36 emulator: drilled Hafez > ghazals > ghazal 3 and walked back up to the poet list, and opened a saved Khayyam rubai and confirmed Back returns to the saved list rather than the poet's section. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
abcffbc43b
commit
1accb5ee78
@ -151,6 +151,38 @@ fun catPath(fullUrl: String) = "poets${fullUrl.trimEnd('/')}/_cat.json"
|
|||||||
|
|
||||||
fun poemPath(fullUrl: String) = "poets${fullUrl.trimEnd('/')}.json"
|
fun poemPath(fullUrl: String) = "poets${fullUrl.trimEnd('/')}.json"
|
||||||
|
|
||||||
|
/** One step of a poem's path. [url] is null for the poem itself, which is already open. */
|
||||||
|
data class Crumb(val label: String, val url: String?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a poem's path into tappable ancestors, pairing the titles in `fullTitle`
|
||||||
|
* ("حافظ » غزلیات » غزل شمارهٔ ۱") with the segments of `fullUrl` ("/hafez/ghazal/sh1").
|
||||||
|
*
|
||||||
|
* Derived from the poem itself rather than the back stack, so the trail is the same whether the
|
||||||
|
* poem was opened by browsing, from a bookmark, or by reading on from the previous poem.
|
||||||
|
* Returns empty if the two don't line up, in which case the caller shows the plain title.
|
||||||
|
*/
|
||||||
|
fun breadcrumbs(fullTitle: String, fullUrl: String): List<Crumb> {
|
||||||
|
val labels = fullTitle.split('»').map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
val segments = fullUrl.trim('/').split('/').filter { it.isNotEmpty() }
|
||||||
|
if (labels.isEmpty() || labels.size != segments.size) return emptyList()
|
||||||
|
return labels.mapIndexed { i, label ->
|
||||||
|
Crumb(
|
||||||
|
label = label,
|
||||||
|
url = if (i == labels.lastIndex) null else "/" + segments.take(i + 1).joinToString("/"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The page one level up: a poem's section, a section's book, a book's poet. Null at a poet's
|
||||||
|
* root, whose parent is the poet list.
|
||||||
|
*/
|
||||||
|
fun parentUrl(fullUrl: String): String? {
|
||||||
|
val segments = fullUrl.trim('/').split('/').filter { it.isNotEmpty() }
|
||||||
|
return if (segments.size <= 1) null else "/" + segments.dropLast(1).joinToString("/")
|
||||||
|
}
|
||||||
|
|
||||||
object Ganjoor {
|
object Ganjoor {
|
||||||
private val dataBase = DATA_BASE.toHttpUrl()
|
private val dataBase = DATA_BASE.toHttpUrl()
|
||||||
private val liveBase = "https://api.ganjoor.net/".toHttpUrl()
|
private val liveBase = "https://api.ganjoor.net/".toHttpUrl()
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import com.ganjoor.android.data.LocalBookmarks
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BookmarksScreen(onBack: () -> Unit, onPoem: (String) -> Unit) {
|
fun BookmarksScreen(onUp: () -> Unit, onPoem: (String) -> Unit) {
|
||||||
val bookmarks = LocalBookmarks.current
|
val bookmarks = LocalBookmarks.current
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@ -37,7 +37,7 @@ fun BookmarksScreen(onBack: () -> Unit, onPoem: (String) -> Unit) {
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.bookmarks)) },
|
title = { Text(stringResource(R.string.bookmarks)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onUp) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.ganjoor.android.ui
|
package com.ganjoor.android.ui
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -45,10 +46,14 @@ import com.ganjoor.android.data.Offline
|
|||||||
@Composable
|
@Composable
|
||||||
fun CategoryScreen(
|
fun CategoryScreen(
|
||||||
fullUrl: String,
|
fullUrl: String,
|
||||||
onBack: () -> Unit,
|
onUp: () -> Unit,
|
||||||
|
onHome: () -> Unit,
|
||||||
onCategory: (String) -> Unit,
|
onCategory: (String) -> Unit,
|
||||||
onPoem: (String) -> Unit,
|
onPoem: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
// System Back climbs the tree too, not the visit history.
|
||||||
|
BackHandler(onBack = onUp)
|
||||||
|
|
||||||
Load(key = fullUrl, block = { Ganjoor.category(fullUrl) }) { cat ->
|
Load(key = fullUrl, block = { Ganjoor.category(fullUrl) }) { cat ->
|
||||||
// First lines are a separate, optional call; the list shows up without waiting for it.
|
// First lines are a separate, optional call; the list shows up without waiting for it.
|
||||||
var excerpts by remember(cat.id) { mutableStateOf(emptyMap<Int, String>()) }
|
var excerpts by remember(cat.id) { mutableStateOf(emptyMap<Int, String>()) }
|
||||||
@ -65,11 +70,12 @@ fun CategoryScreen(
|
|||||||
Text(cat.title, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(cat.title, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onUp) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
HomeAction(onHome)
|
||||||
// A whole poet can be saved for offline reading; a sub-collection can't,
|
// A whole poet can be saved for offline reading; a sub-collection can't,
|
||||||
// because the saved tree is keyed by poet.
|
// because the saved tree is keyed by poet.
|
||||||
poetSlug(fullUrl)?.let { PoetDownloadAction(it) }
|
poetSlug(fullUrl)?.let { PoetDownloadAction(it) }
|
||||||
|
|||||||
@ -54,7 +54,7 @@ import com.ganjoor.android.data.PoetRef
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DownloadsScreen(onBack: () -> Unit) {
|
fun DownloadsScreen(onUp: () -> Unit) {
|
||||||
Load(key = Unit, block = { Ganjoor.manifest() }) { manifest ->
|
Load(key = Unit, block = { Ganjoor.manifest() }) { manifest ->
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
// Re-read the disk whenever a download finishes or a poet is removed.
|
// Re-read the disk whenever a download finishes or a poet is removed.
|
||||||
@ -76,7 +76,7 @@ fun DownloadsScreen(onBack: () -> Unit) {
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.downloads)) },
|
title = { Text(stringResource(R.string.downloads)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onUp) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.AutoMirrored.Filled.ArrowBack,
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
stringResource(R.string.back),
|
stringResource(R.string.back),
|
||||||
|
|||||||
@ -7,10 +7,12 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
|
import com.ganjoor.android.data.parentUrl
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
// Routes carry Ganjoor URLs (/hafez/ghazal/sh1) rather than numeric ids: the data set is laid
|
// Routes carry Ganjoor URLs (/hafez/ghazal/sh1) rather than numeric ids: the data set is laid
|
||||||
@ -21,8 +23,12 @@ object PoetsRoute
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class CategoryRoute(val url: String)
|
data class CategoryRoute(val url: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [fromBookmarks] marks a poem opened from the saved list. Browsing is a tree and Back climbs
|
||||||
|
* it, but a saved poem was reached from a list, not a shelf — so Back returns to that list.
|
||||||
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PoemRoute(val url: String)
|
data class PoemRoute(val url: String, val fromBookmarks: Boolean = false)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object BookmarksRoute
|
object BookmarksRoute
|
||||||
@ -30,6 +36,28 @@ object BookmarksRoute
|
|||||||
@Serializable
|
@Serializable
|
||||||
object DownloadsRoute
|
object DownloadsRoute
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a page with the poet list as the only thing beneath it.
|
||||||
|
*
|
||||||
|
* Browsing is a tree, so Back should climb it — poem to section to book to poet to home —
|
||||||
|
* rather than retrace however you arrived. Each screen works out its own parent from its URL,
|
||||||
|
* so the trail is identical whether you drilled down, followed a bookmark, tapped a breadcrumb
|
||||||
|
* or read on from the previous poem. Keeping the stack flat is what stops Back from walking
|
||||||
|
* you forward again into the page you just came from.
|
||||||
|
*/
|
||||||
|
private fun NavController.open(route: Any) = navigate(route) {
|
||||||
|
popUpTo<PoetsRoute> { inclusive = false }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NavController.goHome() = open(PoetsRoute)
|
||||||
|
|
||||||
|
/** One level up the tree; from a poet's root that means home. */
|
||||||
|
private fun NavController.goUp(fromUrl: String) {
|
||||||
|
val parent = parentUrl(fromUrl)
|
||||||
|
if (parent == null) goHome() else open(CategoryRoute(parent))
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GanjoorApp() {
|
fun GanjoorApp() {
|
||||||
val nav = rememberNavController()
|
val nav = rememberNavController()
|
||||||
@ -48,40 +76,51 @@ fun GanjoorApp() {
|
|||||||
) {
|
) {
|
||||||
composable<PoetsRoute> {
|
composable<PoetsRoute> {
|
||||||
PoetsScreen(
|
PoetsScreen(
|
||||||
onPoet = { nav.navigate(CategoryRoute(it)) },
|
onPoet = { nav.open(CategoryRoute(it)) },
|
||||||
onBookmarks = { nav.navigate(BookmarksRoute) },
|
onBookmarks = { nav.navigate(BookmarksRoute) },
|
||||||
onDownloads = { nav.navigate(DownloadsRoute) },
|
onDownloads = { nav.navigate(DownloadsRoute) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable<CategoryRoute> { entry ->
|
composable<CategoryRoute> { entry ->
|
||||||
|
val url = entry.toRoute<CategoryRoute>().url
|
||||||
CategoryScreen(
|
CategoryScreen(
|
||||||
fullUrl = entry.toRoute<CategoryRoute>().url,
|
fullUrl = url,
|
||||||
onBack = { nav.navigateUp() },
|
onUp = { nav.goUp(url) },
|
||||||
onCategory = { nav.navigate(CategoryRoute(it)) },
|
onHome = { nav.goHome() },
|
||||||
onPoem = { nav.navigate(PoemRoute(it)) },
|
onCategory = { nav.open(CategoryRoute(it)) },
|
||||||
|
onPoem = { nav.open(PoemRoute(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable<PoemRoute> { entry ->
|
composable<PoemRoute> { entry ->
|
||||||
|
val route = entry.toRoute<PoemRoute>()
|
||||||
PoemScreen(
|
PoemScreen(
|
||||||
fullUrl = entry.toRoute<PoemRoute>().url,
|
fullUrl = route.url,
|
||||||
onBack = { nav.navigateUp() },
|
// Reading on through a divan keeps the origin, so Back still lands where
|
||||||
// Reading on through a divan replaces the current poem, so Back returns to
|
// you started rather than in whichever section you drifted into.
|
||||||
// the list instead of unwinding every poem read along the way.
|
onUp = { if (route.fromBookmarks) nav.navigateUp() else nav.goUp(route.url) },
|
||||||
|
onHome = { nav.goHome() },
|
||||||
onPoem = { url ->
|
onPoem = { url ->
|
||||||
nav.navigate(PoemRoute(url)) {
|
if (route.fromBookmarks) {
|
||||||
popUpTo<PoemRoute> { inclusive = true }
|
nav.navigate(PoemRoute(url, fromBookmarks = true)) {
|
||||||
|
popUpTo<PoemRoute> { inclusive = true }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nav.open(PoemRoute(url))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Tapping a breadcrumb leaves the saved list behind and starts browsing.
|
||||||
|
onCategory = { nav.open(CategoryRoute(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable<BookmarksRoute> {
|
composable<BookmarksRoute> {
|
||||||
BookmarksScreen(
|
BookmarksScreen(
|
||||||
onBack = { nav.navigateUp() },
|
onUp = { nav.goHome() },
|
||||||
onPoem = { nav.navigate(PoemRoute(it)) },
|
// Plain navigate, not open(): this keeps the saved list on the stack.
|
||||||
|
onPoem = { nav.navigate(PoemRoute(it, fromBookmarks = true)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable<DownloadsRoute> {
|
composable<DownloadsRoute> {
|
||||||
DownloadsScreen(onBack = { nav.navigateUp() })
|
DownloadsScreen(onUp = { nav.goHome() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
app/src/main/java/com/ganjoor/android/ui/HomeAction.kt
Normal file
17
app/src/main/java/com/ganjoor/android/ui/HomeAction.kt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package com.ganjoor.android.ui
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Home
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import com.ganjoor.android.R
|
||||||
|
|
||||||
|
/** Escape hatch back to the poet list, on every screen that isn't it. */
|
||||||
|
@Composable
|
||||||
|
fun HomeAction(onHome: () -> Unit) {
|
||||||
|
IconButton(onClick = onHome) {
|
||||||
|
Icon(Icons.Default.Home, stringResource(R.string.home))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
package com.ganjoor.android.ui
|
package com.ganjoor.android.ui
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -40,6 +42,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.ganjoor.android.R
|
import com.ganjoor.android.R
|
||||||
import com.ganjoor.android.data.Bookmark
|
import com.ganjoor.android.data.Bookmark
|
||||||
|
import com.ganjoor.android.data.breadcrumbs
|
||||||
import com.ganjoor.android.data.Ganjoor
|
import com.ganjoor.android.data.Ganjoor
|
||||||
import com.ganjoor.android.data.LocalBookmarks
|
import com.ganjoor.android.data.LocalBookmarks
|
||||||
import com.ganjoor.android.data.PoemRef
|
import com.ganjoor.android.data.PoemRef
|
||||||
@ -49,7 +52,15 @@ import com.ganjoor.android.ui.theme.readingStyle
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PoemScreen(fullUrl: String, onBack: () -> Unit, onPoem: (String) -> Unit) {
|
fun PoemScreen(
|
||||||
|
fullUrl: String,
|
||||||
|
onUp: () -> Unit,
|
||||||
|
onHome: () -> Unit,
|
||||||
|
onPoem: (String) -> Unit,
|
||||||
|
onCategory: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
BackHandler(onBack = onUp)
|
||||||
|
|
||||||
Load(key = fullUrl, block = { Ganjoor.poem(fullUrl) }) { poem ->
|
Load(key = fullUrl, block = { Ganjoor.poem(fullUrl) }) { poem ->
|
||||||
val prefs = LocalSettings.current.value
|
val prefs = LocalSettings.current.value
|
||||||
val style = readingStyle(prefs.font, prefs.fontSize, prefs.fontWeight.weight)
|
val style = readingStyle(prefs.font, prefs.fontSize, prefs.fontWeight.weight)
|
||||||
@ -70,11 +81,12 @@ fun PoemScreen(fullUrl: String, onBack: () -> Unit, onPoem: (String) -> Unit) {
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(poem.title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
title = { Text(poem.title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onUp) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
HomeAction(onHome)
|
||||||
BookmarkAction(
|
BookmarkAction(
|
||||||
url = fullUrl,
|
url = fullUrl,
|
||||||
title = poem.title,
|
title = poem.title,
|
||||||
@ -99,10 +111,7 @@ fun PoemScreen(fullUrl: String, onBack: () -> Unit, onPoem: (String) -> Unit) {
|
|||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Column(Modifier.padding(bottom = 12.dp)) {
|
Column(Modifier.padding(bottom = 12.dp)) {
|
||||||
Text(
|
Breadcrumbs(poem.fullTitle, poem.fullUrl.ifBlank { fullUrl }, onCategory)
|
||||||
text = poem.fullTitle,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
poem.metre?.rhythm?.let { rhythm ->
|
poem.metre?.rhythm?.let { rhythm ->
|
||||||
Text(
|
Text(
|
||||||
text = rhythm,
|
text = rhythm,
|
||||||
@ -152,6 +161,36 @@ fun PoemScreen(fullUrl: String, onBack: () -> Unit, onPoem: (String) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The poem's path, with every ancestor tappable: poet » book » section » this poem. */
|
||||||
|
@Composable
|
||||||
|
private fun Breadcrumbs(fullTitle: String, fullUrl: String, onCategory: (String) -> Unit) {
|
||||||
|
val crumbs = remember(fullTitle, fullUrl) { breadcrumbs(fullTitle, fullUrl) }
|
||||||
|
if (crumbs.isEmpty()) {
|
||||||
|
Text(fullTitle, style = MaterialTheme.typography.titleMedium)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowRow(verticalArrangement = Arrangement.Center) {
|
||||||
|
crumbs.forEachIndexed { index, crumb ->
|
||||||
|
if (index > 0) {
|
||||||
|
Text(
|
||||||
|
text = " » ",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = crumb.label,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = if (crumb.url == null) MaterialTheme.colorScheme.onSurface
|
||||||
|
else MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = if (crumb.url == null) Modifier
|
||||||
|
else Modifier.clickable { onCategory(crumb.url) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BookmarkAction(url: String, title: String, subtitle: String) {
|
private fun BookmarkAction(url: String, title: String, subtitle: String) {
|
||||||
val bookmarks = LocalBookmarks.current
|
val bookmarks = LocalBookmarks.current
|
||||||
|
|||||||
@ -48,4 +48,5 @@
|
|||||||
<string name="copy">رونوشت</string>
|
<string name="copy">رونوشت</string>
|
||||||
<string name="copied">رونوشت شد</string>
|
<string name="copied">رونوشت شد</string>
|
||||||
<string name="download_selected">دریافت %d شاعر</string>
|
<string name="download_selected">دریافت %d شاعر</string>
|
||||||
|
<string name="home">خانه</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -48,4 +48,5 @@
|
|||||||
<string name="copy">نقل کریں</string>
|
<string name="copy">نقل کریں</string>
|
||||||
<string name="copied">نقل ہو گیا</string>
|
<string name="copied">نقل ہو گیا</string>
|
||||||
<string name="download_selected">%d شعرا ڈاؤن لوڈ کریں</string>
|
<string name="download_selected">%d شعرا ڈاؤن لوڈ کریں</string>
|
||||||
|
<string name="home">ہوم</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -53,4 +53,5 @@
|
|||||||
<string name="next_poem">%s ›</string>
|
<string name="next_poem">%s ›</string>
|
||||||
<string name="previous_poem">‹ %s</string>
|
<string name="previous_poem">‹ %s</string>
|
||||||
<string name="download_selected">Download %d poets</string>
|
<string name="download_selected">Download %d poets</string>
|
||||||
|
<string name="home">Home</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package com.ganjoor.android
|
package com.ganjoor.android
|
||||||
|
|
||||||
|
import com.ganjoor.android.data.Crumb
|
||||||
import com.ganjoor.android.data.Verse
|
import com.ganjoor.android.data.Verse
|
||||||
|
import com.ganjoor.android.data.breadcrumbs
|
||||||
|
import com.ganjoor.android.data.parentUrl
|
||||||
import com.ganjoor.android.data.couplets
|
import com.ganjoor.android.data.couplets
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -66,3 +69,59 @@ class CoupletsTest {
|
|||||||
assertEquals(listOf(2, 1, 1), grouped.map { it.size })
|
assertEquals(listOf(2, 1, 1), grouped.map { it.size })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class BreadcrumbsTest {
|
||||||
|
@Test
|
||||||
|
fun `every ancestor is linked and the poem itself is not`() {
|
||||||
|
val crumbs = breadcrumbs("حافظ » غزلیات » غزل شمارهٔ ۱", "/hafez/ghazal/sh1")
|
||||||
|
|
||||||
|
assertEquals(listOf("حافظ", "غزلیات", "غزل شمارهٔ ۱"), crumbs.map { it.label })
|
||||||
|
assertEquals(listOf("/hafez", "/hafez/ghazal", null), crumbs.map { it.url })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `deeply nested books keep the whole trail`() {
|
||||||
|
val crumbs = breadcrumbs(
|
||||||
|
"مولانا » مثنوی معنوی » دفتر اول » بخش ۱",
|
||||||
|
"/moulavi/masnavi/daftar1/sh1",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(4, crumbs.size)
|
||||||
|
assertEquals("/moulavi/masnavi/daftar1", crumbs[2].url)
|
||||||
|
assertEquals(null, crumbs.last().url)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a title and url that disagree produce nothing, so the caller falls back`() {
|
||||||
|
assertEquals(emptyList<Crumb>(), breadcrumbs("حافظ » غزل ۱", "/hafez/ghazal/sh1"))
|
||||||
|
assertEquals(emptyList<Crumb>(), breadcrumbs("", "/hafez/ghazal/sh1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a poem directly under a poet still links the poet`() {
|
||||||
|
val crumbs = breadcrumbs("حافظ » ساقینامه", "/hafez/saghinameh")
|
||||||
|
|
||||||
|
assertEquals(listOf("/hafez", null), crumbs.map { it.url })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ParentUrlTest {
|
||||||
|
@Test
|
||||||
|
fun `each level climbs to the one above it`() {
|
||||||
|
assertEquals("/hafez/ghazal", parentUrl("/hafez/ghazal/sh1"))
|
||||||
|
assertEquals("/hafez", parentUrl("/hafez/ghazal"))
|
||||||
|
assertEquals("/moulavi/masnavi", parentUrl("/moulavi/masnavi/daftar1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a poet root has no parent, so the caller sends you home`() {
|
||||||
|
assertEquals(null, parentUrl("/hafez"))
|
||||||
|
assertEquals(null, parentUrl("hafez"))
|
||||||
|
assertEquals(null, parentUrl(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trailing slashes don't invent a level`() {
|
||||||
|
assertEquals("/hafez", parentUrl("/hafez/ghazal/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user