@octane-xplat/gif 0.10.0 → 0.11.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/README.md +43 -0
- package/dist/native/AnimatedImage.tsrx.js +39 -0
- package/dist/native/escape-props.js +12 -0
- package/dist/native/index.js +3 -36
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/prefetch-batch.js +27 -0
- package/dist/native/prefetch.js +26 -0
- package/dist/web/AnimatedImage.web.tsrx.js +49 -0
- package/dist/web/escape-props.web.js +18 -0
- package/dist/web/index.js +3 -38
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/prefetch-batch.js +27 -0
- package/dist/web/prefetch.web.js +17 -0
- package/package.json +1 -1
- package/src/AnimatedImage.tsrx +2 -0
- package/src/AnimatedImage.web.tsrx +2 -0
- package/src/escape-props.ts +15 -0
- package/src/escape-props.web.ts +26 -0
- package/src/index.macos.ts +2 -1
- package/src/index.ts +2 -1
- package/src/index.web.ts +2 -1
- package/src/prefetch-batch.ts +50 -0
- package/src/prefetch.macos.ts +8 -0
- package/src/prefetch.ts +38 -0
- package/src/prefetch.web.test.ts +104 -0
- package/src/prefetch.web.ts +21 -0
- package/src/props.ts +22 -0
- package/types/generated/.tsrx-typegen-manifest.json +5 -0
- package/types/generated/escape-props.d.ts +7 -0
- package/types/generated/escape-props.web.d.ts +6 -0
- package/types/generated/index.d.ts +2 -1
- package/types/generated/index.web.d.ts +2 -1
- package/types/generated/prefetch-batch.d.ts +10 -0
- package/types/generated/prefetch.d.ts +12 -0
- package/types/generated/prefetch.web.d.ts +7 -0
- package/types/generated/props.d.ts +21 -0
- package/types/index.macos.d.ts +3 -2
package/README.md
CHANGED
|
@@ -24,6 +24,49 @@ import { AnimatedImage } from '@octane-xplat/gif'
|
|
|
24
24
|
and the plugin accepts the same values natively. For Lottie JSON
|
|
25
25
|
animations use [`@octane-xplat/lottie`](../lottie/README.md) instead.
|
|
26
26
|
|
|
27
|
+
## Prefetch
|
|
28
|
+
|
|
29
|
+
`prefetch` warms the image engine's disk cache for URLs you expect to show
|
|
30
|
+
soon — bytes on disk now, decode still at display time:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { prefetch } from '@octane-xplat/gif'
|
|
34
|
+
|
|
35
|
+
await prefetch(feed.map((post) => post.imageUrl)) // false if any URL fails
|
|
36
|
+
|
|
37
|
+
await prefetch(urls, {
|
|
38
|
+
headers: { Authorization: 'Bearer …' }, // Android only — see below
|
|
39
|
+
concurrency: 5, // max parallel fetches (default)
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
On Android that is Fresco's `prefetchToDiskCache`; on iOS, SDWebImage's
|
|
44
|
+
prefetcher storing to disk. On web it loads each URL through a throwaway
|
|
45
|
+
`<img>` so the browser HTTP cache is warm. It resolves `false` when any URL
|
|
46
|
+
fails — and always `false` on macOS, where the AppKit host has no image
|
|
47
|
+
pipeline yet. Same contract as `@octane-xplat/image`'s `prefetch`.
|
|
48
|
+
|
|
49
|
+
Options: `headers` is forwarded to the engine — **honored on Android,
|
|
50
|
+
dropped on iOS** (the plugin's prefetch path lacks the request-modifier
|
|
51
|
+
branch its display path has; an upstream patch is needed) — and ignored on
|
|
52
|
+
web. `concurrency` caps parallel fetches JS-side (default 5).
|
|
53
|
+
|
|
54
|
+
## Platform props
|
|
55
|
+
|
|
56
|
+
`ios`, `android`, and `web` escape bags apply after the shared props, the
|
|
57
|
+
same convention as `@octane-xplat/ui`. Native bags assign onto the ui-image
|
|
58
|
+
`Img` view, so engine props such as `noCache`, `decodeWidth`,
|
|
59
|
+
`decodeHeight`, `placeholderImageUri`, and `progressiveRenderingEnabled`
|
|
60
|
+
are reachable where the engine supports them:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<AnimatedImage
|
|
64
|
+
src={photo.url}
|
|
65
|
+
ios={{ noCache: true }}
|
|
66
|
+
android={{ decodeWidth: 288, decodeHeight: 288 }}
|
|
67
|
+
/>
|
|
68
|
+
```
|
|
69
|
+
|
|
27
70
|
Per-target limits are recorded in
|
|
28
71
|
[known limits](../../docs/verify/known-limits.md). Exercised by
|
|
29
72
|
[`AnimatedImageDemo`](../demos/src/AnimatedImageDemo.tsrx).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { applyEscapeProps } from "./escape-props.js";
|
|
2
|
+
import { defineUniversalComponent, registerElement, universalPlan, universalValue } from "@nativescript-community/octane";
|
|
3
|
+
import { Img } from "@nativescript-community/ui-image";
|
|
4
|
+
//#region src/AnimatedImage.tsrx
|
|
5
|
+
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
6
|
+
"kind": "host",
|
|
7
|
+
"type": "animatedimage",
|
|
8
|
+
"props": {
|
|
9
|
+
"animatedImageView": true,
|
|
10
|
+
"autoPlayAnimations": true
|
|
11
|
+
},
|
|
12
|
+
"bindings": [
|
|
13
|
+
["ref", 0],
|
|
14
|
+
["id", 1],
|
|
15
|
+
["className", 2],
|
|
16
|
+
["style", 3],
|
|
17
|
+
["src", 4],
|
|
18
|
+
["stretch", 5],
|
|
19
|
+
["width", 6],
|
|
20
|
+
["height", 7],
|
|
21
|
+
["accessibilityLabel", 8]
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
registerElement("animatedimage", Img);
|
|
25
|
+
var AnimatedImage = defineUniversalComponent("nativescript", function AnimatedImage(props) {
|
|
26
|
+
return universalValue(__octaneUniversalPlan0, [
|
|
27
|
+
(v) => applyEscapeProps(v, props),
|
|
28
|
+
props.id,
|
|
29
|
+
props.className,
|
|
30
|
+
props.style,
|
|
31
|
+
props.src,
|
|
32
|
+
props.stretch ?? "aspectFit",
|
|
33
|
+
props.width,
|
|
34
|
+
props.height,
|
|
35
|
+
props.alt
|
|
36
|
+
]);
|
|
37
|
+
}, { "module": "@nativescript-community/octane" });
|
|
38
|
+
//#endregion
|
|
39
|
+
export { AnimatedImage };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isAndroid, isIOS } from "@nativescript/core";
|
|
2
|
+
//#region src/escape-props.ts
|
|
3
|
+
/** Apply only the active device's escape bag after the shared props — the
|
|
4
|
+
* same convention @octane-xplat/ui uses, kept local so this leaf does not
|
|
5
|
+
* depend on ui. */
|
|
6
|
+
function applyEscapeProps(view, props) {
|
|
7
|
+
if (!view) return;
|
|
8
|
+
const bag = isIOS ? props.ios : isAndroid ? props.android : void 0;
|
|
9
|
+
if (bag) Object.assign(view, bag);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { applyEscapeProps };
|
package/dist/native/index.js
CHANGED
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
5
|
-
"kind": "host",
|
|
6
|
-
"type": "animatedimage",
|
|
7
|
-
"props": {
|
|
8
|
-
"animatedImageView": true,
|
|
9
|
-
"autoPlayAnimations": true
|
|
10
|
-
},
|
|
11
|
-
"bindings": [
|
|
12
|
-
["id", 0],
|
|
13
|
-
["className", 1],
|
|
14
|
-
["style", 2],
|
|
15
|
-
["src", 3],
|
|
16
|
-
["stretch", 4],
|
|
17
|
-
["width", 5],
|
|
18
|
-
["height", 6],
|
|
19
|
-
["accessibilityLabel", 7]
|
|
20
|
-
]
|
|
21
|
-
});
|
|
22
|
-
registerElement("animatedimage", Img);
|
|
23
|
-
var AnimatedImage = defineUniversalComponent("nativescript", function AnimatedImage(props) {
|
|
24
|
-
return universalValue(__octaneUniversalPlan0, [
|
|
25
|
-
props.id,
|
|
26
|
-
props.className,
|
|
27
|
-
props.style,
|
|
28
|
-
props.src,
|
|
29
|
-
props.stretch ?? "aspectFit",
|
|
30
|
-
props.width,
|
|
31
|
-
props.height,
|
|
32
|
-
props.alt
|
|
33
|
-
]);
|
|
34
|
-
}, { "module": "@nativescript-community/octane" });
|
|
35
|
-
//#endregion
|
|
36
|
-
export { AnimatedImage };
|
|
1
|
+
import { AnimatedImage } from "./AnimatedImage.tsrx.js";
|
|
2
|
+
import { prefetch } from "./prefetch.js";
|
|
3
|
+
export { AnimatedImage, prefetch };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function prefetchBatch(srcs, concurrency, warm) {
|
|
2
|
+
const urls = Array.isArray(srcs) ? srcs : [srcs];
|
|
3
|
+
if (urls.length === 0) return Promise.resolve(true);
|
|
4
|
+
const requested = Math.floor(concurrency ?? 5);
|
|
5
|
+
const limit = Number.isFinite(requested) && requested > 0 ? requested : 5;
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
let next = 0;
|
|
8
|
+
let active = Math.min(limit, urls.length);
|
|
9
|
+
for (let i = 0; i < active; i++) (async () => {
|
|
10
|
+
while (next < urls.length) {
|
|
11
|
+
const url = urls[next++];
|
|
12
|
+
try {
|
|
13
|
+
if (!await warm(url)) {
|
|
14
|
+
resolve(false);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
resolve(false);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (--active === 0) resolve(true);
|
|
23
|
+
})();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { prefetchBatch };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { prefetchBatch } from "./prefetch-batch.js";
|
|
2
|
+
import { getImagePipeline, initialize } from "@nativescript-community/ui-image";
|
|
3
|
+
import { isAndroid } from "@nativescript/core";
|
|
4
|
+
//#region src/prefetch.ts
|
|
5
|
+
/** prefetch — warm the image engine's disk cache for `srcs` without displaying
|
|
6
|
+
* anything. Android (Fresco): `prefetchToDiskCache` stores encoded bytes on
|
|
7
|
+
* disk and defers bitmap decode to display. iOS (SDWebImage): the prefetcher
|
|
8
|
+
* stores the downloaded image to the disk cache only. Either way a later
|
|
9
|
+
* `AnimatedImage` with the same src hits disk instead of the network.
|
|
10
|
+
* Resolves false when any URL fails; resolves false when the pipeline is not
|
|
11
|
+
* available yet (e.g. called before app launch).
|
|
12
|
+
* `options.concurrency` caps parallel fetches (default 5); `options.headers`
|
|
13
|
+
* is honored on Android and silently dropped on iOS (upstream gap — see
|
|
14
|
+
* PrefetchOptions). */
|
|
15
|
+
function prefetch(srcs, options) {
|
|
16
|
+
try {
|
|
17
|
+
if (isAndroid) initialize();
|
|
18
|
+
const pipeline = getImagePipeline();
|
|
19
|
+
const { concurrency, ...engineOptions } = options ?? {};
|
|
20
|
+
return prefetchBatch(srcs, concurrency, (url) => pipeline.prefetchToDiskCache(url, engineOptions).then(() => true, () => false));
|
|
21
|
+
} catch {
|
|
22
|
+
return Promise.resolve(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { prefetch };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { applyEscapeProps } from "./escape-props.web.js";
|
|
2
|
+
import { __s, bag7, clone, queueRefAttach, queueRefDetach, setStyle, template } from "octane";
|
|
3
|
+
import { bindSignalAttribute, enableSignalBindings, replaceRef, setPlainAttribute, setURLAttribute } from "octane/internal/client";
|
|
4
|
+
//#region src/AnimatedImage.web.tsrx
|
|
5
|
+
enableSignalBindings(1, true);
|
|
6
|
+
var _t$0 = /* @__PURE__ */ template("<img/>");
|
|
7
|
+
var _rm$0 = [
|
|
8
|
+
"r",
|
|
9
|
+
"b",
|
|
10
|
+
"a"
|
|
11
|
+
];
|
|
12
|
+
var OBJECT_FIT = {
|
|
13
|
+
none: "none",
|
|
14
|
+
fill: "fill",
|
|
15
|
+
aspectFit: "contain",
|
|
16
|
+
aspectFill: "cover"
|
|
17
|
+
};
|
|
18
|
+
var AnimatedImage = /* @__PURE__ */ __s(function AnimatedImage(props, __s, __extra) {
|
|
19
|
+
let _b = __s.slots[0];
|
|
20
|
+
if (_b === void 0) {
|
|
21
|
+
let _m0, _m1;
|
|
22
|
+
const _root = clone(_t$0);
|
|
23
|
+
queueRefAttach(__s, _m1 = (e) => applyEscapeProps(e, props), _m0 = _root);
|
|
24
|
+
(__s.cleanups ??= []).push(() => _b && queueRefDetach(_b.b, _b.a));
|
|
25
|
+
_b = bag7(__s, _root, _m0, _m1, void 0, void 0, void 0, void 0, void 0);
|
|
26
|
+
__s.refFields = _rm$0;
|
|
27
|
+
} else {
|
|
28
|
+
const _r = (e) => applyEscapeProps(e, props);
|
|
29
|
+
if (_r !== _b.b) _b.b = replaceRef(__s, _b.b, _r, _b.a);
|
|
30
|
+
}
|
|
31
|
+
_b.c = bindSignalAttribute(__s, _b.c, _b.a, "id", props.id, "b:524dc1ba", setPlainAttribute);
|
|
32
|
+
_b.d = bindSignalAttribute(__s, _b.d, _b.a, "class", props.className, "b:0279b906", "class");
|
|
33
|
+
{
|
|
34
|
+
const _v = {
|
|
35
|
+
objectFit: OBJECT_FIT[props.stretch ?? "aspectFit"],
|
|
36
|
+
width: props.width,
|
|
37
|
+
height: props.height,
|
|
38
|
+
...props.style
|
|
39
|
+
};
|
|
40
|
+
if (_b.e !== _v) {
|
|
41
|
+
setStyle(_b.a, _v, _b.e);
|
|
42
|
+
_b.e = _v;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
_b.f = bindSignalAttribute(__s, _b.f, _b.a, "src", props.src, "b:f8b000cf", setURLAttribute);
|
|
46
|
+
_b.g = bindSignalAttribute(__s, _b.g, _b.a, "alt", props.alt, "b:29df9938", setPlainAttribute);
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
export { AnimatedImage };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/escape-props.web.ts
|
|
2
|
+
/** Apply DOM escape props after the primitive's own props. Dash-named keys
|
|
3
|
+
* are attributes; other keys use the DOM property where available. Mirrors
|
|
4
|
+
* @octane-xplat/ui's web escape-props so this leaf does not depend on ui. */
|
|
5
|
+
function applyEscapeProps(element, props) {
|
|
6
|
+
if (!element || !props.web) return;
|
|
7
|
+
for (const [key, value] of Object.entries(props.web)) if (key.includes("-")) {
|
|
8
|
+
if (value == null || value === false) element.removeAttribute(key);
|
|
9
|
+
else element.setAttribute(key, value === true ? "" : String(value));
|
|
10
|
+
} else if (key in element) try {
|
|
11
|
+
element[key] = value;
|
|
12
|
+
} catch {
|
|
13
|
+
element.setAttribute(key, String(value));
|
|
14
|
+
}
|
|
15
|
+
else if (value != null) element.setAttribute(key, String(value));
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { applyEscapeProps };
|
package/dist/web/index.js
CHANGED
|
@@ -1,38 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
enableSignalBindings(1, true);
|
|
5
|
-
var _t$0 = /* @__PURE__ */ template("<img/>");
|
|
6
|
-
var OBJECT_FIT = {
|
|
7
|
-
none: "none",
|
|
8
|
-
fill: "fill",
|
|
9
|
-
aspectFit: "contain",
|
|
10
|
-
aspectFill: "cover"
|
|
11
|
-
};
|
|
12
|
-
var AnimatedImage = /* @__PURE__ */ __s(function AnimatedImage(props, __s, __extra) {
|
|
13
|
-
let _b = __s.slots[0];
|
|
14
|
-
if (_b === void 0) {
|
|
15
|
-
let _m1;
|
|
16
|
-
const _root = clone(_t$0);
|
|
17
|
-
_m1 = _root;
|
|
18
|
-
_b = bag6(__s, _root, void 0, _m1, void 0, void 0, void 0, void 0);
|
|
19
|
-
}
|
|
20
|
-
_b.a = bindSignalAttribute(__s, _b.a, _b.b, "id", props.id, "b:42ec7ee7", setPlainAttribute);
|
|
21
|
-
_b.c = bindSignalAttribute(__s, _b.c, _b.b, "class", props.className, "b:1b1f2715", "class");
|
|
22
|
-
{
|
|
23
|
-
const _v = {
|
|
24
|
-
objectFit: OBJECT_FIT[props.stretch ?? "aspectFit"],
|
|
25
|
-
width: props.width,
|
|
26
|
-
height: props.height,
|
|
27
|
-
...props.style
|
|
28
|
-
};
|
|
29
|
-
if (_b.d !== _v) {
|
|
30
|
-
setStyle(_b.b, _v, _b.d);
|
|
31
|
-
_b.d = _v;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
_b.e = bindSignalAttribute(__s, _b.e, _b.b, "src", props.src, "b:3d7475fa", setURLAttribute);
|
|
35
|
-
_b.f = bindSignalAttribute(__s, _b.f, _b.b, "alt", props.alt, "b:3f7e4d1f", setPlainAttribute);
|
|
36
|
-
});
|
|
37
|
-
//#endregion
|
|
38
|
-
export { AnimatedImage };
|
|
1
|
+
import { AnimatedImage } from "./AnimatedImage.web.tsrx.js";
|
|
2
|
+
import { prefetch } from "./prefetch.web.js";
|
|
3
|
+
export { AnimatedImage, prefetch };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function prefetchBatch(srcs, concurrency, warm) {
|
|
2
|
+
const urls = Array.isArray(srcs) ? srcs : [srcs];
|
|
3
|
+
if (urls.length === 0) return Promise.resolve(true);
|
|
4
|
+
const requested = Math.floor(concurrency ?? 5);
|
|
5
|
+
const limit = Number.isFinite(requested) && requested > 0 ? requested : 5;
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
let next = 0;
|
|
8
|
+
let active = Math.min(limit, urls.length);
|
|
9
|
+
for (let i = 0; i < active; i++) (async () => {
|
|
10
|
+
while (next < urls.length) {
|
|
11
|
+
const url = urls[next++];
|
|
12
|
+
try {
|
|
13
|
+
if (!await warm(url)) {
|
|
14
|
+
resolve(false);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
resolve(false);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (--active === 0) resolve(true);
|
|
23
|
+
})();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { prefetchBatch };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { prefetchBatch } from "./prefetch-batch.js";
|
|
2
|
+
//#region src/prefetch.web.ts
|
|
3
|
+
/** prefetch — load each `src` through a throwaway <img> so the response lands
|
|
4
|
+
* in the browser HTTP cache; decode and render stay with the displaying
|
|
5
|
+
* element. `options.headers` is ignored — an <img> request cannot carry
|
|
6
|
+
* custom headers. `options.concurrency` caps parallel loads (default 5).
|
|
7
|
+
* Resolves false when any URL fails. */
|
|
8
|
+
function prefetch(srcs, options) {
|
|
9
|
+
return prefetchBatch(srcs, options?.concurrency, (url) => new Promise((resolve) => {
|
|
10
|
+
const image = new Image();
|
|
11
|
+
image.onload = () => resolve(true);
|
|
12
|
+
image.onerror = () => resolve(false);
|
|
13
|
+
image.src = url;
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { prefetch };
|
package/package.json
CHANGED
package/src/AnimatedImage.tsrx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { registerElement } from '@nativescript-community/octane';
|
|
3
3
|
import { Img } from '@nativescript-community/ui-image';
|
|
4
4
|
import type { AnimatedImageProps } from './props';
|
|
5
|
+
import { applyEscapeProps } from './escape-props';
|
|
5
6
|
|
|
6
7
|
// @nativescript-community/ui-image — Fresco DraweeView on Android,
|
|
7
8
|
// SDAnimatedImageView on iOS when `animatedImageView` is set (plain UIImageView
|
|
@@ -14,6 +15,7 @@ registerElement('animatedimage', Img);
|
|
|
14
15
|
* the shared contract is always-playing. */
|
|
15
16
|
export function AnimatedImage(props: AnimatedImageProps) @{
|
|
16
17
|
<animatedimage
|
|
18
|
+
ref={(v: any) => applyEscapeProps(v, props)}
|
|
17
19
|
id={props.id}
|
|
18
20
|
className={props.className}
|
|
19
21
|
style={props.style}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnimatedImageProps } from './props';
|
|
2
|
+
import { applyEscapeProps } from './escape-props.web';
|
|
2
3
|
|
|
3
4
|
const OBJECT_FIT = {
|
|
4
5
|
none: 'none',
|
|
@@ -11,6 +12,7 @@ const OBJECT_FIT = {
|
|
|
11
12
|
* `stretch` maps to object-fit. */
|
|
12
13
|
export function AnimatedImage(props: AnimatedImageProps) @{
|
|
13
14
|
<img
|
|
15
|
+
ref={(e: any) => applyEscapeProps(e, props)}
|
|
14
16
|
id={props.id}
|
|
15
17
|
className={props.className}
|
|
16
18
|
style={{
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isAndroid, isIOS } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
/** Apply only the active device's escape bag after the shared props — the
|
|
4
|
+
* same convention @octane-xplat/ui uses, kept local so this leaf does not
|
|
5
|
+
* depend on ui. */
|
|
6
|
+
export function applyEscapeProps(view: any, props: { ios?: any; android?: any }): void {
|
|
7
|
+
if (!view) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const bag = isIOS ? props.ios : isAndroid ? props.android : undefined
|
|
12
|
+
if (bag) {
|
|
13
|
+
Object.assign(view, bag)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Apply DOM escape props after the primitive's own props. Dash-named keys
|
|
2
|
+
* are attributes; other keys use the DOM property where available. Mirrors
|
|
3
|
+
* @octane-xplat/ui's web escape-props so this leaf does not depend on ui. */
|
|
4
|
+
export function applyEscapeProps(element: any, props: { web?: any }): void {
|
|
5
|
+
if (!element || !props.web) {
|
|
6
|
+
return
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
for (const [key, value] of Object.entries(props.web)) {
|
|
10
|
+
if (key.includes('-')) {
|
|
11
|
+
if (value == null || value === false) {
|
|
12
|
+
element.removeAttribute(key)
|
|
13
|
+
} else {
|
|
14
|
+
element.setAttribute(key, value === true ? '' : String(value))
|
|
15
|
+
}
|
|
16
|
+
} else if (key in element) {
|
|
17
|
+
try {
|
|
18
|
+
element[key] = value
|
|
19
|
+
} catch {
|
|
20
|
+
element.setAttribute(key, String(value))
|
|
21
|
+
}
|
|
22
|
+
} else if (value != null) {
|
|
23
|
+
element.setAttribute(key, String(value))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.macos.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Platform barrels — explicit .tsrx leaf imports (moduleSuffixes don't
|
|
2
2
|
// reach .tsrx; same convention as @octane-xplat/ui index.*.ts).
|
|
3
3
|
export { AnimatedImage } from './AnimatedImage.tsrx'
|
|
4
|
-
export
|
|
4
|
+
export { prefetch } from './prefetch'
|
|
5
|
+
export type { AnimatedImageProps, PrefetchOptions } from './props'
|
package/src/index.web.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Platform barrels — explicit .tsrx leaf imports (moduleSuffixes don't
|
|
2
2
|
// reach .tsrx; same convention as @octane-xplat/ui index.*.ts).
|
|
3
3
|
export { AnimatedImage } from './AnimatedImage.web.tsrx'
|
|
4
|
-
export
|
|
4
|
+
export { prefetch } from './prefetch.web'
|
|
5
|
+
export type { AnimatedImageProps, PrefetchOptions } from './props'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** VENDORED — identical copy in packages/gif/src/prefetch-batch.ts; keep in
|
|
2
|
+
* sync.
|
|
3
|
+
*
|
|
4
|
+
* Shared batch driver for the image-engine leaves' `prefetch` contract:
|
|
5
|
+
* normalizes `string | string[]`, caps in-flight requests JS-side, and
|
|
6
|
+
* resolves `true` only when every URL warms — `false` on the first failure
|
|
7
|
+
* (in-flight requests settle on their own; the result is already decided). */
|
|
8
|
+
|
|
9
|
+
/** Matches the `maxRequests = 5` ceiling NS core's ImageCache used. */
|
|
10
|
+
export const DEFAULT_PREFETCH_CONCURRENCY = 5
|
|
11
|
+
|
|
12
|
+
export function prefetchBatch(
|
|
13
|
+
srcs: string | string[],
|
|
14
|
+
concurrency: number | undefined,
|
|
15
|
+
warm: (url: string) => Promise<boolean>,
|
|
16
|
+
): Promise<boolean> {
|
|
17
|
+
const urls = Array.isArray(srcs) ? srcs : [srcs]
|
|
18
|
+
if (urls.length === 0) {
|
|
19
|
+
return Promise.resolve(true)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const requested = Math.floor(concurrency ?? DEFAULT_PREFETCH_CONCURRENCY)
|
|
23
|
+
const limit =
|
|
24
|
+
Number.isFinite(requested) && requested > 0 ? requested : DEFAULT_PREFETCH_CONCURRENCY
|
|
25
|
+
|
|
26
|
+
return new Promise<boolean>((resolve) => {
|
|
27
|
+
let next = 0
|
|
28
|
+
let active = Math.min(limit, urls.length)
|
|
29
|
+
for (let i = 0; i < active; i++) {
|
|
30
|
+
void (async () => {
|
|
31
|
+
while (next < urls.length) {
|
|
32
|
+
const url = urls[next++]
|
|
33
|
+
try {
|
|
34
|
+
if (!(await warm(url))) {
|
|
35
|
+
resolve(false)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
resolve(false)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (--active === 0) {
|
|
45
|
+
resolve(true)
|
|
46
|
+
}
|
|
47
|
+
})()
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PrefetchOptions } from './props'
|
|
2
|
+
|
|
3
|
+
/** prefetch — no-op on the AppKit host: it has no image pipeline yet
|
|
4
|
+
* (AnimatedImage renders a placeholder there), so nothing is warmed.
|
|
5
|
+
* Resolves false to report that honestly. */
|
|
6
|
+
export function prefetch(_srcs: string | string[], _options?: PrefetchOptions): Promise<boolean> {
|
|
7
|
+
return Promise.resolve(false)
|
|
8
|
+
}
|
package/src/prefetch.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isAndroid } from '@nativescript/core'
|
|
2
|
+
import { getImagePipeline, initialize } from '@nativescript-community/ui-image'
|
|
3
|
+
import { prefetchBatch } from './prefetch-batch'
|
|
4
|
+
import type { PrefetchOptions } from './props'
|
|
5
|
+
|
|
6
|
+
/** prefetch — warm the image engine's disk cache for `srcs` without displaying
|
|
7
|
+
* anything. Android (Fresco): `prefetchToDiskCache` stores encoded bytes on
|
|
8
|
+
* disk and defers bitmap decode to display. iOS (SDWebImage): the prefetcher
|
|
9
|
+
* stores the downloaded image to the disk cache only. Either way a later
|
|
10
|
+
* `AnimatedImage` with the same src hits disk instead of the network.
|
|
11
|
+
* Resolves false when any URL fails; resolves false when the pipeline is not
|
|
12
|
+
* available yet (e.g. called before app launch).
|
|
13
|
+
* `options.concurrency` caps parallel fetches (default 5); `options.headers`
|
|
14
|
+
* is honored on Android and silently dropped on iOS (upstream gap — see
|
|
15
|
+
* PrefetchOptions). */
|
|
16
|
+
export function prefetch(srcs: string | string[], options?: PrefetchOptions): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
// The plugin lazily initializes Fresco inside createNativeView; a prefetch
|
|
19
|
+
// that runs before any Img mounts has to do it first. initialize() is a
|
|
20
|
+
// no-op once the plugin (or the app) has initialized. iOS has no equivalent
|
|
21
|
+
// requirement and its initialize() is not guarded, so it stays Android-only.
|
|
22
|
+
if (isAndroid) {
|
|
23
|
+
initialize()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const pipeline = getImagePipeline()
|
|
27
|
+
const { concurrency, ...engineOptions } = options ?? {}
|
|
28
|
+
|
|
29
|
+
return prefetchBatch(srcs, concurrency, (url) =>
|
|
30
|
+
pipeline.prefetchToDiskCache(url, engineOptions).then(
|
|
31
|
+
() => true,
|
|
32
|
+
() => false,
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
} catch {
|
|
36
|
+
return Promise.resolve(false)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { prefetch } from './prefetch.web'
|
|
3
|
+
import { prefetchBatch } from './prefetch-batch'
|
|
4
|
+
|
|
5
|
+
// Same web prefetch contract as @octane-xplat/image: throwaway <img> per URL,
|
|
6
|
+
// awaited until load/error, false when any URL fails, JS-side concurrency cap.
|
|
7
|
+
|
|
8
|
+
class MockImage {
|
|
9
|
+
static instances: MockImage[] = []
|
|
10
|
+
onload: (() => void) | null = null
|
|
11
|
+
onerror: (() => void) | null = null
|
|
12
|
+
set src(_url: string) {
|
|
13
|
+
MockImage.instances.push(this)
|
|
14
|
+
}
|
|
15
|
+
load() {
|
|
16
|
+
this.onload?.()
|
|
17
|
+
}
|
|
18
|
+
fail() {
|
|
19
|
+
this.onerror?.()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
24
|
+
|
|
25
|
+
async function drainPending(promise: Promise<boolean>) {
|
|
26
|
+
let result: boolean | undefined
|
|
27
|
+
void promise.then((value) => {
|
|
28
|
+
result = value
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
while (result === undefined) {
|
|
32
|
+
for (const image of MockImage.instances.splice(0)) {
|
|
33
|
+
image.load()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await flush()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('prefetch (web)', () => {
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
MockImage.instances = []
|
|
45
|
+
vi.unstubAllGlobals()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('resolves true only after every <img> fires load', async () => {
|
|
49
|
+
vi.stubGlobal('Image', MockImage)
|
|
50
|
+
const promise = prefetch(['https://x/a.gif', 'https://x/b.gif'])
|
|
51
|
+
let settled = false
|
|
52
|
+
void promise.then(() => {
|
|
53
|
+
settled = true
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
await flush()
|
|
57
|
+
expect(settled).toBe(false)
|
|
58
|
+
expect(await drainPending(promise)).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('resolves false when any URL fails', async () => {
|
|
62
|
+
vi.stubGlobal('Image', MockImage)
|
|
63
|
+
const promise = prefetch(['https://x/a.gif', 'https://x/b.gif'])
|
|
64
|
+
const [failed] = MockImage.instances.splice(0)
|
|
65
|
+
failed.fail()
|
|
66
|
+
expect(await promise).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('accepts a single URL and resolves true for an empty batch', async () => {
|
|
70
|
+
vi.stubGlobal('Image', MockImage)
|
|
71
|
+
expect(await prefetch([])).toBe(true)
|
|
72
|
+
const promise = prefetch('https://x/a.gif')
|
|
73
|
+
expect(MockImage.instances).toHaveLength(1)
|
|
74
|
+
expect(await drainPending(promise)).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('caps in-flight loads at options.concurrency', async () => {
|
|
78
|
+
vi.stubGlobal('Image', MockImage)
|
|
79
|
+
const promise = prefetch(
|
|
80
|
+
Array.from({ length: 7 }, (_, i) => `https://x/${i}.gif`),
|
|
81
|
+
{ concurrency: 2, headers: { Authorization: 'Bearer x' } },
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect(MockImage.instances).toHaveLength(2)
|
|
85
|
+
MockImage.instances.splice(0).forEach((image) => image.load())
|
|
86
|
+
await flush()
|
|
87
|
+
await flush()
|
|
88
|
+
expect(MockImage.instances.length).toBeLessThanOrEqual(2)
|
|
89
|
+
expect(await drainPending(promise)).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('defaults to five parallel loads', async () => {
|
|
93
|
+
vi.stubGlobal('Image', MockImage)
|
|
94
|
+
const promise = prefetch(Array.from({ length: 9 }, (_, i) => `https://x/${i}.gif`))
|
|
95
|
+
expect(MockImage.instances).toHaveLength(5)
|
|
96
|
+
expect(await drainPending(promise)).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('prefetchBatch', () => {
|
|
101
|
+
it('reports false if the warm callback throws', async () => {
|
|
102
|
+
expect(await prefetchBatch(['a', 'b'], 5, () => Promise.reject(new Error('boom')))).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { prefetchBatch } from './prefetch-batch'
|
|
2
|
+
import type { PrefetchOptions } from './props'
|
|
3
|
+
|
|
4
|
+
/** prefetch — load each `src` through a throwaway <img> so the response lands
|
|
5
|
+
* in the browser HTTP cache; decode and render stay with the displaying
|
|
6
|
+
* element. `options.headers` is ignored — an <img> request cannot carry
|
|
7
|
+
* custom headers. `options.concurrency` caps parallel loads (default 5).
|
|
8
|
+
* Resolves false when any URL fails. */
|
|
9
|
+
export function prefetch(srcs: string | string[], options?: PrefetchOptions): Promise<boolean> {
|
|
10
|
+
return prefetchBatch(
|
|
11
|
+
srcs,
|
|
12
|
+
options?.concurrency,
|
|
13
|
+
(url) =>
|
|
14
|
+
new Promise<boolean>((resolve) => {
|
|
15
|
+
const image = new Image()
|
|
16
|
+
image.onload = () => resolve(true)
|
|
17
|
+
image.onerror = () => resolve(false)
|
|
18
|
+
image.src = url
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
}
|
package/src/props.ts
CHANGED
|
@@ -9,4 +9,26 @@ export interface AnimatedImageProps {
|
|
|
9
9
|
stretch?: 'none' | 'fill' | 'aspectFit' | 'aspectFill'
|
|
10
10
|
width?: string | number
|
|
11
11
|
height?: string | number
|
|
12
|
+
/** Platform-specific properties are applied after shared props. Native
|
|
13
|
+
* bags assign onto the ui-image `Img` view — `noCache`, `decodeWidth`,
|
|
14
|
+
* `decodeHeight`, `placeholderImageUri`, `progressiveRenderingEnabled`,
|
|
15
|
+
* `cacheKey`, and the rest of its prop surface are reachable this way;
|
|
16
|
+
* the shared props stay the parity contract. */
|
|
17
|
+
ios?: any
|
|
18
|
+
android?: any
|
|
19
|
+
web?: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Options for `prefetch`. The same contract `@octane-xplat/image`'s
|
|
23
|
+
* `prefetch` carries. */
|
|
24
|
+
export interface PrefetchOptions {
|
|
25
|
+
/** Request headers for the prefetch fetch. Honored on Android (Fresco);
|
|
26
|
+
* **dropped on iOS** — the plugin's prefetch path builds its SDWebImage
|
|
27
|
+
* context without the download-request-modifier branch its `Img`
|
|
28
|
+
* display path has; an upstream patch is needed there. Ignored on web —
|
|
29
|
+
* a plain <img> cannot send custom headers. */
|
|
30
|
+
headers?: Record<string, string>
|
|
31
|
+
/** Max URLs fetched in parallel (JS-side cap). Default 5 — the same
|
|
32
|
+
* ceiling NS core's ImageCache used. */
|
|
33
|
+
concurrency?: number
|
|
12
34
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Apply only the active device's escape bag after the shared props — the
|
|
2
|
+
* same convention @octane-xplat/ui uses, kept local so this leaf does not
|
|
3
|
+
* depend on ui. */
|
|
4
|
+
export declare function applyEscapeProps(view: any, props: {
|
|
5
|
+
ios?: any;
|
|
6
|
+
android?: any;
|
|
7
|
+
}): void;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Apply DOM escape props after the primitive's own props. Dash-named keys
|
|
2
|
+
* are attributes; other keys use the DOM property where available. Mirrors
|
|
3
|
+
* @octane-xplat/ui's web escape-props so this leaf does not depend on ui. */
|
|
4
|
+
export declare function applyEscapeProps(element: any, props: {
|
|
5
|
+
web?: any;
|
|
6
|
+
}): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** VENDORED — identical copy in packages/gif/src/prefetch-batch.ts; keep in
|
|
2
|
+
* sync.
|
|
3
|
+
*
|
|
4
|
+
* Shared batch driver for the image-engine leaves' `prefetch` contract:
|
|
5
|
+
* normalizes `string | string[]`, caps in-flight requests JS-side, and
|
|
6
|
+
* resolves `true` only when every URL warms — `false` on the first failure
|
|
7
|
+
* (in-flight requests settle on their own; the result is already decided). */
|
|
8
|
+
/** Matches the `maxRequests = 5` ceiling NS core's ImageCache used. */
|
|
9
|
+
export declare const DEFAULT_PREFETCH_CONCURRENCY = 5;
|
|
10
|
+
export declare function prefetchBatch(srcs: string | string[], concurrency: number | undefined, warm: (url: string) => Promise<boolean>): Promise<boolean>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PrefetchOptions } from './props.js';
|
|
2
|
+
/** prefetch — warm the image engine's disk cache for `srcs` without displaying
|
|
3
|
+
* anything. Android (Fresco): `prefetchToDiskCache` stores encoded bytes on
|
|
4
|
+
* disk and defers bitmap decode to display. iOS (SDWebImage): the prefetcher
|
|
5
|
+
* stores the downloaded image to the disk cache only. Either way a later
|
|
6
|
+
* `AnimatedImage` with the same src hits disk instead of the network.
|
|
7
|
+
* Resolves false when any URL fails; resolves false when the pipeline is not
|
|
8
|
+
* available yet (e.g. called before app launch).
|
|
9
|
+
* `options.concurrency` caps parallel fetches (default 5); `options.headers`
|
|
10
|
+
* is honored on Android and silently dropped on iOS (upstream gap — see
|
|
11
|
+
* PrefetchOptions). */
|
|
12
|
+
export declare function prefetch(srcs: string | string[], options?: PrefetchOptions): Promise<boolean>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PrefetchOptions } from './props.js';
|
|
2
|
+
/** prefetch — load each `src` through a throwaway <img> so the response lands
|
|
3
|
+
* in the browser HTTP cache; decode and render stay with the displaying
|
|
4
|
+
* element. `options.headers` is ignored — an <img> request cannot carry
|
|
5
|
+
* custom headers. `options.concurrency` caps parallel loads (default 5).
|
|
6
|
+
* Resolves false when any URL fails. */
|
|
7
|
+
export declare function prefetch(srcs: string | string[], options?: PrefetchOptions): Promise<boolean>;
|
|
@@ -9,4 +9,25 @@ export interface AnimatedImageProps {
|
|
|
9
9
|
stretch?: 'none' | 'fill' | 'aspectFit' | 'aspectFill';
|
|
10
10
|
width?: string | number;
|
|
11
11
|
height?: string | number;
|
|
12
|
+
/** Platform-specific properties are applied after shared props. Native
|
|
13
|
+
* bags assign onto the ui-image `Img` view — `noCache`, `decodeWidth`,
|
|
14
|
+
* `decodeHeight`, `placeholderImageUri`, `progressiveRenderingEnabled`,
|
|
15
|
+
* `cacheKey`, and the rest of its prop surface are reachable this way;
|
|
16
|
+
* the shared props stay the parity contract. */
|
|
17
|
+
ios?: any;
|
|
18
|
+
android?: any;
|
|
19
|
+
web?: any;
|
|
20
|
+
}
|
|
21
|
+
/** Options for `prefetch`. The same contract `@octane-xplat/image`'s
|
|
22
|
+
* `prefetch` carries. */
|
|
23
|
+
export interface PrefetchOptions {
|
|
24
|
+
/** Request headers for the prefetch fetch. Honored on Android (Fresco);
|
|
25
|
+
* **dropped on iOS** — the plugin's prefetch path builds its SDWebImage
|
|
26
|
+
* context without the download-request-modifier branch its `Img`
|
|
27
|
+
* display path has; an upstream patch is needed there. Ignored on web —
|
|
28
|
+
* a plain <img> cannot send custom headers. */
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
/** Max URLs fetched in parallel (JS-side cap). Default 5 — the same
|
|
31
|
+
* ceiling NS core's ImageCache used. */
|
|
32
|
+
concurrency?: number;
|
|
12
33
|
}
|
package/types/index.macos.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AnimatedImageProps } from './generated/props.js'
|
|
1
|
+
import type { AnimatedImageProps, PrefetchOptions } from './generated/props.js'
|
|
2
2
|
|
|
3
|
-
export type { AnimatedImageProps } from './generated/props.js'
|
|
3
|
+
export type { AnimatedImageProps, PrefetchOptions } from './generated/props.js'
|
|
4
4
|
export declare function AnimatedImage(props: AnimatedImageProps): unknown
|
|
5
|
+
export declare function prefetch(srcs: string | string[], options?: PrefetchOptions): Promise<boolean>
|