Commit Graph

52 Commits

Author SHA1 Message Date
3486a89b56 Test the rule that chooses the dictionary's container
The guard added in #3 decides whether the dictionary opens beside the poem or as
a sheet over it, and until now nothing proved it. I tried four times to verify it
by hand and failed every time: reaching it needs a tap landing on the glyphs of
one Persian word inside a SelectionContainer, and adb's synthetic taps either
became a long press and opened the selection menu, or landed between words and
opened the couplet's actions instead. The verses are not exposed to uiautomator
either, so there is nothing to aim at.

The arithmetic behind the rule can be tested even though the gesture cannot, so
it moves into a pure function and gets four cases: a tablet keeps the panel, a
foldable open in portrait does not, a phone never gets it however wide the page
is measured, and the boundary sits exactly at the panel plus the minimum measure
(616dp passes, 615dp does not).

No behaviour changes — this is the same expression, named and covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 18:43:01 +02:00
Anas Rashid
701b984f4b
Merge pull request #4 from anas-rashid/fix/dictionary-panel-crushes-narrow-page
Open the dictionary as a sheet where a panel would crush the poem
2026-10-07 18:34:27 +02:00
41d776a35d Merge main, keeping only the part of this branch that is still needed
main has moved on since this branch opened, and most of what it carried has been
answered better there:

- the reading settings no longer need a fallback to the sheet, because they fold
  the columns away and take the room that frees (aab8b2e);
- the two panels no longer share a width, because the dictionary is now 216dp
  against the settings' 360dp.

Both were dropped: the conflicting files are taken from main as they stand.

What survives is the reason this branch exists. Which container the dictionary
uses is still decided by the width of the *window*, and that is the wrong
question — what matters is what is left of the page once the columns have taken
theirs. On a 700dp foldable that is about 450dp, and a panel beside it leaves
the verse a couple of characters a line. It now opens beside the poem only while
the page keeps 400dp, and falls back to the sheet below that, which covers the
foot of the poem but leaves every line whole.

Recalculated for the narrower panel: a tablet keeps the panel (~900dp page less
216dp leaves 684dp), a foldable in portrait does not (236dp).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 18:33:24 +02:00
bcdad2e699 Merge fix/one-side-panel-at-a-time
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 18:28:34 +02:00
ce26676d4e Merge fix/recitation-keeps-playing
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 18:28:34 +02:00
f60f9b4079 One side panel at a time, and a narrower one for the dictionary
Two fixes to the left of the reader.

The dictionary and the reading settings both want that side of the screen, and
opening the second put two panels there at once — or, where there was no longer
room for two, left the dictionary as a sheet in the middle of the page while the
settings sat beside it. Either way the reader is asked to look in two places.
The settings now replace the dictionary; closing them leaves the poem, which is
where the reader was.

The dictionary also no longer takes the settings' full 360dp. It is read against
the line it came from, so the verse should keep the width: 216dp, a little over
half. The settings keep 360dp, which costs nothing — they fold the columns away
and take the room that frees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 18:25:13 +02:00
488f48ad39 Keep the reading playing when the poem is scrolled, and let it be scrubbed
The player was an item of the LazyColumn, so scrolling it off screen disposed
it, and the DisposableEffect that releases the MediaPlayer cut the reading off
mid-line. It is now pinned above the scrolling text: it stays in composition for
as long as the poem is open, and stays in reach while you read further down,
which is when a player is wanted.

A seek bar comes with it — position, a draggable handle, and the two times —
shown only once the length is known, because a bar that cannot be dragged
anywhere is furniture. The handle follows the audio on a 250ms poll while it
plays and stops fighting the finger while it is being dragged. MediaPlayer
already seeks; this needed no media library.

The bar and its times run left to right inside the otherwise right-to-left page,
because time does, whatever the script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 18:22:28 +02:00
Anas Rashid
c3b22fdf0e
Merge pull request #3 from anas-rashid/claude/design-system-extraction-c49naf
Fold the columns away while settings are open; couplets in rounded cards
2026-10-07 18:13:20 +02:00
936fd3cf3b Open the dictionary as a sheet where a panel would crush the poem
On a book-style foldable held open in portrait — 700dp, inside the 600–839dp
band — tapping a word made the poem unreadable. The panel is a fixed 360dp and
the columns keep their widths, so the page was left about 75dp: the verse broke
to one or two characters a line, and the breadcrumbs and the metre shattered the
same way.

