Add poem search, an OLED theme, and credit every licence

Search: full-text across the poems, scoped to all poets, a few, or one. The
static data set has no index, so this is ganjoor.net's own search endpoint,
which scopes to one poet at a time — several picked poets run as several
queries and merge. Entry is the poets filter box, which now sits in the bottom
bar within thumb reach and offers its own words to the poem search one tap
further. Snippets show the line the term is actually on.

Themes: a sixth, true black, for OLED panels where an unlit pixel costs no
power. Surfaces step up in near-blacks so cards stay distinguishable.

Licences: every component is now credited in-app under Reading settings >
About & licences, with the full text bundled in assets/licenses — the OFL
requires the licence to travel with the software, so linking to it wasn't
enough. licenses/README.md indexes the same thing for the repo. The credits
list and the licence texts lay out left-to-right in English, since the rest of
the app is right-to-left for the poetry's sake and English prose is unreadable
right-aligned.

Fixes a crash: the settings sheet was composed outside the provider supplying
LocalOpenAbout, so opening it threw.

Search decoding is covered by a unit test against a real API response; the
rest verified on an API 36 emulator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anas RashidandClaude Opus 5 committed 2026-10-04 01:05:52 +02:00
1 parent 1accb5ee78
commit a5fc608bca
26 files changed
+1319 -27

No files matched your search

@@ -0,0 +1,64 @@
package com.ganjoor.android
import com.ganjoor.android.data.SearchHit
import com.ganjoor.android.data.snippet
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Trimmed from a real api.ganjoor.net response: the live endpoint returns ~30 fields per poem,
* so the decoder has to ignore almost all of them.
*/
private const val RESPONSE = """
[
{
"id": 2130,
"title": "غزل شمارهٔ ۱",
"fullTitle": "حافظ » غزلیات » غزل شمارهٔ ۱",
"urlSlug": "sh1",
"fullUrl": "/hafez/ghazal/sh1",
"plainText": "الا یا ایها الساقی ادر کأسا و ناولها\nکه عشق آسان نمود اول ولی افتاد مشکل ها\nبه بوی نافه ای کآخر صبا زان طره بگشاید",
"htmlText": "<div>…</div>",
"sourceName": "ویکی‌درج",
"published": true,
"coupletsCount": 7,
"verses": [],
"recitations": []
}
]
"""
class SearchHitTest {
private val json = Json { ignoreUnknownKeys = true }
@Test
fun `a live search response decodes despite its many unused fields`() {
val hits = json.decodeFromString<List<SearchHit>>(RESPONSE)
assertEquals(1, hits.size)
assertEquals("/hafez/ghazal/sh1", hits[0].fullUrl)
assertEquals("حافظ » غزلیات » غزل شمارهٔ ۱", hits[0].fullTitle)
}
@Test
fun `the snippet is the line the term is on, not the opening line`() {
val hit = json.decodeFromString<List<SearchHit>>(RESPONSE).first()
assertTrue(snippet(hit, "نافه").contains("نافه"))
assertTrue(snippet(hit, "عشق").startsWith("که عشق"))
}
@Test
fun `a term that isn't there falls back to the opening line`() {
val hit = json.decodeFromString<List<SearchHit>>(RESPONSE).first()
assertEquals("الا یا ایها الساقی ادر کأسا و ناولها", snippet(hit, "زرافه"))
}
@Test
fun `a poem with no text yields an empty snippet rather than throwing`() {
assertEquals("", snippet(SearchHit(id = 1), "عشق"))
}
}