@quran.ws/qvp-react-native 0.1.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +172 -0
- package/README.md +30 -28
- package/android/build.gradle +2 -2
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/Marshal.kt +33 -32
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/QvpModule.kt +55 -35
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/QvpPackage.kt +1 -1
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/QvpPageViewManager.kt +9 -6
- package/android/src/main/java/{net/quranpedia → ws/quran}/qvp/rn/QvpRnPageView.kt +54 -28
- package/package.json +1 -1
- package/src/index.tsx +147 -104
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
package
|
|
1
|
+
package ws.quran.qvp.rn
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.bridge.*
|
|
4
4
|
import com.facebook.react.module.annotations.ReactModule
|
|
5
5
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import ws.quran.qvp.*
|
|
7
|
+
import ws.quran.qvp.Target
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Promise-based queries on the page behind a `<QvpPageView />` (looked up by react tag) plus engine-wide
|
|
@@ -41,24 +41,26 @@ class QvpModule(private val ctx: ReactApplicationContext) : ReactContextBaseJava
|
|
|
41
41
|
// ── page facts ──
|
|
42
42
|
@ReactMethod fun info(tag: Int, promise: Promise) = withPage(tag, promise) { v, p -> Marshal.pageInfo(p) + mapOf("loadMs" to v.loadMs, "bytes" to v.pageBytes) }
|
|
43
43
|
@ReactMethod fun words(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.words.map { Marshal.word(p, it) } }
|
|
44
|
-
@ReactMethod fun word(tag: Int,
|
|
44
|
+
@ReactMethod fun word(tag: Int, index: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.words.getOrNull(index)?.let { Marshal.word(p, it) } }
|
|
45
45
|
@ReactMethod fun ayahs(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.ayahs.map { Marshal.ayah(it) } }
|
|
46
46
|
@ReactMethod fun lines(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.lines.map { Marshal.line(it) } }
|
|
47
|
-
@ReactMethod fun
|
|
47
|
+
@ReactMethod fun decorations(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.decorations.map { Marshal.decoration(it) } }
|
|
48
48
|
@ReactMethod fun findWord(tag: Int, s: Int, a: Int, w: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.findWord(s, a, w) }
|
|
49
|
-
@ReactMethod fun
|
|
50
|
-
@ReactMethod fun wordForm(tag: Int,
|
|
49
|
+
@ReactMethod fun targetWords(tag: Int, target: Dynamic, promise: Promise) = withPage(tag, promise) { _, p -> p.targetWords(tgt(target, p)).toList() }
|
|
50
|
+
@ReactMethod fun wordForm(tag: Int, index: Int, form: String?, promise: Promise) = withPage(tag, promise) { _, p -> p.wordForm(index, Marshal.form(form)) }
|
|
51
51
|
@ReactMethod fun hasForm(tag: Int, form: String?, promise: Promise) = withPage(tag, promise) { _, p -> p.hasForm(Marshal.form(form)) }
|
|
52
52
|
@ReactMethod fun attachWords(tag: Int, json: String, promise: Promise) = withPage(tag, promise) { _, p -> p.attachWords(json) }
|
|
53
53
|
|
|
54
54
|
// ── metadata ──
|
|
55
55
|
@ReactMethod fun surahs(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.surahs().map { Marshal.surah(it) } }
|
|
56
56
|
@ReactMethod fun divisions(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.divisions().map { Marshal.division(it) } }
|
|
57
|
+
@ReactMethod fun surahHeaders(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.surahHeaders().map { Marshal.surahHeader(it) } }
|
|
58
|
+
@ReactMethod fun surahHeadersView(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.surahHeadersView().map { Marshal.surahHeader(it) } }
|
|
57
59
|
@ReactMethod fun ayahMarks(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.ayahMarks().map { Marshal.ayahMark(it) } }
|
|
58
60
|
@ReactMethod fun rosettes(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.rosettes().map { Marshal.rosette(it) } }
|
|
59
61
|
@ReactMethod fun sajdahs(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.sajdahs().map { Marshal.sajdah(it) } }
|
|
60
62
|
@ReactMethod fun ayahKeys(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.ayahKeys().map { mapOf("surah" to it.first, "ayah" to it.second, "ayahKey" to "${it.first}:${it.second}") } }
|
|
61
|
-
@ReactMethod fun ayahWordCount(tag: Int, s: Int, a: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.ayahWordCount(s, a).let { mapOf("count" to it.first, "
|
|
63
|
+
@ReactMethod fun ayahWordCount(tag: Int, s: Int, a: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.ayahWordCount(s, a).let { mapOf("count" to it.first, "isComplete" to it.second) } }
|
|
62
64
|
@ReactMethod fun reciteMap(tag: Int, s: Int, a: Int, n: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.reciteMap(s, a, n)?.toList() }
|
|
63
65
|
@ReactMethod fun wordLabel(tag: Int, i: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.wordLabel(i) }
|
|
64
66
|
@ReactMethod fun ayahLabel(tag: Int, i: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.ayahLabel(i) }
|
|
@@ -68,21 +70,33 @@ class QvpModule(private val ctx: ReactApplicationContext) : ReactContextBaseJava
|
|
|
68
70
|
val o = opt(opts); p.text(tgt(target, p), Marshal.form(o["form"]), (o["wordSep"] as? String) ?: " ", (o["lineSep"] as? String) ?: "\n") }
|
|
69
71
|
@ReactMethod fun search(tag: Int, query: String, opts: ReadableMap?, promise: Promise) = withPage(tag, promise) { _, p ->
|
|
70
72
|
val o = opt(opts)
|
|
71
|
-
p.search(query, Marshal.form(o["form"], Form.SEARCH), Marshal.searchMode(o["mode"]), o["normalize"] != false, o["
|
|
73
|
+
p.search(query, Marshal.form(o["form"], Form.SEARCH), Marshal.searchMode(o["mode"]), o["normalize"] != false, o["looseMatch"] != false, (o["limit"] as? Number)?.toInt() ?: 0).map { Marshal.match(it) } }
|
|
72
74
|
@ReactMethod fun citation(tag: Int, words: ReadableArray, promise: Promise) = withPage(tag, promise) { _, p -> p.citation(ints(words)) }
|
|
73
|
-
@ReactMethod fun arabic(
|
|
75
|
+
@ReactMethod fun arabic(op: String, s: String, promise: Promise) = ui(promise) { when (op) { "strip" -> QvpEngine.strip(s); "fold" -> QvpEngine.fold(s); "normalize" -> QvpEngine.normalize(s); "loose", "looseKey" -> QvpEngine.looseKey(s); else -> s } }
|
|
74
76
|
|
|
75
77
|
// ── hit testing / layout (the view already does gestures; these are for scroll-into-view etc.) ──
|
|
76
|
-
|
|
78
|
+
private fun hitOptions(o: Map<String, Any?>) = QvpHitOptions((o["maxDistance"] as? Number)?.toFloat() ?: QvpDefaults.TAP_DISTANCE, (o["gapBias"] as? Number)?.toFloat() ?: QvpDefaults.GAP_BIAS, o["preferExact"] != false)
|
|
79
|
+
/** Exact outline hit at a point in page units. */
|
|
80
|
+
@ReactMethod fun hitTestExact(tag: Int, x: Double, y: Double, promise: Promise) = withPage(tag, promise) { _, p -> p.hitTestExact(x.toFloat(), y.toFloat())?.let { Marshal.hit(p, it) } }
|
|
81
|
+
/** Gap-aware hit at a point in page units. */
|
|
82
|
+
@ReactMethod fun hitTest(tag: Int, x: Double, y: Double, opts: ReadableMap?, promise: Promise) = withPage(tag, promise) { _, p -> p.hitTest(x.toFloat(), y.toFloat(), hitOptions(opt(opts)))?.let { Marshal.hit(p, it) } }
|
|
83
|
+
/** Exact outline hit at a point in view dp (density and pan/zoom removed here, the layout in the engine). */
|
|
84
|
+
@ReactMethod fun hitTestExactView(tag: Int, x: Double, y: Double, promise: Promise) = withPage(tag, promise) { v, p -> val d = v.resources.displayMetrics.density
|
|
85
|
+
p.hitTestExactView(((x * d).toFloat() - v.inner.viewOx) / v.inner.viewScale, ((y * d).toFloat() - v.inner.viewOy) / v.inner.viewScale)?.let { Marshal.hit(p, it) } }
|
|
86
|
+
@ReactMethod fun hitTestView(tag: Int, x: Double, y: Double, opts: ReadableMap?, promise: Promise) = withPage(tag, promise) { v, p ->
|
|
77
87
|
val o = opt(opts); val d = v.resources.displayMetrics.density
|
|
78
|
-
p.
|
|
79
|
-
@ReactMethod fun
|
|
80
|
-
p.
|
|
88
|
+
p.hitTestView(((x * d).toFloat() - v.inner.viewOx) / v.inner.viewScale, ((y * d).toFloat() - v.inner.viewOy) / v.inner.viewScale, hitOptions(o))?.let { Marshal.hit(p, it) } }
|
|
89
|
+
@ReactMethod fun wordBoundsView(tag: Int, i: Int, promise: Promise) = withPage(tag, promise) { v, p -> val d = v.resources.displayMetrics.density
|
|
90
|
+
p.wordBoundsView(i)?.let { b -> mapOf("x0" to (v.inner.viewOx + b[0] * v.inner.viewScale) / d, "y0" to (v.inner.viewOy + b[1] * v.inner.viewScale) / d, "x1" to (v.inner.viewOx + b[2] * v.inner.viewScale) / d, "y1" to (v.inner.viewOy + b[3] * v.inner.viewScale) / d) } }
|
|
81
91
|
@ReactMethod fun currentLayout(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.currentLayout?.let { Marshal.layout(it) } }
|
|
82
92
|
@ReactMethod fun relayout(tag: Int, promise: Promise) = withPage(tag, promise) { v, _ -> v.inner.relayout(); v.resetView(); null }
|
|
83
93
|
@ReactMethod fun resetView(tag: Int, promise: Promise) = withPage(tag, promise) { v, _ -> v.resetView(); null }
|
|
84
|
-
|
|
85
|
-
@ReactMethod fun
|
|
94
|
+
/** The leading that fills this view's viewport, from the spec the view lays out with. */
|
|
95
|
+
@ReactMethod fun layoutLineSpacingToFill(tag: Int, max: Double, promise: Promise) = withPage(tag, promise) { v, p -> p.layoutLineSpacingToFill(v.inner.layoutSpec(), max.toFloat()) }
|
|
96
|
+
@ReactMethod fun layoutWastedFraction(tag: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.layoutWastedFraction(v.inner.layoutSpec()) }
|
|
97
|
+
/** The page's height at the printed pitch for the spec this view lays out with. */
|
|
98
|
+
@ReactMethod fun layoutPrintedHeight(tag: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.layoutPrintedHeight(v.inner.layoutSpec()) }
|
|
99
|
+
@ReactMethod fun grid(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> mapOf("lines" to p.grid.lines, "lineSpacing" to p.grid.lineSpacing) }
|
|
86
100
|
@ReactMethod fun stats(tag: Int, promise: Promise) = ui(promise) { view(tag)?.stats() }
|
|
87
101
|
@ReactMethod fun invalidate(tag: Int, promise: Promise) = ui(promise) { view(tag)?.inner?.invalidate(); null }
|
|
88
102
|
|
|
@@ -90,26 +104,26 @@ class QvpModule(private val ctx: ReactApplicationContext) : ReactContextBaseJava
|
|
|
90
104
|
@ReactMethod fun select(tag: Int, anchor: Int, focus: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.select(anchor, focus); v.inner.invalidate(); Marshal.selection(p) }
|
|
91
105
|
@ReactMethod fun clearSelection(tag: Int, promise: Promise) = withPage(tag, promise) { v, _ -> v.inner.clearSelection(); null }
|
|
92
106
|
@ReactMethod fun selection(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> Marshal.selection(p) }
|
|
93
|
-
@ReactMethod fun selectionText(tag: Int, form: String?,
|
|
107
|
+
@ReactMethod fun selectionText(tag: Int, form: String?, includeCitation: Boolean, promise: Promise) = withPage(tag, promise) { _, p -> p.selectionText(Marshal.form(form), includeCitation) }
|
|
94
108
|
|
|
95
109
|
// ── memorisation (stepwise ops; `mask` / `reveal` props hold the declarative part) ──
|
|
96
|
-
@ReactMethod fun
|
|
97
|
-
@ReactMethod fun
|
|
98
|
-
@ReactMethod fun
|
|
99
|
-
@ReactMethod fun
|
|
100
|
-
@ReactMethod fun
|
|
101
|
-
@ReactMethod fun
|
|
110
|
+
@ReactMethod fun unmaskNext(tag: Int, n: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.unmaskNext(n).also { v.inner.invalidate() } }
|
|
111
|
+
@ReactMethod fun maskBack(tag: Int, n: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.maskBack(n).also { v.inner.invalidate() } }
|
|
112
|
+
@ReactMethod fun unmaskWord(tag: Int, i: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.unmaskWord(i).also { v.inner.invalidate() } }
|
|
113
|
+
@ReactMethod fun maskWord(tag: Int, i: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.maskWord(i).also { v.inner.invalidate() } }
|
|
114
|
+
@ReactMethod fun unmaskAll(tag: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.unmaskAll(); v.inner.invalidate(); null }
|
|
115
|
+
@ReactMethod fun maskAll(tag: Int, promise: Promise) = withPage(tag, promise) { v, p -> p.maskAll(); v.inner.invalidate(); null }
|
|
102
116
|
@ReactMethod fun maskHidden(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.maskHidden().toList() }
|
|
103
117
|
@ReactMethod fun maskWords(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.maskWords().toList() }
|
|
104
|
-
@ReactMethod fun
|
|
105
|
-
@ReactMethod fun
|
|
118
|
+
@ReactMethod fun revealStepCount(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.revealStepCount() }
|
|
119
|
+
@ReactMethod fun revealPosition(tag: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.revealPosition()?.toDouble() }
|
|
106
120
|
@ReactMethod fun revealStepOf(tag: Int, i: Int, promise: Promise) = withPage(tag, promise) { _, p -> p.revealStepOf(i).toDouble() }
|
|
107
121
|
|
|
108
122
|
// ── crop ──
|
|
109
|
-
@ReactMethod fun
|
|
110
|
-
val o = opt(opts); p.
|
|
123
|
+
@ReactMethod fun cropBounds(tag: Int, target: Dynamic, opts: ReadableMap?, promise: Promise) = withPage(tag, promise) { _, p ->
|
|
124
|
+
val o = opt(opts); p.cropBounds(tgt(target, p), (o["pad"] as? Number)?.toFloat() ?: QvpDefaults.CROP_PAD, o["keepAyahMarks"] != false)?.let { Marshal.cropBounds(it) } }
|
|
111
125
|
@ReactMethod fun cropSvg(tag: Int, target: Dynamic, opts: ReadableMap?, promise: Promise) = withPage(tag, promise) { _, p ->
|
|
112
|
-
val o = opt(opts); p.cropSvg(tgt(target, p), (o["pad"] as? Number)?.toFloat() ?:
|
|
126
|
+
val o = opt(opts); p.cropSvg(tgt(target, p), (o["pad"] as? Number)?.toFloat() ?: QvpDefaults.CROP_PAD, o["keepAyahMarks"] != false, Marshal.color(o["background"], 0)) }
|
|
113
127
|
|
|
114
128
|
// ── atlas ──
|
|
115
129
|
@ReactMethod fun loadAtlas(uri: String, promise: Promise) = ui(promise) {
|
|
@@ -128,21 +142,27 @@ class QvpModule(private val ctx: ReactApplicationContext) : ReactContextBaseJava
|
|
|
128
142
|
@ReactMethod fun freeAtlas(id: Int, promise: Promise) = ui(promise) { atlases.remove(id)?.close(); atlasByUri.entries.removeAll { it.value == id }; null }
|
|
129
143
|
@ReactMethod fun atlasPageOf(id: Int, s: Int, a: Int, promise: Promise) = withAtlas(id, promise) { it.pageOf(s, a) }
|
|
130
144
|
@ReactMethod fun atlasPageRange(id: Int, page: Int, promise: Promise) = withAtlas(id, promise) { it.pageRange(page)?.let { r -> mapOf("first" to mapOf("surah" to r.first.first, "ayah" to r.first.second), "last" to mapOf("surah" to r.second.first, "ayah" to r.second.second)) } }
|
|
131
|
-
@ReactMethod fun
|
|
145
|
+
@ReactMethod fun atlasPageCount(id: Int, promise: Promise) = withAtlas(id, promise) { it.pageCount() }
|
|
132
146
|
@ReactMethod fun atlasSurah(id: Int, n: Int, promise: Promise) = withAtlas(id, promise) { it.surah(n)?.let { s -> Marshal.atlasSurah(s) } }
|
|
133
147
|
@ReactMethod fun atlasSurahs(id: Int, promise: Promise) = withAtlas(id, promise) { it.surahs().map { s -> Marshal.atlasSurah(s) } }
|
|
134
148
|
@ReactMethod fun atlasPageOfSurah(id: Int, n: Int, promise: Promise) = withAtlas(id, promise) { it.pageOfSurah(n) }
|
|
135
|
-
@ReactMethod fun atlasDivision(id: Int,
|
|
136
|
-
@ReactMethod fun
|
|
137
|
-
@ReactMethod fun
|
|
149
|
+
@ReactMethod fun atlasDivision(id: Int, division: String, n: Int, promise: Promise) = withAtlas(id, promise) { it.division(Marshal.division(division), n)?.let { r -> Marshal.atlasRubuAlHizb(r) } }
|
|
150
|
+
@ReactMethod fun atlasDivisionOf(id: Int, division: String, s: Int, a: Int, promise: Promise) = withAtlas(id, promise) { it.divisionOf(Marshal.division(division), s, a) }
|
|
151
|
+
@ReactMethod fun atlasJuzOf(id: Int, s: Int, a: Int, promise: Promise) = withAtlas(id, promise) { it.juzOf(s, a) }
|
|
138
152
|
@ReactMethod fun atlasPagesOfJuz(id: Int, n: Int, promise: Promise) = withAtlas(id, promise) { it.pagesOfJuz(n)?.let { r -> listOf(r.first, r.second) } }
|
|
139
|
-
@ReactMethod fun
|
|
153
|
+
@ReactMethod fun atlasSearchSurahs(id: Int, text: String, promise: Promise) = withAtlas(id, promise) { it.searchSurahs(text).map { s -> Marshal.atlasSurah(s) } }
|
|
140
154
|
|
|
141
155
|
// ── names ──
|
|
142
156
|
@ReactMethod fun markName(m: Int, promise: Promise) = ui(promise) { QvpEngine.markName(m) }
|
|
143
157
|
@ReactMethod fun markFromName(s: String, promise: Promise) = ui(promise) { QvpEngine.markFromName(s) }
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
@ReactMethod fun markCategory(m: Int, promise: Promise) = ui(promise) { QvpEngine.markCategory(m) }
|
|
159
|
+
@ReactMethod fun engineName(promise: Promise) = ui(promise) { QvpEngine.engineName() }
|
|
160
|
+
/** Every name table, read from the engine; the JavaScript side resolves names with these. */
|
|
161
|
+
override fun getConstants(): Map<String, Any> = mapOf("version" to QvpEngine.version(), "formatVersion" to QvpEngine.formatVersion(),
|
|
162
|
+
"marks" to QvpEngine.names(QvpEngine.Names.MARK), "kinds" to QvpEngine.names(QvpEngine.Names.KIND),
|
|
163
|
+
"families" to QvpEngine.names(QvpEngine.Names.FAMILY), "categories" to QvpEngine.names(QvpEngine.Names.CATEGORY),
|
|
164
|
+
"decorations" to QvpEngine.names(QvpEngine.Names.DECORATION), "divisions" to QvpEngine.names(QvpEngine.Names.DIVISION),
|
|
165
|
+
"places" to QvpEngine.names(QvpEngine.Names.PLACE))
|
|
146
166
|
|
|
147
167
|
override fun invalidate() { atlases.values.forEach { it.close() }; atlases.clear(); atlasByUri.clear(); super.invalidate() }
|
|
148
168
|
companion object { const val NAME = "QvpModule" }
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
package
|
|
1
|
+
package ws.quran.qvp.rn
|
|
2
2
|
|
|
3
3
|
import com.facebook.react.bridge.ReadableArray
|
|
4
4
|
import com.facebook.react.bridge.ReadableMap
|
|
5
5
|
import com.facebook.react.uimanager.SimpleViewManager
|
|
6
6
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
7
7
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
8
|
-
import
|
|
8
|
+
import ws.quran.qvp.QvpColor
|
|
9
|
+
import ws.quran.qvp.QvpDefaults
|
|
9
10
|
|
|
10
11
|
/** `<QvpPageView />`: props are marshalled onto [QvpRnPageView]; [onAfterUpdateTransaction] commits them in one pass. */
|
|
11
12
|
class QvpPageViewManager : SimpleViewManager<QvpRnPageView>() {
|
|
@@ -27,14 +28,16 @@ class QvpPageViewManager : SimpleViewManager<QvpRnPageView>() {
|
|
|
27
28
|
@ReactProp(name = "padBottom", defaultFloat = 0f) fun setPadBottom(v: QvpRnPageView, f: Float) = v.setPadBottomDp(f)
|
|
28
29
|
@ReactProp(name = "padSide", defaultFloat = 0f) fun setPadSide(v: QvpRnPageView, f: Float) = v.setPadSideDp(f)
|
|
29
30
|
@ReactProp(name = "lineSpacing", defaultFloat = 1f) fun setLineSpacing(v: QvpRnPageView, f: Float) = v.setLineSpacingProp(f)
|
|
30
|
-
@ReactProp(name = "lineGap", defaultFloat = 0f) fun setLineGap(v: QvpRnPageView, f: Float) = v.setLineGapProp(f)
|
|
31
31
|
@ReactProp(name = "fillHeight", defaultBoolean = false) fun setFillHeight(v: QvpRnPageView, b: Boolean) = v.setFillHeightProp(b)
|
|
32
|
+
@ReactProp(name = "surahFrames", defaultBoolean = true) fun setSurahFrames(v: QvpRnPageView, b: Boolean) = v.setSurahFramesProp(b)
|
|
32
33
|
@ReactProp(name = "paperColor") fun setPaperColor(v: QvpRnPageView, s: String?) { v.paperColor = if (s.isNullOrBlank()) 0 else QvpColor.argb(QvpColor.parse(s)); v.invalidate() }
|
|
33
|
-
@ReactProp(name = "defaultInk") fun
|
|
34
|
+
@ReactProp(name = "defaultInk") fun setDefaultColor(v: QvpRnPageView, s: String?) { v.defaultInkProp = s }
|
|
34
35
|
@ReactProp(name = "selectionBand") fun setSelectionBand(v: QvpRnPageView, s: String?) { if (!s.isNullOrBlank()) v.selectionBand = QvpColor.parse(s) }
|
|
35
36
|
@ReactProp(name = "selectionEnabled", defaultBoolean = true) fun setSelectionEnabled(v: QvpRnPageView, b: Boolean) { v.selectionEnabled = b }
|
|
36
37
|
@ReactProp(name = "zoomEnabled", defaultBoolean = true) fun setZoomEnabled(v: QvpRnPageView, b: Boolean) { v.zoomEnabled = b }
|
|
37
|
-
@ReactProp(name = "
|
|
38
|
+
@ReactProp(name = "zoomMode") fun setZoomMode(v: QvpRnPageView, s: String?) { v.zoomModeProp = s }
|
|
39
|
+
@ReactProp(name = "zoomStep", defaultInt = -1) fun setZoomStep(v: QvpRnPageView, i: Int) { v.zoomStepProp = i }
|
|
40
|
+
@ReactProp(name = "hitMaxDistance", defaultFloat = QvpDefaults.TAP_DISTANCE) fun setHitMaxDistance(v: QvpRnPageView, f: Float) { v.hitOptions = v.hitOptions.copy(maxDistance = f) }
|
|
38
41
|
@ReactProp(name = "theme") fun setTheme(v: QvpRnPageView, m: ReadableMap?) { v.themeProp = Marshal.plain(m) }
|
|
39
42
|
@ReactProp(name = "styles") fun setStyles(v: QvpRnPageView, a: ReadableArray?) { v.stylesProp = Marshal.plain(a) }
|
|
40
43
|
@ReactProp(name = "highlights") fun setHighlights(v: QvpRnPageView, a: ReadableArray?) { v.highlightsProp = Marshal.plain(a) }
|
|
@@ -43,6 +46,6 @@ class QvpPageViewManager : SimpleViewManager<QvpRnPageView>() {
|
|
|
43
46
|
|
|
44
47
|
companion object {
|
|
45
48
|
const val NAME = "QvpPageView"
|
|
46
|
-
val EVENTS = listOf("onWordTap", "
|
|
49
|
+
val EVENTS = listOf("onWordTap", "onDecorationTap", "onEmptyTap", "onSelectionChanged", "onPageLoad", "onRevealChanged", "onSwipe", "onZoomChanged", "onError")
|
|
47
50
|
}
|
|
48
51
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package ws.quran.qvp.rn
|
|
2
2
|
|
|
3
3
|
import android.util.Base64
|
|
4
4
|
import android.util.Log
|
|
@@ -8,8 +8,8 @@ import com.facebook.react.uimanager.ThemedReactContext
|
|
|
8
8
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
9
9
|
import com.facebook.react.uimanager.events.Event
|
|
10
10
|
import android.widget.FrameLayout
|
|
11
|
-
import
|
|
12
|
-
import
|
|
11
|
+
import ws.quran.qvp.*
|
|
12
|
+
import ws.quran.qvp.Target
|
|
13
13
|
import java.io.File
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -36,14 +36,11 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
36
36
|
val w = r - l; val h = b - t
|
|
37
37
|
inner.measure(MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY))
|
|
38
38
|
inner.layout(0, 0, w, h)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Fit + centre: the library's resetView fits by height and pins the pan at x = 0; centre the pan horizontally (pan is host state). */
|
|
42
|
-
fun resetView() { inner.resetView(); centre() }
|
|
43
|
-
private fun centre() {
|
|
44
|
-
val l = page?.currentLayout ?: return
|
|
45
|
-
inner.viewOx = ((inner.width - l.contentW * inner.viewScale) / 2f).coerceAtLeast(0f); inner.invalidate()
|
|
39
|
+
// A size change reaches the child's onSizeChanged, which lays the page out again and
|
|
40
|
+
// fits it; a pass that changed nothing leaves the reader's view where it is.
|
|
46
41
|
}
|
|
42
|
+
/** Fit and centre, as the engine's layout says. */
|
|
43
|
+
fun resetView() { inner.resetView() }
|
|
47
44
|
|
|
48
45
|
// ── desired props (set by the manager) ──
|
|
49
46
|
var pageUri: String? = null
|
|
@@ -68,14 +65,18 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
68
65
|
private var appliedReveal: Map<String, Any?>? = null // without "at"
|
|
69
66
|
private var appliedRevealAt: Long? = null
|
|
70
67
|
private var appliedInk: Int? = null
|
|
71
|
-
var
|
|
68
|
+
var revealStepCount = 0; private set
|
|
72
69
|
var loadMs = 0.0; private set
|
|
73
70
|
var pageBytes = 0; private set
|
|
74
71
|
|
|
75
72
|
init {
|
|
76
73
|
inner.onWordTap = { w, h -> page?.let { p -> emit("onWordTap", mapOf("word" to Marshal.word(p, w), "hit" to Marshal.hit(p, h))) } }
|
|
77
|
-
inner.
|
|
74
|
+
inner.onDecorationTap = { d, h -> page?.let { p -> emit("onDecorationTap", mapOf("decoration" to Marshal.decoration(d), "hit" to Marshal.hit(p, h))) } }
|
|
78
75
|
inner.onEmptyTap = { emit("onEmptyTap", emptyMap()) }
|
|
76
|
+
// The engine says a flick turns the page and by how many, in reading order. Which page
|
|
77
|
+
// that is belongs to the app, so this is an event and not a page change.
|
|
78
|
+
inner.onSwipe = { pages -> emit("onSwipe", mapOf("pages" to pages)) }
|
|
79
|
+
inner.onZoomChanged = { z -> emit("onZoomChanged", mapOf("mode" to z.mode.name.lowercase(), "step" to z.step, "zoom" to z.zoom)) }
|
|
79
80
|
inner.onSelectionChanged = { page?.let { p -> emit("onSelectionChanged", Marshal.selection(p)) } }
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -89,8 +90,31 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
89
90
|
fun setPadBottomDp(v: Float) { inner.padBottom = v * density; layoutDirty = true }
|
|
90
91
|
fun setPadSideDp(v: Float) { inner.padSide = v * density; layoutDirty = true }
|
|
91
92
|
fun setLineSpacingProp(v: Float) { inner.lineSpacing = v; layoutDirty = true }
|
|
92
|
-
|
|
93
|
+
|
|
94
|
+
// ── the reader's zoom control ──
|
|
95
|
+
// What a pinch does to the page: reflow onto the page's own steps (the default), reflow to
|
|
96
|
+
// the zoom the fingers ask for, or magnify the printed page. The engine owns the policy;
|
|
97
|
+
// these props only say which one the app wants and which step to sit on.
|
|
98
|
+
var zoomModeProp: String? = null
|
|
99
|
+
var zoomStepProp: Int = -1
|
|
100
|
+
/** The zooms this page's steps land on, for an app drawing its own size control. */
|
|
101
|
+
val zoomSteps: FloatArray get() = page?.zoomSteps(inner.layoutSpec()) ?: FloatArray(0)
|
|
102
|
+
|
|
103
|
+
private fun commitZoom() {
|
|
104
|
+
val p = page ?: return
|
|
105
|
+
if (!p.isOpen) return
|
|
106
|
+
zoomModeProp?.let { name ->
|
|
107
|
+
val mode = when (name) {
|
|
108
|
+
"continuous" -> QvpZoomMode.CONTINUOUS
|
|
109
|
+
"magnify" -> QvpZoomMode.MAGNIFY
|
|
110
|
+
else -> QvpZoomMode.STEPPED
|
|
111
|
+
}
|
|
112
|
+
if (mode != inner.zoomMode) inner.zoomMode = mode
|
|
113
|
+
}
|
|
114
|
+
if (zoomStepProp >= 0 && zoomStepProp != inner.zoom.step) inner.zoomToStep(zoomStepProp)
|
|
115
|
+
}
|
|
93
116
|
fun setFillHeightProp(v: Boolean) { inner.fillHeight = v; layoutDirty = true }
|
|
117
|
+
fun setSurahFramesProp(v: Boolean) { inner.surahFrames = v; layoutDirty = true }
|
|
94
118
|
|
|
95
119
|
// ── bytes ──
|
|
96
120
|
private fun readUri(uri: String): ByteArray {
|
|
@@ -114,6 +138,7 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
114
138
|
applyInk(p); applyTheme(p); applyStyles(p); applyHighlights(p); applyMask(p); applyReveal(p)
|
|
115
139
|
}
|
|
116
140
|
if (layoutDirty) { layoutDirty = false; inner.relayout(); resetView() }
|
|
141
|
+
commitZoom()
|
|
117
142
|
inner.invalidate()
|
|
118
143
|
} catch (e: Exception) {
|
|
119
144
|
Log.e("QvpRn", "commit failed", e)
|
|
@@ -125,7 +150,7 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
125
150
|
val key = pageBase64?.let { "b64:" + it.hashCode() } ?: pageUri
|
|
126
151
|
if (key == loadedKey) return
|
|
127
152
|
// drop everything bound to the old page: handles die with it
|
|
128
|
-
themeHandle = 0; appliedTheme = null; styleHandles.clear(); highlightHandles.clear(); appliedMask = null; appliedReveal = null; appliedRevealAt = null; appliedInk = null;
|
|
153
|
+
themeHandle = 0; appliedTheme = null; styleHandles.clear(); highlightHandles.clear(); appliedMask = null; appliedReveal = null; appliedRevealAt = null; appliedInk = null; revealStepCount = 0
|
|
129
154
|
page?.close(); page = null; loadedKey = key
|
|
130
155
|
if (key == null) return
|
|
131
156
|
val bytes = pageBase64?.let { Base64.decode(it, Base64.DEFAULT) } ?: readUri(pageUri!!)
|
|
@@ -138,12 +163,12 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
138
163
|
}
|
|
139
164
|
|
|
140
165
|
private fun applyInk(p: QvpPage) {
|
|
141
|
-
val ink = Marshal.color(defaultInkProp,
|
|
142
|
-
if (appliedInk != ink) { appliedInk = ink; p.
|
|
166
|
+
val ink = Marshal.color(defaultInkProp, QvpDefaults.INK)
|
|
167
|
+
if (appliedInk != ink) { appliedInk = ink; p.setDefaultColor(ink) }
|
|
143
168
|
}
|
|
144
169
|
private fun applyTheme(p: QvpPage) {
|
|
145
170
|
if (themeProp == appliedTheme) return
|
|
146
|
-
if (themeHandle != 0) { p.
|
|
171
|
+
if (themeHandle != 0) { p.removeStyle(themeHandle); themeHandle = 0 }
|
|
147
172
|
appliedTheme = themeProp
|
|
148
173
|
themeProp?.let { themeHandle = p.theme(Marshal.theme(it)) }
|
|
149
174
|
}
|
|
@@ -154,13 +179,13 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
154
179
|
}
|
|
155
180
|
private fun applyStyles(p: QvpPage) {
|
|
156
181
|
val want = entries(stylesProp)
|
|
157
|
-
for (id in styleHandles.keys.toList()) if (!want.containsKey(id)) { p.
|
|
182
|
+
for (id in styleHandles.keys.toList()) if (!want.containsKey(id)) { p.removeStyle(styleHandles.remove(id)!!.handle) }
|
|
158
183
|
for ((id, e) in want) {
|
|
159
184
|
val old = styleHandles[id]
|
|
160
185
|
if (old != null && old.entry == e) continue
|
|
161
186
|
val sameShape = old != null && old.entry["selector"] == e["selector"] && old.entry["target"] == e["target"] && old.entry["layer"] == e["layer"] && old.entry["hide"] == e["hide"] && e["hide"] != true
|
|
162
|
-
if (sameShape) { p.
|
|
163
|
-
if (old != null) p.
|
|
187
|
+
if (sameShape) { p.recolorStyle(old!!.handle, Marshal.color(e["color"], 0), (e["ms"] as? Number)?.toInt() ?: 0); styleHandles[id] = Rule(old.handle, e); continue }
|
|
188
|
+
if (old != null) p.removeStyle(old.handle)
|
|
164
189
|
val ms = (e["ms"] as? Number)?.toInt() ?: 0
|
|
165
190
|
val layer = (e["layer"] as? Number)?.toInt() ?: QvpLayer.BASE
|
|
166
191
|
val color = Marshal.color(e["color"], 0)
|
|
@@ -175,14 +200,14 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
175
200
|
}
|
|
176
201
|
private fun applyHighlights(p: QvpPage) {
|
|
177
202
|
val want = entries(highlightsProp)
|
|
178
|
-
for (id in highlightHandles.keys.toList()) if (!want.containsKey(id)) { p.
|
|
203
|
+
for (id in highlightHandles.keys.toList()) if (!want.containsKey(id)) { p.removeHighlight(highlightHandles.remove(id)!!.handle) }
|
|
179
204
|
for ((id, e) in want) {
|
|
180
205
|
val old = highlightHandles[id]
|
|
181
206
|
if (old != null && old.entry == e) continue
|
|
182
207
|
val style = Marshal.highlightStyle(e["style"])
|
|
183
208
|
if (old == null) { val h = p.highlight(Marshal.target(e["target"], p), style); if (h != 0) highlightHandles[id] = Rule(h, e); continue }
|
|
184
209
|
if (old.entry["style"] != e["style"]) p.restyleHighlight(old.handle, style)
|
|
185
|
-
if (old.entry["target"] != e["target"]) p.
|
|
210
|
+
if (old.entry["target"] != e["target"]) p.moveHighlight(old.handle, Marshal.target(e["target"], p))
|
|
186
211
|
highlightHandles[id] = Rule(old.handle, e)
|
|
187
212
|
}
|
|
188
213
|
}
|
|
@@ -191,19 +216,20 @@ class QvpRnPageView(private val ctx: ThemedReactContext) : FrameLayout(ctx) {
|
|
|
191
216
|
appliedMask = maskProp
|
|
192
217
|
p.unmask()
|
|
193
218
|
val m = maskProp ?: return
|
|
194
|
-
val paper = Marshal.color(m["blockColor"],
|
|
195
|
-
p.maskOptions(paper, (m["padX"] as? Number)?.toFloat() ?:
|
|
196
|
-
|
|
219
|
+
val paper = Marshal.color(m["blockColor"], QvpDefaults.MASK_BLOCK)
|
|
220
|
+
p.maskOptions(paper, (m["padX"] as? Number)?.toFloat() ?: QvpDefaults.MASK_PAD, (m["padY"] as? Number)?.toFloat() ?: QvpDefaults.MASK_PAD, (m["radius"] as? Number)?.toFloat() ?: QvpDefaults.MASK_RADIUS, m["reverse"] == true)
|
|
221
|
+
val from = m["from"] as? Number
|
|
222
|
+
if (from != null) p.maskFrom(from.toInt(), Marshal.maskMode(m["mode"])) else p.mask(Marshal.target(m["target"], p), Marshal.maskMode(m["mode"]))
|
|
197
223
|
}
|
|
198
224
|
private fun applyReveal(p: QvpPage) {
|
|
199
225
|
val r = revealProp
|
|
200
226
|
val cfg = r?.filterKeys { it != "at" }
|
|
201
227
|
if (cfg != appliedReveal) {
|
|
202
228
|
appliedReveal = cfg; appliedRevealAt = null
|
|
203
|
-
if (cfg == null) { p.revealStop();
|
|
204
|
-
|
|
229
|
+
if (cfg == null) { p.revealStop(); revealStepCount = 0; emit("onRevealChanged", mapOf("steps" to 0, "at" to null)); return }
|
|
230
|
+
revealStepCount = p.revealStart((cfg["lit"] as? Number)?.toInt() ?: QvpDefaults.REVEAL_LIT, cfg["byAyah"] == true, Marshal.color(cfg["grey"], QvpDefaults.REVEAL_GREY), Marshal.color(cfg["ink"], appliedInk ?: QvpDefaults.INK),
|
|
205
231
|
cfg["ayahMarks"] != false, (cfg["ms"] as? Number)?.toInt() ?: 0)
|
|
206
|
-
emit("onRevealChanged", mapOf("steps" to
|
|
232
|
+
emit("onRevealChanged", mapOf("steps" to revealStepCount, "at" to -1))
|
|
207
233
|
}
|
|
208
234
|
if (r == null) return
|
|
209
235
|
val at = (r["at"] as? Number)?.toLong() ?: -1L
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quran.ws/qvp-react-native",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "React Native wrapper for the QVP vector mushaf engine (Android; iOS TODO). Thin marshalling over the Kotlin library, which is JNI over qvp.h.",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"types": "src/index.tsx",
|