The panel was chosen from the width of the window. That is the wrong question.
What matters is what is left of the page once the columns have taken their room,
so both panels now ask that instead:

- the dictionary opens beside the poem only while the page keeps 400dp;
- reading settings open beside the browser only while it keeps its own 600dp,
  below which the panel would squeeze the columns and the poem into less than
  the layout is built for.

Where they do not fit, the bottom sheet is used, exactly as on a phone: it
covers the foot of the poem but leaves every line whole. A tablet is unaffected
— verified at 2560x1600 that the panel still opens and the poem still reads.

The two panels now share one width, and the design notes record the rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 18:04:39 +02:00
Claude
64a67ddbbd
Set each couplet in a dim rounded card, on every screen size
Each line of verse now sits in its own soft card (surface-container-high,
12dp corners), so the eye finds where one couplet ends and the next
begins, and the couplet's chevron, actions and summary visibly belong to
it. On OLED black that step is all but black, so the card takes the next
one up there. Prose stays bare: a paragraph in a box reads as a quotation.

Text keeps at least 6:1 on the card in every theme. The phone layout is
otherwise unchanged.

Updates the Couplet notes, the stylesheet and wireframes 4-8, and adds
wireframe 9 for the phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhmDdrN5hgWrgRcFLAMsV6
2026-10-07 16:04:08 +00:00
Claude
aab8b2e3e1
Fold the columns away while reading settings are open
On a large screen the settings panel opens on the left while the poets
and book columns sit on the right, which left the page squeezed between
them. The columns now step aside while the panel is open and come back
when it closes, but only if they were showing: this is a transient flag
(LocalSidePanelOpen), never the saved reader-view choice. The floating
show-the-list button stays hidden while the panel is open too.

Updates wireframe 8 and the ColumnBrowser notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhmDdrN5hgWrgRcFLAMsV6
2026-10-07 16:00:55 +00:00
f21f4fa727 Keep home and the reading settings up while a page loads
The skeleton screens carried a top bar with nothing but Back, so going home or
changing the theme meant waiting for a poem you might not have wanted. Neither
action depends on the content: both are now in the loading bar for the poem and
the category alike, from the first frame.

Share and bookmark are not. They need a poem to act on, so they still arrive
with it — the bar fills out as the content lands rather than appearing whole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 17:08:19 +02:00
b3f4e527f1 Look up the word, not the word and the comma after it
A tapped word was taken as the longest run of characters in U+0600–U+06FF, but
that block is not only letters. The Arabic comma ، semicolon ؛ question mark ؟
full stop ۔ and both sets of Indic digits all live inside it, so the run ran
straight through them: tapping دستم in «ز دستم، صاحب‌دلان» looked up «دستم،»,
which no dictionary carries and which the near-word search cannot rescue either,
since the punctuation counts against every candidate's letter overlap.

The run now keeps only letters, the marks that sit on them, and the joiner,
decided by Unicode category rather than a hand-kept list of code points. Harakat
and tatweel stay in: they sit inside a word — منِ is one word — and normalise()
strips them before the lookup. Latin punctuation already fell outside the block.

The range is also what the verse highlights, so the comma is no longer painted
as part of the tapped word.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 17:08:19 +02:00
Anas Rashid
1a1c86ee64
Merge pull request #2 from anas-rashid/claude/design-system-extraction-c49naf
Open reading settings in the left-hand panel on large screens
2026-10-07 16:57:43 +02:00
Claude
af330d081f
Merge remote-tracking branch 'origin/main' into claude/design-system-extraction-c49naf 2026-10-07 14:57:04 +00:00
a118fc2510 Fix the build: AnimatedVisibility resolved to the RowScope overload
main did not compile. The floating button that brings the columns back sits in
a Box, but that Box is inside the browser's Row, so RowScope was still an
implicit receiver where AnimatedVisibility was called. Kotlin picked
RowScope.AnimatedVisibility — which has no alignment parameter and cannot take
an implicit receiver there — and the Kotlin compile failed.

