Reading - Naskh is now the default face, and both Arabic-script fonts are registered at four weights so the text can be thickened for a lit screen. - Category listings show each poem's opening line, fetched best-effort from api.ganjoor.net since the data set doesn't carry excerpts. - Tap a couplet to save that passage or copy it; a saved passage keeps a tappable link back to its poem. Poem text is selectable for plain copying. Languages - Persian, Urdu and English, Persian by default whatever the phone's locale, applied in attachBaseContext and switchable from the top bar or the sheet. - English chrome uses Libron (OFL); poems stay naskh or nastaliq throughout. - Each language gets its own values-* folder: with Farsi only in values/, Android was resolving it to the Urdu strings, since a same-script locale outranks the default. Offline - Downloaded poets mirror the data set's layout under filesDir, so offline mode is one lookup rather than a parallel path. Downloads run one poet at a time, skip what's on disk, and resume by restarting. - Downloads screen lists every poet with their portrait and tick boxes for picking several, plus storage used, delete, and download-everything behind a size warning. - Offline mode refuses the network and says what's missing rather than blaming the connection. Also: poet sort (Ganjoor's order or alphabetical, via a Persian collator), RTL nav transitions, an original shamsa launcher icon with a monochrome layer for themed icons, and preferences written with commit so they survive the process being killed. F-Droid: dependenciesInfo off, optional signing so the build works with no keystore, fastlane metadata in three languages, cleartext traffic disabled. Verified on an API 36 emulator: downloaded a poet, pulled the emulator's network, read them offline, and confirmed an undownloaded poet reports being missing rather than erroring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
150 lines
7.1 KiB
Markdown
150 lines
7.1 KiB
Markdown
# Ganjoor for Android
|
||
|
||
<div dir="rtl">گنجور — خوانندهٔ شعر پارسی برای اندروید</div>
|
||
|
||
An Android reader for [Ganjoor](https://ganjoor.net), the open archive of Persian poetry —
|
||
240 poets and ~135,000 poems, laid out for comfortable long-form reading in Persian, Urdu
|
||
and Arabic script, online or fully offline.
|
||
|
||
Jetpack Compose, Material 3, `minSdk 24`. No account, no tracking, no server of its own.
|
||
|
||
## Reading
|
||
|
||
Poems are set as couplets: the two hemistichs of each line stack on a phone, the first aligned
|
||
to the start of the line and the second to the end, the way Ganjoor itself reads. Prose sections
|
||
— Golestan, Nowruznameh — fill the column instead. Category listings show each poem's opening
|
||
line under its title, because "Ghazal 237" tells you nothing.
|
||
|
||
Tap a couplet to save that passage or copy it; a saved passage keeps a tappable link back to the
|
||
poem it came from, which a plain copy would lose. The whole poem can be bookmarked from the top
|
||
bar, and any span of text can be selected and copied the usual way.
|
||
|
||
The whole interface lays out and navigates right-to-left, whichever UI language is chosen.
|
||
|
||
## Fonts
|
||
|
||
| Font | Used for | Licence |
|
||
|---|---|---|
|
||
| Noto Naskh Arabic | Poems, and the Persian/Urdu interface | OFL 1.1 |
|
||
| Noto Nastaliq Urdu | Poems, when nastaliq is selected | OFL 1.1 |
|
||
| [Libron](https://github.com/nicoverbruggen/libron) | The English interface only | OFL 1.1 |
|
||
|
||
Naskh is the default. Both Arabic-script faces are variable fonts registered at four weights, so
|
||
the text can be thickened — thin naskh strokes wash out on a lit screen, especially in the dark
|
||
themes. Real axis interpolation needs API 26+; below that Android synthesises the heavier
|
||
weights. Libron is a reading serif and never touches the poems: content is always naskh or
|
||
nastaliq, whatever language the interface is in.
|
||
|
||
Licences are in [`licenses/`](licenses/).
|
||
|
||
## Themes, size, language
|
||
|
||
Five themes, persisted: System, Light, Dark, Sepia and Sepia night. The two sepia schemes are
|
||
warm paper tones for long sessions; the dark one has no blue cast. Text size runs 14–40 sp with a
|
||
live preview in the settings sheet.
|
||
|
||
The interface speaks **Persian, Urdu and English**, Persian by default regardless of the phone's
|
||
locale. Switch from the globe in the top bar or from the settings sheet.
|
||
|
||
## Offline
|
||
|
||
Download a poet — or every poet — and read with no connection at all. Downloads are listed with
|
||
each poet's portrait and tick boxes for picking several at once; the screen shows how much space
|
||
they take and lets you delete any of them. Offline mode then refuses the network entirely and
|
||
reads only what's on the device, saying so plainly when you open something that was never
|
||
downloaded, rather than blaming your connection.
|
||
|
||
Downloads run one poet at a time and are resumable: anything already on disk is skipped, so
|
||
restarting an interrupted download picks up where it left off.
|
||
|
||
## Where the poems come from
|
||
|
||
There is no backend. Every "endpoint" is a JSON file in
|
||
[`anas-rashid/ganjoor-data`](https://github.com/anas-rashid/ganjoor-data), served over
|
||
jsDelivr's CDN, and addressed by the poem's own Ganjoor URL:
|
||
|
||
```
|
||
/hafez/ghazal/sh1 -> poets/hafez/ghazal/sh1.json
|
||
/hafez/ghazal -> poets/hafez/ghazal/_cat.json
|
||
```
|
||
|
||
Because paths are URLs, the app never touches the numeric id indexes. Downloaded files mirror
|
||
that same layout under `filesDir/offline`, which is why offline mode is a single lookup rather
|
||
than a parallel code path. Everything else lands in a 64 MB OkHttp disk cache.
|
||
|
||
One exception: opening lines aren't in the data set, so they're fetched best-effort from
|
||
`api.ganjoor.net` and cached. Adding an `Excerpt` field to `_cat.json` upstream would remove
|
||
that dependency — see the `ponytail:` note in `Ganjoor.kt`.
|
||
|
||
## Build
|
||
|
||
```sh
|
||
./gradlew :app:assembleDebug # APK
|
||
./gradlew :app:testDebugUnitTest # couplet grouping tests
|
||
```
|
||
|
||
Release signing is optional. Drop a `keystore.properties` next to `settings.gradle.kts` with
|
||
`storeFile`, `storePassword`, `keyAlias` and `keyPassword` to sign locally; without it the
|
||
release build still succeeds, unsigned. The file and any `*.jks` are gitignored.
|
||
|
||
## Layout
|
||
|
||
```
|
||
data/Ganjoor.kt models, couplet grouping, the static-file client
|
||
data/Offline.kt downloaded poems on disk
|
||
data/Downloads.kt download queue and progress
|
||
data/Bookmarks.kt saved poems and passages
|
||
ui/GanjoorApp.kt nav graph; routes carry Ganjoor URLs
|
||
ui/PoetsScreen.kt poet grid, search, sort
|
||
ui/CategoryScreen.kt collections and poem lists
|
||
ui/PoemScreen.kt the reader
|
||
ui/DownloadsScreen.kt multi-select offline downloads
|
||
ui/BookmarksScreen.kt saved poems and passages
|
||
ui/ReadingSettings.kt theme / font / weight / size / language / offline
|
||
ui/theme/ colour schemes and typography
|
||
```
|
||
|
||
Single module, no DI framework, no ViewModels yet — screens fetch through `Load` and lean on the
|
||
cache. `ponytail:` comments mark the deliberate shortcuts and what would replace them.
|
||
|
||
## Not built yet
|
||
|
||
- **Search within poems.** The data set has no search index; this needs either a client-side
|
||
index or `api.ganjoor.net`'s `/api/ganjoor/poems/search`.
|
||
- **Reading position.** Bookmarks are saved, but not where you stopped reading.
|
||
- **Recitations.** Ganjoor has audio for many poems; it isn't in this data set.
|
||
|
||
## Publishing to F-Droid
|
||
|
||
The build already meets the [quick start
|
||
guide](https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/):
|
||
|
||
- MIT licensed, with a `LICENSE` file.
|
||
- Every dependency is FOSS (AndroidX, Kotlin, OkHttp, Coil, Accompanist). No Play Services, no
|
||
Firebase, no analytics, no trackers. Only the `INTERNET` permission, and cleartext disabled.
|
||
- `versionCode`/`versionName` are literals in `app/build.gradle.kts`, not derived from git.
|
||
- Dependency versions are all pinned; no version ranges.
|
||
- `dependenciesInfo` is switched off — that blob is signed with a Google key and isn't
|
||
reproducible, so F-Droid rejects APKs carrying it.
|
||
- The build succeeds with no keystore, so F-Droid can sign with its own key.
|
||
- Store listing lives in `fastlane/metadata/android/{en-US,fa,ur}/`. Drop screenshots into each
|
||
locale's `images/phoneScreenshots/`.
|
||
|
||
Two things to do before submitting: tag a release (`v0.1.0`), and check that F-Droid's build
|
||
server supports **AGP 9.4.1** — it is new, and that is the most likely thing to hold up a merge.
|
||
|
||
## Licensing
|
||
|
||
The app code is MIT (see [`LICENSE`](LICENSE)). What it builds on:
|
||
|
||
- **The poetry** is classical Persian verse, long out of copyright.
|
||
- **The data set** ([`ganjoor/ganjoor-data`](https://github.com/ganjoor/ganjoor-data)) carries no
|
||
licence file at all. Worth asking upstream to add an explicit one.
|
||
- **[GanjoorService](https://github.com/ganjoor/GanjoorService)**, Ganjoor's own backend and site,
|
||
is GPL-3.0. This app uses none of its code — only data over HTTPS — so it is not a derivative
|
||
work of it. Nothing in Ganjoor's repositories restricts AI-assisted use.
|
||
- **The icon** is original: an eight-point shamsa, the star that tiles Persian architecture.
|
||
Ganjoor's own app icons are unlicensed and are their mark, so they are not used here.
|
||
|
||
Not affiliated with or endorsed by Ganjoor.
|