Pin poets to the home screen
Three views, chosen by the chips: the poets you pinned, Ganjoor's own order, and alphabetical. Press and hold a poet anywhere to pin or unpin — tapping still opens them, and a pin marker sits beside the name. Pinned is the default. With nothing pinned it is Ganjoor's order untouched, so a fresh install never opens to an empty screen; the hint about holding a poet shows only while the shelf is empty, because once there is something on it the shelf explains itself. Pins keep the order they were made in rather than being sorted. A shelf someone arranges themselves should stay where they put it, and a new pin appearing in the middle of the row is disorienting. Cards or a list, switched from the app bar and remembered. The same storage as bookmarks: a list of a few urls needs no schema. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d7568d1f49
commit
c9269713d1
@ -16,7 +16,7 @@ val keystoreProperties = Properties().apply {
|
||||
|
||||
// A delivered file should say what it is without being opened.
|
||||
base {
|
||||
archivesName = "ganjoor-0.2.2"
|
||||
archivesName = "ganjoor-0.3.0"
|
||||
}
|
||||
|
||||
android {
|
||||
@ -32,8 +32,8 @@ android {
|
||||
// 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
|
||||
// fastlane/metadata/android/*/changelogs/.
|
||||
versionCode = 4
|
||||
versionName = "0.2.2"
|
||||
versionCode = 5
|
||||
versionName = "0.3.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@ import com.ganjoor.android.data.LocalBookmarks
|
||||
import com.ganjoor.android.ui.GanjoorApp
|
||||
import com.ganjoor.android.data.AssistantSettings
|
||||
import com.ganjoor.android.data.LocalAssistant
|
||||
import com.ganjoor.android.data.LocalPinnedPoets
|
||||
import com.ganjoor.android.data.PinnedPoets
|
||||
import com.ganjoor.android.ui.LocalSettings
|
||||
import com.ganjoor.android.ui.Settings
|
||||
import com.ganjoor.android.ui.theme.GanjoorTheme
|
||||
@ -56,12 +58,14 @@ class MainActivity : ComponentActivity() {
|
||||
val settings = remember { settings }
|
||||
val bookmarks = remember { Bookmarks(applicationContext) }
|
||||
val assistant = remember { AssistantSettings(applicationContext) }
|
||||
val pinned = remember { PinnedPoets(applicationContext) }
|
||||
// The client reads this flag on every request, so keep it in step with the setting.
|
||||
Ganjoor.offline = settings.value.offline
|
||||
CompositionLocalProvider(
|
||||
LocalSettings provides settings,
|
||||
LocalBookmarks provides bookmarks,
|
||||
LocalAssistant provides assistant,
|
||||
LocalPinnedPoets provides pinned,
|
||||
// The poetry is Persian, Urdu and Arabic throughout, so the whole app reads and
|
||||
// navigates right-to-left whichever UI language is selected.
|
||||
LocalLayoutDirection provides LayoutDirection.Rtl,
|
||||
|
||||
43
app/src/main/java/com/ganjoor/android/data/PinnedPoets.kt
Normal file
43
app/src/main/java/com/ganjoor/android/data/PinnedPoets.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package com.ganjoor.android.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
* The poets someone keeps to hand, by the url that identifies them.
|
||||
*
|
||||
* Kept in the order they were pinned rather than sorted: a shelf someone arranges themselves
|
||||
* should stay where they put it, and a new pin appearing in the middle of the row is disorienting.
|
||||
*
|
||||
* ponytail: the same SharedPreferences and the same shape as [Bookmarks] — a list of a few strings
|
||||
* doesn't need a database or a schema, and the two have no reason to diverge.
|
||||
*/
|
||||
class PinnedPoets(context: Context) {
|
||||
private val prefs = context.getSharedPreferences("ganjoor", Context.MODE_PRIVATE)
|
||||
|
||||
val items = mutableStateListOf<String>().apply {
|
||||
prefs.getString(KEY, null)
|
||||
?.split(SEPARATOR)
|
||||
?.filter { it.isNotBlank() }
|
||||
?.let(::addAll)
|
||||
}
|
||||
|
||||
fun contains(fullUrl: String) = fullUrl in items
|
||||
|
||||
fun toggle(fullUrl: String) {
|
||||
if (!items.remove(fullUrl)) items.add(fullUrl)
|
||||
// commit, so a pin survives the process being killed right after it's made.
|
||||
prefs.edit(commit = true) { putString(KEY, items.joinToString(SEPARATOR)) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY = "pinnedPoets"
|
||||
|
||||
/** A newline cannot appear in a Ganjoor url, so it needs no escaping. */
|
||||
const val SEPARATOR = "\n"
|
||||
}
|
||||
}
|
||||
|
||||
val LocalPinnedPoets = staticCompositionLocalOf<PinnedPoets> { error("No PinnedPoets provided") }
|
||||
@ -2,6 +2,10 @@ package com.ganjoor.android.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import com.ganjoor.android.data.LocalPinnedPoets
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items as listItems
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -70,6 +74,7 @@ fun PoetsScreen(
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.app_name)) },
|
||||
actions = {
|
||||
ViewAction()
|
||||
LanguageAction()
|
||||
IconButton(onClick = onBookmarks) {
|
||||
Icon(Icons.Default.Favorite, stringResource(R.string.bookmarks))
|
||||
@ -92,39 +97,39 @@ fun PoetsScreen(
|
||||
Load(key = Unit, block = { Ganjoor.manifest() }) { manifest ->
|
||||
val settings = LocalSettings.current
|
||||
val sort = settings.value.poetSort
|
||||
val poets = remember(query, manifest, sort) {
|
||||
val pinned = LocalPinnedPoets.current
|
||||
val poets = remember(query, manifest, sort, pinned.items.toList()) {
|
||||
val matches =
|
||||
if (query.isBlank()) manifest.poets
|
||||
else manifest.poets.filter {
|
||||
it.nickname.contains(query.trim(), ignoreCase = true)
|
||||
}
|
||||
when (sort) {
|
||||
// Persian letters don't sort correctly by code point (آ vs ا, ی vs ي), so hand
|
||||
// the ordering to a collator rather than String.compareTo.
|
||||
if (sort == PoetSort.Name) {
|
||||
PoetSort.Name -> {
|
||||
val collator = Collator.getInstance(Locale.forLanguageTag("fa"))
|
||||
matches.sortedWith { a, b -> collator.compare(a.nickname, b.nickname) }
|
||||
} else {
|
||||
matches
|
||||
}
|
||||
|
||||
// Pinned first, in the order they were pinned, then the rest untouched. With
|
||||
// nothing pinned this is Ganjoor's order, which is why it can be the default.
|
||||
PoetSort.Pinned -> pinnedFirst(matches, pinned.items)
|
||||
|
||||
PoetSort.Default -> matches
|
||||
}
|
||||
}
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(132.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
val padding = PaddingValues(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = insets.calculateTopPadding() + 8.dp,
|
||||
bottom = insets.calculateBottomPadding() + 16.dp,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
)
|
||||
// The chips and the hint are the same whichever shape the poets take.
|
||||
val header: @Composable () -> Unit = {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PoetSort.entries.forEach { option ->
|
||||
FilterChip(
|
||||
selected = sort == option,
|
||||
@ -133,9 +138,51 @@ fun PoetsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
// Only while the shelf is empty: once there is something on it, it explains
|
||||
// itself, and a standing instruction is just clutter.
|
||||
if (sort == PoetSort.Pinned && pinned.items.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.pin_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.value.poetGrid) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(132.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = padding,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) { header() }
|
||||
items(poets, key = { it.id }) { poet ->
|
||||
PoetCard(poet) { onPoet(poet.fullUrl) }
|
||||
PoetCard(
|
||||
poet = poet,
|
||||
pinned = pinned.contains(poet.fullUrl),
|
||||
onClick = { onPoet(poet.fullUrl) },
|
||||
onPin = { pinned.toggle(poet.fullUrl) },
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = padding,
|
||||
) {
|
||||
item { header() }
|
||||
listItems(poets, key = { it.id }) { poet ->
|
||||
PoetRow(
|
||||
poet = poet,
|
||||
pinned = pinned.contains(poet.fullUrl),
|
||||
onClick = { onPoet(poet.fullUrl) },
|
||||
onPin = { pinned.toggle(poet.fullUrl) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -176,9 +223,62 @@ private fun SearchBar(query: String, onQueryChange: (String) -> Unit, onSearchPo
|
||||
}
|
||||
}
|
||||
|
||||
/** Switches the poets between cards and a list, and remembers which. */
|
||||
@Composable
|
||||
private fun PoetCard(poet: PoetRef, onClick: () -> Unit) {
|
||||
Card(modifier = Modifier.clickable(onClick = onClick)) {
|
||||
private fun ViewAction() {
|
||||
val settings = LocalSettings.current
|
||||
val grid = settings.value.poetGrid
|
||||
IconButton(onClick = { settings.update { it.copy(poetGrid = !grid) } }) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
if (grid) R.drawable.ic_view_list else R.drawable.ic_view_grid
|
||||
),
|
||||
contentDescription = stringResource(if (grid) R.string.view_list else R.string.view_grid),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The pin marker, shown only on a poet someone chose to keep. */
|
||||
@Composable
|
||||
private fun PinMark(modifier: Modifier = Modifier) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_pin),
|
||||
contentDescription = stringResource(R.string.unpin_poet),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
/** One poet as a row, for readers who would rather scan names than faces. */
|
||||
@Composable
|
||||
private fun PoetRow(poet: PoetRef, pinned: Boolean, onClick: () -> Unit, onPin: () -> Unit) {
|
||||
ListItem(
|
||||
headlineContent = { Text(poet.nickname, style = MaterialTheme.typography.titleMedium) },
|
||||
leadingContent = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(poet.nickname.take(1), color = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||
AsyncImage(
|
||||
model = poet.imageUrl,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = { if (pinned) PinMark() },
|
||||
modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onPin),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PoetCard(poet: PoetRef, pinned: Boolean, onClick: () -> Unit, onPin: () -> Unit) {
|
||||
Card(modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onPin)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@ -204,6 +304,11 @@ private fun PoetCard(poet: PoetRef, onClick: () -> Unit) {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (pinned) PinMark()
|
||||
Text(
|
||||
text = poet.nickname,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
@ -214,3 +319,16 @@ private fun PoetCard(poet: PoetRef, onClick: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pinned poets first, in the order they were pinned, then everyone else as Ganjoor has them.
|
||||
*
|
||||
* With nothing pinned this is Ganjoor's order untouched, which is what lets it be the default
|
||||
* without anyone opening the app to an empty screen.
|
||||
*/
|
||||
internal fun pinnedFirst(poets: List<PoetRef>, pins: List<String>): List<PoetRef> {
|
||||
if (pins.isEmpty()) return poets
|
||||
val (kept, rest) = poets.partition { it.fullUrl in pins }
|
||||
return kept.sortedBy { pins.indexOf(it.fullUrl) } + rest
|
||||
}
|
||||
|
||||
@ -42,6 +42,12 @@ enum class Language(val tag: String, val label: String) {
|
||||
|
||||
/** How the poet grid is ordered. */
|
||||
enum class PoetSort(@StringRes val label: Int) {
|
||||
/**
|
||||
* Whoever the reader pinned, first. With nothing pinned it is Ganjoor's order, so this can be
|
||||
* the default without anyone ever opening the app to an empty screen.
|
||||
*/
|
||||
Pinned(R.string.sort_pinned),
|
||||
|
||||
/** The data set's own order, which is Ganjoor's — best known poets first. */
|
||||
Default(R.string.sort_default),
|
||||
Name(R.string.sort_name),
|
||||
@ -57,7 +63,8 @@ data class Prefs(
|
||||
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.Pinned,
|
||||
val poetGrid: Boolean = true,
|
||||
)
|
||||
|
||||
/** Reading preferences, kept in SharedPreferences and read once at startup. */
|
||||
@ -75,7 +82,8 @@ class Settings(context: Context) {
|
||||
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.Pinned),
|
||||
poetGrid = prefs.getBoolean("poetGrid", true),
|
||||
)
|
||||
)
|
||||
private set
|
||||
@ -94,6 +102,7 @@ class Settings(context: Context) {
|
||||
putBoolean("offline", p.offline)
|
||||
putBoolean("oled", p.oled)
|
||||
putString("poetSort", p.poetSort.name)
|
||||
putBoolean("poetGrid", p.poetGrid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
app/src/main/res/drawable/ic_pin.xml
Normal file
10
app/src/main/res/drawable/ic_pin.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<!-- Material "push_pin", marking a poet kept on the home screen. -->
|
||||
<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="M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_view_grid.xml
Normal file
10
app/src/main/res/drawable/ic_view_grid.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<!-- Material "grid_view", for switching the poets back to cards. -->
|
||||
<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="M3,3v8h8V3H3zM9,9H5V5h4V9zM3,13v8h8v-8H3zM9,19H5v-4h4V19zM13,3v8h8V3H13zM19,9h-4V5h4V9zM13,13v8h8v-8H13zM19,19h-4v-4h4V19z" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_view_list.xml
Normal file
11
app/src/main/res/drawable/ic_view_list.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<!-- Material "view_list", for switching the poets to a list of names. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,14h4v-4H3V14zM3,19h4v-4H3V19zM3,9h4V5H3V9zM8,14h13v-4H8V14zM8,19h13v-4H8V19zM8,5v4h13V5H8z" />
|
||||
</vector>
|
||||
@ -107,4 +107,10 @@
|
||||
<string name="assistant_translate_prompt">این متن را ترجمه کن:</string>
|
||||
<string name="this_couplet">این بیت</string>
|
||||
<string name="couplet_options">گزینههای این بیت</string>
|
||||
<string name="sort_pinned">برگزیده</string>
|
||||
<string name="pin_poet">نگهداشتن در بالا</string>
|
||||
<string name="unpin_poet">برداشتن</string>
|
||||
<string name="pin_hint">برای نگهداشتن یک شاعر در اینجا، انگشت را روی او نگه دارید</string>
|
||||
<string name="view_grid">نمایش کارتی</string>
|
||||
<string name="view_list">نمایش فهرستی</string>
|
||||
</resources>
|
||||
|
||||
@ -107,4 +107,10 @@
|
||||
<string name="assistant_translate_prompt">اس کا ترجمہ کریں:</string>
|
||||
<string name="this_couplet">یہ شعر</string>
|
||||
<string name="couplet_options">اس شعر کے اختیارات</string>
|
||||
<string name="sort_pinned">منتخب</string>
|
||||
<string name="pin_poet">اوپر رکھیں</string>
|
||||
<string name="unpin_poet">ہٹا دیں</string>
|
||||
<string name="pin_hint">کسی شاعر کو یہاں رکھنے کے لیے اُس پر دبا کر رکھیں</string>
|
||||
<string name="view_grid">کارڈ کی صورت</string>
|
||||
<string name="view_list">فہرست کی صورت</string>
|
||||
</resources>
|
||||
|
||||
@ -112,4 +112,10 @@
|
||||
<string name="assistant_translate_prompt">Translate this:</string>
|
||||
<string name="this_couplet">This couplet</string>
|
||||
<string name="couplet_options">Options for this couplet</string>
|
||||
<string name="sort_pinned">Pinned</string>
|
||||
<string name="pin_poet">Pin to the top</string>
|
||||
<string name="unpin_poet">Unpin</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_list">Show as a list</string>
|
||||
</resources>
|
||||
|
||||
39
app/src/test/java/com/ganjoor/android/PinnedPoetsTest.kt
Normal file
39
app/src/test/java/com/ganjoor/android/PinnedPoetsTest.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package com.ganjoor.android
|
||||
|
||||
import com.ganjoor.android.data.PoetRef
|
||||
import com.ganjoor.android.ui.pinnedFirst
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class PinnedPoetsTest {
|
||||
|
||||
private fun poets(vararg slugs: String) =
|
||||
slugs.mapIndexed { i, slug -> PoetRef(id = i, nickname = slug, fullUrl = "/$slug") }
|
||||
|
||||
private val all = poets("hafez", "moulavi", "saadi", "ferdousi")
|
||||
|
||||
/** The default view must never be empty, which is the whole reason it can be the default. */
|
||||
@Test
|
||||
fun `no pins leaves ganjoor's order untouched`() {
|
||||
assertEquals(all, pinnedFirst(all, emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pinned poets come first, in the order they were pinned`() {
|
||||
val order = pinnedFirst(all, listOf("/saadi", "/hafez")).map { it.nickname }
|
||||
assertEquals(listOf("saadi", "hafez", "moulavi", "ferdousi"), order)
|
||||
}
|
||||
|
||||
/** A pin for a poet who isn't in the list — filtered out by a search — must not crash. */
|
||||
@Test
|
||||
fun `a pin with no matching poet is ignored`() {
|
||||
val order = pinnedFirst(poets("hafez"), listOf("/saadi", "/hafez")).map { it.nickname }
|
||||
assertEquals(listOf("hafez"), order)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the unpinned keep their relative order`() {
|
||||
val order = pinnedFirst(all, listOf("/ferdousi")).map { it.nickname }
|
||||
assertEquals(listOf("ferdousi", "hafez", "moulavi", "saadi"), order)
|
||||
}
|
||||
}
|
||||
8
fastlane/metadata/android/en-US/changelogs/5.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/5.txt
Normal file
@ -0,0 +1,8 @@
|
||||
Keep the poets you read on the home screen. Press and hold any poet to pin
|
||||
them; pinned poets sit first, in the order you pinned them.
|
||||
|
||||
The home screen now opens on that shelf, with Ganjoor's own order and an
|
||||
alphabetical listing a tap away. Until you pin anything the shelf is simply
|
||||
Ganjoor's order, so there is never an empty screen.
|
||||
|
||||
Poets can be shown as cards or as a list, whichever reads better.
|
||||
8
fastlane/metadata/android/fa/changelogs/5.txt
Normal file
8
fastlane/metadata/android/fa/changelogs/5.txt
Normal file
@ -0,0 +1,8 @@
|
||||
شاعرانی را که میخوانید روی صفحهٔ نخست نگه دارید. انگشت را روی هر شاعر نگه دارید
|
||||
تا برگزیده شود؛ برگزیدگان به همان ترتیبی که انتخاب کردهاید، نخست میآیند.
|
||||
|
||||
صفحهٔ نخست حالا با همین قفسه باز میشود و ترتیب خود گنجور و فهرست الفبایی یک
|
||||
ضربه فاصله دارند. تا چیزی برنگزیدهاید، قفسه همان ترتیب گنجور است، پس هرگز
|
||||
صفحهای خالی نمیبینید.
|
||||
|
||||
شاعران را میتوان کارتی یا فهرستی دید، هرکدام که خواناتر است.
|
||||
9
fastlane/metadata/android/ur/changelogs/5.txt
Normal file
9
fastlane/metadata/android/ur/changelogs/5.txt
Normal file
@ -0,0 +1,9 @@
|
||||
جن شاعروں کو آپ پڑھتے ہیں انہیں صفحۂ اوّل پر رکھیں۔ کسی بھی شاعر پر دبا کر رکھیں
|
||||
تو وہ منتخب ہو جائے گا؛ منتخب شاعر اُسی ترتیب سے سب سے پہلے آتے ہیں جس ترتیب سے
|
||||
آپ نے انہیں رکھا۔
|
||||
|
||||
صفحۂ اوّل اب اسی فہرست سے کھلتا ہے، اور گنجور کی اپنی ترتیب اور حروفِ تہجی ایک
|
||||
ٹیپ کی دوری پر ہیں۔ جب تک آپ کچھ منتخب نہ کریں، یہ گنجور ہی کی ترتیب رہتی ہے،
|
||||
سو خالی صفحہ کبھی نہیں ملتا۔
|
||||
|
||||
شاعروں کو کارڈ کی صورت یا فہرست کی صورت، جو بہتر لگے، دیکھا جا سکتا ہے۔
|
||||
Loading…
Reference in New Issue
Block a user