Moving it into its own composable gives it a scope of its own, so the plain
overload resolves and Modifier.align stays with the Box that provides it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 16:54:59 +02:00
Claude
8451cc45fb
Open reading settings in the left-hand panel on large screens
On a tablet or an unfolded foldable the gear now opens reading settings
as a panel on the left, where the dictionary opens, instead of a bottom
sheet. The page stays in view beside it, so a change of theme, font,
weight or size shows on the poem as it is made. The sheet and the panel
share one ReadingSettingsContent; phones keep the sheet.

Adds wireframe 8 and notes in the design docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhmDdrN5hgWrgRcFLAMsV6
2026-10-07 14:51:12 +00:00
Anas Rashid
f0c1dd5541
Merge pull request #1 from anas-rashid/claude/design-system-extraction-c49naf
Design system, and a column layout for tablets and foldables
2026-10-07 16:10:53 +02:00
Claude
ebbf737dac
Add the design system extracted from the app
design/ holds the tokens for all six themes, the type scale, spacing,
radii and sizes, the brand and usage guidelines, a guide and static
preview for each component, the logo and icons as SVG, and wireframes of
the tablet and foldable column layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhmDdrN5hgWrgRcFLAMsV6
2026-10-07 14:06:52 +00:00
Claude
cf65072af4
Add a column layout for tablets and unfolded foldables
At 600dp and wider, opening a poet turns the home grid into a narrow
column of poets on the right, with a column for each level of the open
book (books, chapters, poems) beside it and the page in the rest of the
screen. Columns narrow as more open and while a poem is being read, and
can be hidden for a reader view, with a floating button to bring them
back. Below 840dp only the newest list column is shown. The routes are
the same as on a phone, so folding or unfolding keeps your place.

On large screens the dictionary opens in a panel to the left of the poem
rather than as a bottom sheet, and the looked-up word stays highlighted.

Loading no longer covers the whole screen with a spinner, on phones too:
only the part that is waiting shows a skeleton shaped like its content,
under the real top bar, and fades into the content when it arrives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhmDdrN5hgWrgRcFLAMsV6
2026-10-07 14:05:07 +00:00
50bbe0b729 Correct the unofficial statement: separate, not unendorsed
The first wording claimed this app is "not affiliated with, endorsed by" the
owners of ganjoor.net and that they "bear no responsibility for it". That
overstates the distance: permission to build it was given. What is true, and
all that needed saying, is that this is a separate project and they do not run
or manage it. The denial of endorsement is gone from all three languages and
from both places in the README.

The author is named in full — Muhammad Anas Rashid — everywhere it appears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 15:12:11 +02:00
5d12a1c29c Merge 0.3.0: pinned poets, list view, and the unofficial-client statement
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 14:59:02 +02:00
16bcd6949f Say plainly that this is an unofficial client, and name what it is built on
Someone arriving from ganjoor.net had to infer from the credits that this is a
separate project. The About page now opens with it, before the licence list,
in all three interface languages: independent, unofficial, maintained by Anas
Rashid, not affiliated with or endorsed by the owners of ganjoor.net, who carry
no responsibility for it. The README leads with the same paragraph and points
issues here rather than at the Ganjoor project.

The README also gains a full inventory of the open source this app is built on
— Ganjoor and GanjoorService, ganjoor-data, Daneshjoo, Wiktionary and Urdu
Wiktionary, wiktextract, the three fonts, and every library — each with its
repository and terms. licenses/README.md already carried most of it, but a
reader looking for "which repos does this use" should not have to open a second
file to find out.

GanjoorService is listed with the point that matters: GPL-3.0, no code used,
only data over HTTPS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-07 14:58:28 +02:00
Anas Rashid
b4a8efe9d4 Refresh the 0.3.0 checksum after a clean rebuild
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 20:39:45 +02:00
Anas Rashid
929aee2a02 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>
2026-10-04 20:36:59 +02:00
Anas Rashid
a5cc0421b6 Archive 0.3.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 20:28:12 +02:00
Anas Rashid
c9269713d1 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>
2026-10-04 20:26:26 +02:00
Anas Rashid
d7568d1f49 Credit Material Symbols, and state the assistant's terms
Two gaps in the licence file, both from work done after it was written.

The share, lookup and assistant glyphs are Material Symbols path data copied
into our own vector files. The Libraries table credits the Material icons
library, which is not the same thing as copying its paths, so the drawables now
have their own entry and appear in the app's About page.

