Skip to content

Commit aa36fd4

Browse files
committed
feat(quran): download ayah-marker ornament pack (schema-2 manifest)
1 parent f7e058c commit aa36fd4

5 files changed

Lines changed: 115 additions & 3 deletions

File tree

src/app/(tabs)/quran.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ const QuranScreen = () => {
148148
}, [setSheetReturnAyah])
149149
);
150150

151+
// Ensure the edition's ayah-marker frames are installed (covers editions added
152+
// before the ornament pack shipped). Idempotent + tiny (~20KB); no-ops once on
153+
// disk. New downloads already include markers via the edition download.
154+
useEffect(() => {
155+
if (showReader) QuranDownload.ensureMarkersInstalled(currentVersion);
156+
}, [showReader, currentVersion]);
157+
151158
// First time the reader is shown, run the gesture walkthrough.
152159
useEffect(() => {
153160
// eslint-disable-next-line react-hooks/set-state-in-effect

src/services/quran-download-plan.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Images and meta/bounds are versioned independently; we track both installed
33
// versions to re-download only the leg that changed and keep bounds geometry
44
// matched to its render.
5-
export type InstalledVersions = { images?: string; meta?: string };
5+
export type InstalledVersions = { images?: string; meta?: string; markers?: string };
66

77
// Decide which legs to (re)download. Meta is "ok" only when its own version
88
// matches AND the images it was built against (`requiresImages`) are the ones

src/services/quran-download.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { QuranManifestService } from "@/services/quran-manifest";
1616
import { planEditionDownload, type InstalledVersions } from "@/services/quran-download-plan";
1717
import { useQuranStore } from "@/stores/quran";
1818
import { AppLogger } from "@/utils/appLogger";
19-
import type { DownloadProgress } from "@/types/quran";
19+
import type { DownloadProgress, QuranManifestVersion } from "@/types/quran";
2020

2121
const log = AppLogger.create("quran-download");
2222

@@ -125,6 +125,12 @@ const getVersionDir = (version: MushafVersion): Directory => {
125125
return new Directory(Paths.document, `quran/${version}`);
126126
};
127127

128+
// Where the ayah-marker medallion frames live (AyahMarker reads them here);
129+
// populated by extracting the edition's ayah-marker ornament pack.
130+
const getMarkersDir = (version: MushafVersion): Directory => {
131+
return new Directory(Paths.document, `quran/${version}/markers`);
132+
};
133+
128134
// Dark-theme images live in a sibling directory so they can be downloaded and
129135
// deleted independently of the main (light) bundle.
130136
const getDarkVersionDir = (version: MushafVersion): Directory => {
@@ -315,6 +321,57 @@ const downloadAndExtractBundle = async (
315321
}
316322
};
317323

324+
// Download + extract the edition's default ayah-marker frame pack into the markers
325+
// dir, when absent or out of date. Self-contained + non-fatal: any failure is
326+
// logged and swallowed so the caller's edition flow proceeds (just without markers).
327+
const ensureAyahMarkers = async (
328+
active: ActiveDownload,
329+
version: MushafVersion,
330+
manifestVersion: QuranManifestVersion
331+
): Promise<void> => {
332+
try {
333+
const pack = await QuranManifestService.getAyahMarkerPack(manifestVersion);
334+
if (!pack || active.cancelled) return;
335+
const markersDir = getMarkersDir(version);
336+
const haveFrames = new File(markersDir, "marker-sepia.png").exists;
337+
if (haveFrames && readInstalled(version).markers === pack.version) return;
338+
339+
const outcome = await downloadAndExtractBundle(active, {
340+
url: pack.url,
341+
sizeBytes: 0,
342+
dir: markersDir,
343+
zipName: `quran-${version}-markers.zip`,
344+
alreadyOnDisk: false,
345+
emit: () => {},
346+
});
347+
if (outcome === BundleOutcome.EXTRACTED) {
348+
writeInstalled(version, { markers: pack.version });
349+
log.d("Download", `Installed ayah-marker pack ${pack.version} for ${version}`);
350+
}
351+
} catch (error) {
352+
log.e(
353+
"Download",
354+
`Ayah-marker pack failed for ${version} (markers absent)`,
355+
error instanceof Error ? error : undefined
356+
);
357+
}
358+
};
359+
360+
// Opportunistic, idempotent marker fetch for an already-installed edition (the
361+
// edition download covers fresh installs; this catches editions installed before
362+
// the ornament pack existed, or a pack version bump). Cheap: no-ops once the
363+
// current pack is on disk. Runs in its own lightweight context (not a tracked
364+
// download) so it never shows progress chrome.
365+
const ensureMarkersInstalled = async (version: MushafVersion): Promise<void> => {
366+
const manifestVersion = await QuranManifestService.getVersionInfo(version);
367+
if (!manifestVersion) return;
368+
await ensureAyahMarkers(
369+
{ controller: new AbortController(), cancelled: false, promise: Promise.resolve() },
370+
version,
371+
manifestVersion
372+
);
373+
};
374+
318375
const start = (version: MushafVersion): Promise<void> =>
319376
beginDownload(activeDownloads, version, doStart);
320377

@@ -431,6 +488,11 @@ const doStart = async (version: MushafVersion, active: ActiveDownload): Promise<
431488
}
432489
if (plan.needMeta) writeInstalled(version, { meta: manifestVersion.meta.version });
433490

491+
// 3) Ayah-marker ornament (tiny ~20KB): the edition's default medallion frames
492+
// the reader overlays on each ayah. Fetched when missing or its version changed.
493+
// Non-fatal — a failure leaves the edition usable (text/images), just no markers.
494+
await ensureAyahMarkers(active, version, manifestVersion);
495+
434496
// Complete only once BOTH legs have landed.
435497
clearResume(version);
436498
store.updateDownloadState(version, { status: DownloadStatus.COMPLETE });
@@ -656,6 +718,7 @@ const checkDiskSpace = (requiredMB: number): { available: boolean; availableMB:
656718

657719
export const QuranDownload = {
658720
start,
721+
ensureMarkersInstalled,
659722
startDark,
660723
deleteDark,
661724
pause,

src/services/quran-manifest.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ const getMetaUrl = async (version: QuranManifestVersion): Promise<string | null>
7474
return manifest ? assetUrl(manifest, version.meta.url) : null;
7575
};
7676

77+
// The edition's default ayah-marker ornament pack (medallion frames) + its version.
78+
// Null when the manifest ships no ayah-marker ornaments for this edition.
79+
const getAyahMarkerPack = async (
80+
version: QuranManifestVersion
81+
): Promise<{ url: string; version: string } | null> => {
82+
const manifest = await fetchManifest();
83+
const group = manifest?.ornaments?.ayahMarker;
84+
if (!manifest || !group) return null;
85+
const optionId = group.defaultByEdition?.[version.id] ?? group.default;
86+
const option =
87+
group.options.find((o) => o.id === optionId) ??
88+
group.options.find((o) => o.editions?.includes(version.id));
89+
return option ? { url: assetUrl(manifest, option.url), version: option.version } : null;
90+
};
91+
7792
const getImagesSizeBytes = (version: QuranManifestVersion, dark = false): number =>
7893
(dark ? version.images.dark?.bytes : version.images.light.bytes) ?? 0;
7994

@@ -118,6 +133,7 @@ export const QuranManifestService = {
118133
getContent,
119134
getImagesUrl,
120135
getMetaUrl,
136+
getAyahMarkerPack,
121137
getImagesSizeBytes,
122138
getMetaSizeBytes,
123139
getTotalSizeBytes,

src/types/quran.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,38 @@ export type QuranContent = {
145145
sha256: string;
146146
};
147147

148+
// A selectable ornament style (e.g. ayah-marker frames) as a downloadable pack.
149+
export type QuranOrnamentOption = QuranAsset & {
150+
id: string;
151+
version: string;
152+
resolution?: number;
153+
// Editions this option applies to (the marker artwork differs per edition).
154+
editions?: string[];
155+
preview?: string;
156+
};
157+
158+
export type QuranOrnamentGroup = {
159+
default: string;
160+
// Per-edition default option id, overriding `default`.
161+
defaultByEdition?: Record<string, string>;
162+
options: QuranOrnamentOption[];
163+
};
164+
165+
// Selectable style packs overlaid on the reader, keyed off bounds positions.
166+
export type QuranOrnaments = {
167+
ayahMarker?: QuranOrnamentGroup;
168+
surahFrame?: QuranOrnamentGroup;
169+
pageHolder?: QuranOrnamentGroup;
170+
};
171+
148172
export type QuranManifest = {
149173
manifestSchema: number;
150174
baseUrl: string;
151175
editions: QuranManifestVersion[];
152176
content: QuranContent;
153-
// `ornaments` exists in the manifest but is deferred (app uses bundled ornaments).
177+
// Style packs (ayah-marker frames etc.) downloaded per edition; the ayah-marker
178+
// default pack carries the medallion frames the reader overlays on each ayah.
179+
ornaments?: QuranOrnaments;
154180
};
155181

156182
export type QuranLibraryTab = "index" | "highlights" | "bookmarks" | "khatmah" | "guide";

0 commit comments

Comments
 (0)