The assistant was not mentioned at all. It ships no vendor SDK, no key and no
default endpoint, so nothing non-free is distributed — but a reader may point
it at a service whose terms are their own, and what a model writes back belongs
to neither Ganjoor nor this app. Written down for F-Droid's sake as much as the
reader's.

No new dependencies came with any of it: the assistant reuses the HTTP client
and JSON parser already here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 20:19:00 +02:00
Anas Rashid
e3cad61e9e Keep every release build
Three so far, each signed with the same key so any of them upgrades any other
without losing bookmarks: 0.1.0 as it stood last night, 0.2.0 with share and
the assistant, and 0.2.1 with answers in the page.

These are binaries in git history, which is permanent and makes every clone
carry them. Gitea's own releases attach a build to a tag without that cost;
keeping them here is deliberate, so a version can be reinstalled without
finding the commit and rebuilding it. Tags v0.1.0, v0.2.0 and v0.2.1 mark the
commits each was built from, so they stay reproducible either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 20:12:04 +02:00
Anas Rashid
ec05989f7d Release 0.2.1
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:51:20 +02:00
Anas Rashid
5daebcd87a Show the assistant's answers in the page, and make a couplet's options findable
Translations appear under the summary they translate. Leaving the poem to read
one — to a sheet over the top, or to another app entirely — breaks the reading,
which is the thing the feature is meant to help with. Ganjoor prints a summary
under each couplet as well as under the poem, so both now carry a translate
button, the couplet one only once a server is configured: a button under every
couplet earns its space only if it can answer.

A couplet's own actions were effectively unreachable. They opened on a tap that
landed between words, and on a full line of poetry a tap almost always lands on
a word, which opens the dictionary instead — three attempts here failed before
one worked. There is now a chevron at the end of every couplet, and the same
actions sit at the foot of the word sheet, inside its scroll rather than after
it, where they were pushed past the bottom of the sheet with no way to reach
them.

Answers are remembered. A LazyColumn disposes what scrolls out of view, which
restarted the request behind it: one tap on translate sent two, confirmed
against a stub server, and scrolling away and back would have sent more. On a
metered API that is money.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:49:15 +02:00
Anas Rashid
d92445f21e Release 0.2.0
Pre-1.0 while the app is still being shaped: 0.1.0 was the build after the
dictionary work, this is the one with share and the optional assistant.

The APK is named for its version, because a file called app-release.apk tells
whoever receives it nothing. Changelogs in all three languages, one per
versionCode, which is the layout F-Droid reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:21:53 +02:00
2f9cb0e211 Merge pull request 'feature/ai-assistant' (#2) from feature/ai-assistant into main
Reviewed-on: #2
2026-10-04 17:16:53 +00:00
Anas Rashid
d320137063 Drop the "ask an assistant" entry from the selection menu
With no local server configured it opened the same chooser Share does, only
with a prompt in front of the text. Two entries for one chooser makes the
reader choose twice, and the third entry pushed Look up into the overflow;
both now fit in the bar.

Asking moves to the lookup screen's top bar, where it keeps the part that was
actually distinct: a configured server answers in place, and without one the
question still goes out with "translate this and explain what it means"
already written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:14:22 +02:00
Anas Rashid
90b527522b Use the AI sparkle for the assistant action
Material's auto_awesome, the shape that has come to mean "ask a model" — the
same one Gemini and the rest use. Theirs are trademarks and have no place in an
F-Droid build; this one ships under the Apache licence already covering the
app's icons.

It now marks the action everywhere it appears: the selection-menu entry, the
lookup screen's top bar, and the translate and explain buttons in the reader,
which were text alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:02:27 +02:00
Anas Rashid
2f16b52c2c Use the standard glyphs for share, look up and ask
Share was already the Material share icon in both top bars, but appeared as
bare text in the couplet actions and under an assistant's answer, so the same
action looked like two different things. It now carries the glyph everywhere.

The selection-menu entries had no icons of their own, so the system drew the
app logo three times over. Each alias now has the icon for its verb, from
Material's own path data: the share glyph people know from every other app,
a magnifier for the dictionary, and send for handing a question elsewhere.
Whether the floating toolbar draws them is the system's decision, not ours.

"Ask an assistant" had a magnifier, which belongs to searching. It sends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:00:09 +02:00
Anas Rashid
65bc9ed0fc Talk to Claude as well as the OpenAI-shaped services
Everything else worth connecting to — Ollama, LM Studio, llama.cpp, LocalAI,
OpenAI, DeepSeek, and Gemini through its compatibility endpoint — speaks the
OpenAI chat-completions shape, so one client covered them all. Anthropic does
not: it is /v1/messages, an x-api-key header, and a reply that arrives as
content blocks rather than a message. That one exception is now handled, keyed
off the host, so the callers never see the difference.

The endpoint note lists the services by name and URL. Knowing the app "supports
OpenAI-compatible servers" is not the same as knowing what to type.

Presets stay self-hosted only: naming a paid service in a one-tap button is
steering people towards it, and the field takes any URL regardless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 18:54:19 +02:00
Anas Rashid
576a07ae31 Wire up the optional AI assistant
The page existed but nothing reached it. It now has a route, a provider in
MainActivity, and an entry at the foot of the reading-settings sheet beside
About, where it reads as one more optional setting rather than a feature of
the app.

Two actions use it: translating Ganjoor's own Persian summary under a poem,
and explaining a selected couplet. Both go through AssistantAction, which asks
the reader's own server when one is configured and otherwise hands the question
to another installed app. Neither path needs anyone to have set anything up,
which matters because the whole feature is optional.

Urdu for AI is مصنوعی ذہانت, not the transliterated "اے آئی"; the Farsi now
says هوش مصنوعی to match, rather than "smart assistant".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 18:49:37 +02:00
Anas Rashid
3d4f7aae3a Share verses and poems, and reach the dictionary from any app
Selected text now gets three entries in the system selection menu — look up,
ask an assistant, and share — and the poem screen gains a share action of its
own, next to the per-couplet save and copy.

Compose 1.10 stopped routing SelectionContainer through LocalTextToolbar, so a
custom entry cannot be added to that menu from inside the reader.
ACTION_PROCESS_TEXT goes round it: the system builds part of the menu from
installed activities handling that intent, so these appear here and in every
other app. Three activity-aliases rather than one Ganjoor entry, because the
menu is a place for verbs: a reader who means "look this up" should not have to
pick an app first and then say what they wanted.

"Ask an assistant" cannot name Claude or Gemini — only those apps can put their
own name in that menu. It opens the share chooser with the question already
written, which lists whichever assistant is installed. That hand-off is also
what keeps this acceptable under F-Droid's rules: no vendor SDK, no API key,
nothing but an intent the reader confirms.

The activity is opaque and themed rather than a transparent one hosting a
sheet. PROCESS_TEXT starts it in its own task, so the app the text came from is
not behind this window, and a scrim over nothing is just a grey screen.

WordLookup and AssistantAnswer are split out of their sheets so the same code
serves as a sheet in the reader and a screen here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 18:45:13 +02:00
9445f44759 Merge pull request 'Dictionary coverage, pronunciation, and chapter order' (#1) from feature/dictionary-coverage-and-chapter-order into main
Reviewed-on: #1
2026-10-04 16:12:01 +00:00
Anas Rashid
0d083b6638 Make OLED a switch on the dark themes, and stream recitations
OLED was a sixth theme, which meant choosing it threw away sepia night. It is
a switch now, applied to whichever dark theme is in use: the surfaces move
towards black rather than being replaced by it, so sepia night keeps its warmth
and only the background goes truly black. Text and accents are untouched.
Anyone who had picked the old Black theme comes out as Dark with the switch on.

Recitations: Ganjoor hosts readings of the poems — twenty of Hafez's first
ghazal alone — and the poem screen now plays them. Streamed, never stored,
because a dozen readings of one ghazal would dwarf the poems themselves, which
also makes this the one part of the app that genuinely needs a connection; it
doesn't appear without one. The platform's MediaPlayer rather than ExoPlayer:
one URL, play and pause, nothing to justify a media library.

Pronunciations were laid out as a flowing row, which put Latin IPA and Urdu
spelling in the same right-to-left run — the chips reordered and each label
ended up under someone else's value. They are one per line now, in a single
direction.

Verified on an API 36 emulator: sepia night with the switch on is black with
warm cards; the player shows play on arrival with no network call, and streams
on press.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 18:05:21 +02:00
Anas Rashid
0df736fd89 Show how a word is pronounced, Classical Persian first
Wiktionary carries pronunciation for 45,000 of these headwords and the build
was throwing it away. It is now kept: 101,306 entries of IPA, each tagged with
the variety it belongs to, which matters here because a word in a 14th-century
ghazal was not said the way Tehran says it now — عشق is /ˈʔiʃq/ in Classical
Persian and [ʔeʃɢ̥] in Iran today, and the app leads with the former.

The IPA's dots and stress marks are the syllable breakdown. Urdu Wiktionary
adds its own, in Urdu script: the fully vowelled spelling عِشْق and the split
عِش + قوں, both pulled out of its wikitext.

Costs 7.7 MB of database, taking it to 94 MB.

Verified on an API 36 emulator: tapping عشق shows the Classical Persian IPA,
the Urdu syllable split, the vowelled spelling and two modern variants above
the definitions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 17:48:12 +02:00
Anas Rashid
b16d020ce0 Add the full Arabic Wiktionary, and answer in Urdu before English
Classical Persian quotes Arabic outright — Hafez opens with a whole hemistich
of it — and none of it resolved before. The full Arabic export brings 36,627
entries and 819,608 new form pairs, and the forms are the point: السّاقی,
الناس, تَلْقَ and تَهْوی are all conjugated or carry the article, so they only
reach a definition through that index. Nine of the ten Arabic words in the
sample now answer where none did.

The cost is real and worth stating: the database goes from 28 MB to 87 MB and
the release APK from 10.6 MB to 29.2 MB, with another 87 MB unpacked on first
run, so about 116 MB installed.

Results are now ordered the way a reader of this app wants them: the Urdu
definition first, because it needs no translating, then Persian, then sources
keyed on another language, with English arriving only through whatever is left.
Arabic sorts last outright — its index is larger than every other source
combined, which makes it the likeliest to match by coincidence.

No Persian-to-Urdu dictionary. Wiktionary's Persian entries carry no
translations at all; the tables live only on English pages, in a 3.3 GB export,
so it would mean pivoting through an English sense. A sample of that file
projects about 6,900 Persian words with any Urdu equivalent, mostly modern
dictionary vocabulary rather than the language of the poems — not worth the
pivot. tools/README.md records why, so the question doesn't get reopened from
scratch.

Verified on an API 36 emulator: عشق returns all five sources with the Urdu
definition at the top.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 17:42:09 +02:00
Anas Rashid
c0cb6a8ab6 Add the Urdu dictionaries, and suggest near words when nothing matches
Two Urdu sources join the Persian ones. Wiktionary's Urdu extract gives Urdu
headwords glossed in English, and Urdu Wiktionary itself gives definitions
written in Urdu — the only source here that does. The latter is thin, about
3,100 usable entries out of 31,000 pages since many are stubs, but for a word
it carries an Urdu reader is better served by it than by a translation into
English: عشق comes back as شدید جذبۂ محبت، گہری چاہت، محبت، پریم، پیار.

Every definition now names the dictionary and its language pair, and lays out
in the direction its own script reads, so an Urdu definition is right-aligned
beside a left-aligned English one.

When nothing matches, the sheet offers near words ranked by how many letters
they share with what was looked up, drawn from an index range scan on the
leading letters rather than a scan of the whole table. خودکامی, which has no
entry, offers خودکامه — the lemma it wants.

Measured honestly: the Urdu sources add little coverage over Persian — nine
words from the English-glossed extract, two from Urdu Wiktionary, against
1,285 from thirteen poems. They are here because an Urdu reader wants Urdu,
not because they widen the net.

The suggestion ranking is verified against the built database rather than only
on device: خودکامی → خودکامه, شیرازی → شیراز, مشکلها → مشکل. On an API 36
emulator all four sources answer عشق with their labels.

Known rough edge: affix stripping across four languages can mislead. ناولها
reaches ناول, the Urdu for "novel", which is not what Hafez meant. The matched
headword is always shown, so it is visible rather than silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 17:29:38 +02:00
Anas Rashid
8aad086b08 Fix chapter order, and cover more words without more data
Chapter order: Golestan's دیباچه was rendering below the eight باب because the
listing drew every child category and then every poem. ganjoor.net interleaves
them — a book's preface first, then its chapters, then its remaining poems, so
Hafez reads مقدّمه, five collections, مثنوی, ساقی‌نامه. Ganjoor decides this
with each poem's MixedModeOrder (1 above the chapters, 0 below), which the
exported _cat.json doesn't carry and the live API only exposes one poem at a
time, so prefaces are matched by title for now. Adding MixedModeOrder to the
Poems entries in ganjoor-data would make it exact.

Coverage: I went looking for Arabic and Urdu Wiktionary and measured them
instead of assuming. Against 1,285 distinct words from thirteen poems, Urdu
added nine words and Arabic could reach at most five — the uncovered words
were never Arabic, they were Persian morphology the lookup didn't handle:
enclitic pronouns (آیدت, باشدش, تربتش), negation stacked on a prefix
(برنیاید), and compounds. Deepening the affix chain to two passes takes 87% to
91% with no new data at all, so neither dictionary ships.

Each definition now names its language pair rather than just its source, so a
reader can tell what they are looking at.

Dropped the selection-toolbar lookup: Compose 1.10 stopped routing
SelectionContainer through LocalTextToolbar, so a custom toolbar is never
asked to show, and the replacement in foundation's contextmenu package is
internal. Tapping a word already does the lookup; the note in PoemScreen says
when to revisit.

Verified on an API 36 emulator: Golestan lists دیباچه first, and tapping
نافه‌ای resolves through its affix to ناف with both sources labelled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 17:20:24 +02:00
Anas Rashid
c548e6ec07 Add a tap-a-word dictionary, layering Wiktionary and Daneshjoo
Tapping a word in a poem now opens its meaning. Neither source covers enough
alone — Daneshjoo reaches 71% of real poem vocabulary — so both ship, each row
carrying its source so the credit stays attached and either can be dropped
later. Together with the lookup chain that reaches 88%, and 13 of the 55
remaining misses are Arabic lines quoted inside Persian poems.

Lookup widens until something matches: the word as written, the lemma it
inflects from, the word with an affix stripped, then the parts of a ZWNJ
compound. The lemma step is what makes classical verse readable, and it comes
from Wiktionary's 149,589 form->lemma pairs: افتاد is only findable as افتادن.
The Arabic definite article is stripped too, since poems quote Arabic.

Two things the build taught me. Wiktionary's 93 MB export is 0.94 MB of
definitions wrapped in inflection tables, etymology templates and IPA, so
tools/build_dictionary.py keeps the definitions and the form index and drops
the rest. And the packager silently gunzips .gz assets and strips the
extension, which shipped the database under a name the code wasn't opening —
every lookup failed silently until the APK listing gave it away.

Hit testing needed care as well: nastaliq is set with 2.4x leading, so most of
a line box is empty space, and a tap there clamps to the line's first
character — which made every tap return the opening word. Taps are now checked
against the baseline band, with the multipliers left as a knob for swapped
fonts. Definitions are English, so they read left-to-right inside the
otherwise right-to-left sheet.

Verified on the release build, where R8 could have broken the SQLite path:
tapping عشق returns both sources, and دانند resolves to دانستن.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 02:02:00 +02:00
Anas Rashid
b545220bbc Stop loading screens flashing white against the chosen theme
Two causes. The activity's window was a Light platform theme with no
values-night, so the bare window was white whatever theme was picked. And on
the category and poem screens Load sits outside the Scaffold, so while a page
was loading nothing painted a background and that white window showed through.

The window is now painted from the chosen theme before the first frame, and
kept in step when the theme changes, which a static XML theme could not do for
sepia or OLED. Load paints its own loading and error states too.

Android 12+ draws its splash before any app code runs, so that one can only
follow the system light/dark setting; picking sepia or OLED on a light-mode
phone still shows a light splash for a frame.

Verified on an API 36 emulator under the OLED theme: the loading screen is now
true black with the themed spinner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 01:14:02 +02:00
Anas Rashid
a5fc608bca 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>
2026-10-04 01:05:52 +02:00
Anas Rashid
1accb5ee78 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>
2026-10-04 00:28:52 +02:00
Anas Rashid
abcffbc43b Add offline reading, bookmarks, three UI languages and downloads
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>
2026-10-03 23:20:42 +02:00