react-native-fast-image 8.22.0 → 8.24.0
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 +30 -1
- package/dist/constants.d.ts +16 -0
- package/dist/index.cjs.js +3 -1
- package/dist/index.cjs.web.js +193 -0
- package/dist/index.cjs.web.js.flow +154 -0
- package/dist/index.d.ts +1 -16
- package/dist/index.js +3 -1
- package/dist/index.web.d.ts +6 -0
- package/dist/index.web.js +165 -0
- package/dist/index.web.js.flow +154 -0
- package/ios/FastImage/FFFDownsampledImage.m +6 -1
- package/ios/FastImage/FFFastImageSource.h +10 -1
- package/ios/FastImage/FFFastImageSource.m +157 -0
- package/ios/FastImage/FFFastImageView.m +18 -5
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -105,6 +105,25 @@ When `source` changes, the image that's showing stays until the new one has load
|
|
|
105
105
|
|
|
106
106
|
Remote url to load the image from. e.g. `'https://facebook.github.io/react/img/logo_og.png'`.
|
|
107
107
|
|
|
108
|
+
#### Photo library images (iOS)
|
|
109
|
+
|
|
110
|
+
A photo library url (`ph://<localIdentifier>`, as camera roll libraries give) loads when the app has SDWebImagePhotosPlugin. Add it to the app's `ios/Podfile` and run `pod install`:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
pod 'SDWebImagePhotosPlugin'
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
With Expo, add it with [expo-build-properties](https://docs.expo.dev/versions/latest/sdk/build-properties/):
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
[
|
|
120
|
+
"expo-build-properties",
|
|
121
|
+
{ "ios": { "extraPods": [{ "name": "SDWebImagePhotosPlugin" }] } }
|
|
122
|
+
]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The app needs access to the photo library, which it has if it got the url from there. A photo library image is decoded at about the view's size, since photos are large and usually shown small; `onLoad` still reports the photo's own size. `FastImage.preload` of a `ph://` source loads the full-size photo (there's no view to size it for), which doesn't make a view's smaller copy load faster, so preloading photo library images usually isn't worth it. Without the plugin, a `ph://` source fails with `onError`, saying so. `assets-library://` urls aren't supported. On Android, photo pickers give `content://` urls, which load as they are.
|
|
126
|
+
|
|
108
127
|
---
|
|
109
128
|
|
|
110
129
|
### `source.headers?: object`
|
|
@@ -239,7 +258,7 @@ iOS only. Decodes a large image at about the size it's shown at, instead of at f
|
|
|
239
258
|
|
|
240
259
|
Use it when you show images much larger than their views and can't get them at the right size, e.g. user uploads or other people's URLs in a list. If you control the images, serve them at the size they're shown instead (resized on your server or by an image CDN), which also saves bandwidth.
|
|
241
260
|
|
|
242
|
-
Decoding a smaller copy can take a little longer, so use it where the memory matters. Needs SDWebImage 5.19.7 or later. On Android images are already decoded at about the view's size.
|
|
261
|
+
Decoding a smaller copy can take a little longer, so use it where the memory matters. Needs SDWebImage 5.19.7 or later. Photo library images are always decoded this way. On Android images are already decoded at about the view's size.
|
|
243
262
|
|
|
244
263
|
---
|
|
245
264
|
|
|
@@ -486,6 +505,16 @@ On Android, if your app has its own `AppGlideModule` (see [Are you using Glide a
|
|
|
486
505
|
|
|
487
506
|
Images with `cache: 'web'` are kept in their own HTTP cache instead, up to 50 MB on each platform, which these limits don't change.
|
|
488
507
|
|
|
508
|
+
## Web
|
|
509
|
+
|
|
510
|
+
With [react-native-web](https://necolas.github.io/react-native-web/), FastImage shows images with the web's `Image`. Bundlers pick the web version through the package's `browser` field, or the `.web.js` files next to the native ones.
|
|
511
|
+
|
|
512
|
+
Support is minimal:
|
|
513
|
+
|
|
514
|
+
- Props that work: `source` (a `uri`, or a `require()`d image), `defaultSource`, `resizeMode`, `tintColor`, `blurRadius`, `style`, children, `onLoadStart`, `onLoad`, `onError`, `onLoadEnd`, and View props such as `testID`, accessibility props, `onLayout` and `pointerEvents`.
|
|
515
|
+
- Ignored: `source.headers` (a browser can't send them for an image), `source.priority`, `source.cache`, `source.cacheKey`, `source.memoryCache`, `recyclingKey`, `loop`, `imageRendering`, `paused`, `transition`, `downsample`, `fallback` and `onProgress`.
|
|
516
|
+
- `FastImage.preload` loads the images into the browser's cache and resolves with a result per source. `clearMemoryCache`, `clearDiskCache` and `configureCache` resolve without doing anything (the browser manages its cache), and `getCachePath` and `writeToCache` resolve with `{ ok: false, error: 'Not supported on the web' }`.
|
|
517
|
+
|
|
489
518
|
## Troubleshooting
|
|
490
519
|
|
|
491
520
|
If you have any problems using this library try the steps in [troubleshooting](docs/troubleshooting.md) and see if they fix it.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const resizeMode: {
|
|
2
|
+
readonly contain: 'contain';
|
|
3
|
+
readonly cover: 'cover';
|
|
4
|
+
readonly stretch: 'stretch';
|
|
5
|
+
readonly center: 'center';
|
|
6
|
+
};
|
|
7
|
+
export declare const priority: {
|
|
8
|
+
readonly low: 'low';
|
|
9
|
+
readonly normal: 'normal';
|
|
10
|
+
readonly high: 'high';
|
|
11
|
+
};
|
|
12
|
+
export declare const cacheControl: {
|
|
13
|
+
readonly immutable: 'immutable';
|
|
14
|
+
readonly web: 'web';
|
|
15
|
+
readonly cacheOnly: 'cacheOnly';
|
|
16
|
+
};
|
package/dist/index.cjs.js
CHANGED
|
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
let react = require("react");
|
|
28
28
|
react = __toESM(react);
|
|
29
29
|
let react_native = require("react-native");
|
|
30
|
-
//#region src/
|
|
30
|
+
//#region src/constants.ts
|
|
31
31
|
const resizeMode = {
|
|
32
32
|
contain: "contain",
|
|
33
33
|
cover: "cover",
|
|
@@ -44,6 +44,8 @@ const cacheControl = {
|
|
|
44
44
|
web: "web",
|
|
45
45
|
cacheOnly: "cacheOnly"
|
|
46
46
|
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/index.tsx
|
|
47
49
|
const resolveDefaultSource = (defaultSource) => {
|
|
48
50
|
if (!defaultSource) return null;
|
|
49
51
|
if (react_native.Platform.OS === "android") {
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
14
|
+
key = keys[i];
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
//#endregion
|
|
27
|
+
let react = require("react");
|
|
28
|
+
react = __toESM(react);
|
|
29
|
+
let react_native = require("react-native");
|
|
30
|
+
//#region src/constants.ts
|
|
31
|
+
const resizeMode = {
|
|
32
|
+
contain: "contain",
|
|
33
|
+
cover: "cover",
|
|
34
|
+
stretch: "stretch",
|
|
35
|
+
center: "center"
|
|
36
|
+
};
|
|
37
|
+
const priority = {
|
|
38
|
+
low: "low",
|
|
39
|
+
normal: "normal",
|
|
40
|
+
high: "high"
|
|
41
|
+
};
|
|
42
|
+
const cacheControl = {
|
|
43
|
+
immutable: "immutable",
|
|
44
|
+
web: "web",
|
|
45
|
+
cacheOnly: "cacheOnly"
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/index.web.tsx
|
|
49
|
+
const notSupported = {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: "Not supported on the web"
|
|
52
|
+
};
|
|
53
|
+
function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, resizeMode: mode = "cover", forwardedRef, onLayout, pointerEvents, fallback: _fallback, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
54
|
+
const result = (0, react.useRef)(void 0);
|
|
55
|
+
const sent = (0, react.useRef)(Promise.resolve());
|
|
56
|
+
const image = (0, react.useRef)(null);
|
|
57
|
+
const uri = typeof source === "object" && source ? source.uri : void 0;
|
|
58
|
+
const webSource = typeof source === "number" ? source : uri ? { uri } : void 0;
|
|
59
|
+
return /* @__PURE__ */ react.default.createElement(react_native.View, {
|
|
60
|
+
style: [styles.container, style],
|
|
61
|
+
onLayout,
|
|
62
|
+
pointerEvents,
|
|
63
|
+
ref: forwardedRef
|
|
64
|
+
}, /* @__PURE__ */ react.default.createElement(react_native.Image, {
|
|
65
|
+
...props,
|
|
66
|
+
style: styles.image,
|
|
67
|
+
source: webSource,
|
|
68
|
+
defaultSource,
|
|
69
|
+
resizeMode: mode,
|
|
70
|
+
tintColor,
|
|
71
|
+
blurRadius,
|
|
72
|
+
ref: image,
|
|
73
|
+
onLoadStart,
|
|
74
|
+
onLoad: (event) => {
|
|
75
|
+
var _event$nativeEvent;
|
|
76
|
+
sent.current = loadedSize(event === null || event === void 0 || (_event$nativeEvent = event.nativeEvent) === null || _event$nativeEvent === void 0 ? void 0 : _event$nativeEvent.target, image.current, uri).then(({ width, height }) => {
|
|
77
|
+
result.current = {
|
|
78
|
+
ok: true,
|
|
79
|
+
width,
|
|
80
|
+
height
|
|
81
|
+
};
|
|
82
|
+
onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
|
|
83
|
+
width,
|
|
84
|
+
height
|
|
85
|
+
} });
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
onError: (event) => {
|
|
89
|
+
var _event$nativeEvent$er, _event$nativeEvent2;
|
|
90
|
+
const error = String((_event$nativeEvent$er = event === null || event === void 0 || (_event$nativeEvent2 = event.nativeEvent) === null || _event$nativeEvent2 === void 0 ? void 0 : _event$nativeEvent2.error) !== null && _event$nativeEvent$er !== void 0 ? _event$nativeEvent$er : "Failed to load the image");
|
|
91
|
+
result.current = {
|
|
92
|
+
ok: false,
|
|
93
|
+
error
|
|
94
|
+
};
|
|
95
|
+
sent.current = Promise.resolve();
|
|
96
|
+
onError === null || onError === void 0 || onError({ nativeEvent: { error } });
|
|
97
|
+
},
|
|
98
|
+
onLoadEnd: () => {
|
|
99
|
+
sent.current.then(() => {
|
|
100
|
+
var _result$current;
|
|
101
|
+
onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd((_result$current = result.current) !== null && _result$current !== void 0 ? _result$current : {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: "Failed to load the image"
|
|
104
|
+
});
|
|
105
|
+
result.current = void 0;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}), children);
|
|
109
|
+
}
|
|
110
|
+
const FastImageMemo = (0, react.memo)(FastImageBase);
|
|
111
|
+
const FastImageComponent = (0, react.forwardRef)((props, ref) => /* @__PURE__ */ react.default.createElement(FastImageMemo, {
|
|
112
|
+
forwardedRef: ref,
|
|
113
|
+
...props
|
|
114
|
+
}));
|
|
115
|
+
FastImageComponent.displayName = "FastImage";
|
|
116
|
+
async function loadedSize(loaded, view, uri) {
|
|
117
|
+
var _view$querySelector;
|
|
118
|
+
if (loaded === null || loaded === void 0 ? void 0 : loaded.naturalWidth) return {
|
|
119
|
+
width: loaded.naturalWidth,
|
|
120
|
+
height: loaded.naturalHeight
|
|
121
|
+
};
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
123
|
+
const shown = view === null || view === void 0 || (_view$querySelector = view.querySelector) === null || _view$querySelector === void 0 ? void 0 : _view$querySelector.call(view, "img");
|
|
124
|
+
if (shown === null || shown === void 0 ? void 0 : shown.naturalWidth) return {
|
|
125
|
+
width: shown.naturalWidth,
|
|
126
|
+
height: shown.naturalHeight
|
|
127
|
+
};
|
|
128
|
+
const src = uri !== null && uri !== void 0 ? uri : shown === null || shown === void 0 ? void 0 : shown.src;
|
|
129
|
+
return src ? getSize(src) : {
|
|
130
|
+
width: 0,
|
|
131
|
+
height: 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function getSize(uri) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
react_native.Image.getSize(uri, (width, height) => resolve({
|
|
137
|
+
width,
|
|
138
|
+
height
|
|
139
|
+
}), () => resolve({
|
|
140
|
+
width: 0,
|
|
141
|
+
height: 0
|
|
142
|
+
}));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const FastImage = FastImageComponent;
|
|
146
|
+
FastImage.resizeMode = resizeMode;
|
|
147
|
+
FastImage.cacheControl = cacheControl;
|
|
148
|
+
FastImage.priority = priority;
|
|
149
|
+
FastImage.preload = (sources) => Promise.all(sources.map((source) => {
|
|
150
|
+
const uri = source ? source.uri : void 0;
|
|
151
|
+
if (typeof uri !== "string" || !uri) return Promise.resolve({
|
|
152
|
+
ok: false,
|
|
153
|
+
error: "Invalid source: no uri",
|
|
154
|
+
uri
|
|
155
|
+
});
|
|
156
|
+
return react_native.Image.prefetch(uri).then(() => getSize(uri).then((size) => ({
|
|
157
|
+
ok: true,
|
|
158
|
+
uri,
|
|
159
|
+
...size
|
|
160
|
+
})), () => ({
|
|
161
|
+
ok: false,
|
|
162
|
+
error: "Failed to load the image",
|
|
163
|
+
uri
|
|
164
|
+
}));
|
|
165
|
+
}));
|
|
166
|
+
FastImage.clearMemoryCache = () => Promise.resolve();
|
|
167
|
+
FastImage.clearDiskCache = () => Promise.resolve();
|
|
168
|
+
FastImage.configureCache = () => Promise.resolve({});
|
|
169
|
+
FastImage.writeToCache = () => Promise.resolve(notSupported);
|
|
170
|
+
FastImage.getCachePath = () => Promise.resolve(notSupported);
|
|
171
|
+
const styles = react_native.StyleSheet.create({
|
|
172
|
+
container: { overflow: "hidden" },
|
|
173
|
+
image: {
|
|
174
|
+
position: "absolute",
|
|
175
|
+
top: 0,
|
|
176
|
+
left: 0,
|
|
177
|
+
width: "100%",
|
|
178
|
+
height: "100%"
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const FastImageWithRef = FastImageComponent;
|
|
182
|
+
const FastImageBackground = (0, react.forwardRef)(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ react.default.createElement(react_native.View, {
|
|
183
|
+
style,
|
|
184
|
+
ref
|
|
185
|
+
}, /* @__PURE__ */ react.default.createElement(FastImageWithRef, {
|
|
186
|
+
...props,
|
|
187
|
+
style: [react_native.StyleSheet.absoluteFill, imageStyle],
|
|
188
|
+
ref: imageRef
|
|
189
|
+
}), children));
|
|
190
|
+
FastImageBackground.displayName = "FastImageBackground";
|
|
191
|
+
//#endregion
|
|
192
|
+
exports.FastImageBackground = FastImageBackground;
|
|
193
|
+
exports.default = FastImage;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes'
|
|
4
|
+
import type { SyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes'
|
|
5
|
+
|
|
6
|
+
export type OnLoadEvent = SyntheticEvent<
|
|
7
|
+
$ReadOnly<{
|
|
8
|
+
width: number,
|
|
9
|
+
height: number,
|
|
10
|
+
// The view's React tag. Missing on Android with the legacy architecture.
|
|
11
|
+
// TODO: make it required once the New Architecture is the minimum.
|
|
12
|
+
target?: number,
|
|
13
|
+
}>,
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export type OnErrorEvent = SyntheticEvent<
|
|
17
|
+
$ReadOnly<{|
|
|
18
|
+
error: string,
|
|
19
|
+
|}>,
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
export type OnProgressEvent = SyntheticEvent<
|
|
23
|
+
$ReadOnly<{|
|
|
24
|
+
loaded: number,
|
|
25
|
+
total: number,
|
|
26
|
+
// loaded / total, from 0 to 1.
|
|
27
|
+
progress: number,
|
|
28
|
+
|}>,
|
|
29
|
+
>
|
|
30
|
+
|
|
31
|
+
export type ResizeMode = $ReadOnly<{|
|
|
32
|
+
contain: 'contain',
|
|
33
|
+
cover: 'cover',
|
|
34
|
+
stretch: 'stretch',
|
|
35
|
+
center: 'center',
|
|
36
|
+
|}>
|
|
37
|
+
|
|
38
|
+
export type Priority = $ReadOnly<{|
|
|
39
|
+
low: 'low',
|
|
40
|
+
normal: 'normal',
|
|
41
|
+
high: 'high',
|
|
42
|
+
|}>
|
|
43
|
+
|
|
44
|
+
export type CacheControl = $ReadOnly<{|
|
|
45
|
+
immutable: 'immutable',
|
|
46
|
+
web: 'web',
|
|
47
|
+
cacheOnly: 'cacheOnly',
|
|
48
|
+
|}>
|
|
49
|
+
|
|
50
|
+
export type ResizeModes = $Values<ResizeMode>
|
|
51
|
+
export type Priorities = $Values<Priority>
|
|
52
|
+
export type CacheControls = $Values<CacheControl>
|
|
53
|
+
|
|
54
|
+
export type LoadResult =
|
|
55
|
+
| {| ok: true, width: number, height: number |}
|
|
56
|
+
| {| ok: false, error: string |}
|
|
57
|
+
|
|
58
|
+
export type CacheLimits = {|
|
|
59
|
+
maxDiskSize?: ?number,
|
|
60
|
+
maxDiskAge?: ?number,
|
|
61
|
+
maxMemorySize?: ?number,
|
|
62
|
+
|}
|
|
63
|
+
|
|
64
|
+
export type CacheState = {|
|
|
65
|
+
maxDiskSize?: number,
|
|
66
|
+
maxDiskAge?: number,
|
|
67
|
+
maxMemorySize?: number,
|
|
68
|
+
diskSize?: number,
|
|
69
|
+
|}
|
|
70
|
+
|
|
71
|
+
export type CachePathResult =
|
|
72
|
+
| {| ok: true, path: string |}
|
|
73
|
+
| {| ok: false, error: string |}
|
|
74
|
+
|
|
75
|
+
export type Transition = $ReadOnly<{|
|
|
76
|
+
duration?: ?number,
|
|
77
|
+
betweenImages?: ?boolean,
|
|
78
|
+
skipOnCacheHit?: ?('none' | 'memory' | 'all'),
|
|
79
|
+
|}>
|
|
80
|
+
|
|
81
|
+
export type PreloadSuccess = {|
|
|
82
|
+
uri: string,
|
|
83
|
+
ok: true,
|
|
84
|
+
width: number,
|
|
85
|
+
height: number,
|
|
86
|
+
|}
|
|
87
|
+
|
|
88
|
+
export type PreloadFailure = {|
|
|
89
|
+
uri?: string,
|
|
90
|
+
ok: false,
|
|
91
|
+
error: string,
|
|
92
|
+
|}
|
|
93
|
+
|
|
94
|
+
export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
95
|
+
|
|
96
|
+
export type PreloadFn = (
|
|
97
|
+
sources: Array<FastImageSource>,
|
|
98
|
+
) => Promise<Array<PreloadResult>>
|
|
99
|
+
export type FastImageSource = {
|
|
100
|
+
uri?: string,
|
|
101
|
+
headers?: Object,
|
|
102
|
+
priority?: Priorities,
|
|
103
|
+
cache?: CacheControls,
|
|
104
|
+
cacheKey?: string,
|
|
105
|
+
memoryCache?: boolean,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type FastImageProps = $ReadOnly<{|
|
|
109
|
+
...ViewProps,
|
|
110
|
+
onError?: ?(event: OnErrorEvent) => void,
|
|
111
|
+
onLoad?: ?(event: OnLoadEvent) => void,
|
|
112
|
+
onLoadEnd?: ?(result: LoadResult) => void,
|
|
113
|
+
onLoadStart?: ?() => void,
|
|
114
|
+
onProgress?: ?(event: OnProgressEvent) => void,
|
|
115
|
+
|
|
116
|
+
source?: ?(FastImageSource | number),
|
|
117
|
+
defaultSource?: ?number,
|
|
118
|
+
|
|
119
|
+
tintColor?: number | string,
|
|
120
|
+
resizeMode?: ?ResizeModes,
|
|
121
|
+
fallback?: ?boolean,
|
|
122
|
+
recyclingKey?: ?string,
|
|
123
|
+
loop?: ?(boolean | number),
|
|
124
|
+
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
125
|
+
paused?: ?boolean,
|
|
126
|
+
transition?: ?(boolean | number | Transition),
|
|
127
|
+
downsample?: ?boolean,
|
|
128
|
+
blurRadius?: ?number,
|
|
129
|
+
testID?: ?string,
|
|
130
|
+
|}>
|
|
131
|
+
|
|
132
|
+
export type FastImageBackgroundProps = $ReadOnly<{|
|
|
133
|
+
...FastImageProps,
|
|
134
|
+
imageStyle?: $PropertyType<ViewProps, 'style'>,
|
|
135
|
+
imageRef?: React$Ref<any>,
|
|
136
|
+
children?: React$Node,
|
|
137
|
+
|}>
|
|
138
|
+
|
|
139
|
+
declare export var FastImageBackground: React$ComponentType<FastImageBackgroundProps>
|
|
140
|
+
|
|
141
|
+
declare export default class FastImage extends React$Component<FastImageProps> {
|
|
142
|
+
static resizeMode: ResizeMode;
|
|
143
|
+
static priority: Priority;
|
|
144
|
+
static cacheControl: CacheControl;
|
|
145
|
+
static preload: PreloadFn;
|
|
146
|
+
static clearMemoryCache: () => Promise<void>;
|
|
147
|
+
static clearDiskCache: () => Promise<void>;
|
|
148
|
+
static getCachePath: (source: FastImageSource) => Promise<CachePathResult>;
|
|
149
|
+
static configureCache: (limits?: CacheLimits) => Promise<CacheState>;
|
|
150
|
+
static writeToCache: (
|
|
151
|
+
source: FastImageSource,
|
|
152
|
+
file: string,
|
|
153
|
+
) => Promise<CachePathResult>;
|
|
154
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { LayoutChangeEvent, StyleProp, ViewStyle, ImageRequireSource, AccessibilityProps, ViewProps } from 'react-native';
|
|
3
|
+
import { cacheControl, priority, resizeMode } from './constants';
|
|
3
4
|
type ColorValue = NonNullable<ViewStyle['backgroundColor']>;
|
|
4
5
|
export type ResizeMode = 'contain' | 'cover' | 'stretch' | 'center';
|
|
5
|
-
declare const resizeMode: {
|
|
6
|
-
readonly contain: 'contain';
|
|
7
|
-
readonly cover: 'cover';
|
|
8
|
-
readonly stretch: 'stretch';
|
|
9
|
-
readonly center: 'center';
|
|
10
|
-
};
|
|
11
6
|
export type Priority = 'low' | 'normal' | 'high';
|
|
12
|
-
declare const priority: {
|
|
13
|
-
readonly low: 'low';
|
|
14
|
-
readonly normal: 'normal';
|
|
15
|
-
readonly high: 'high';
|
|
16
|
-
};
|
|
17
7
|
export type Cache = 'immutable' | 'web' | 'cacheOnly';
|
|
18
|
-
declare const cacheControl: {
|
|
19
|
-
readonly immutable: 'immutable';
|
|
20
|
-
readonly web: 'web';
|
|
21
|
-
readonly cacheOnly: 'cacheOnly';
|
|
22
|
-
};
|
|
23
8
|
export type Source = {
|
|
24
9
|
uri?: string;
|
|
25
10
|
headers?: {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { forwardRef, memo, useRef } from "react";
|
|
2
2
|
import { Image, NativeModules, Platform, StyleSheet, View, requireNativeComponent } from "react-native";
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/constants.ts
|
|
4
4
|
const resizeMode = {
|
|
5
5
|
contain: "contain",
|
|
6
6
|
cover: "cover",
|
|
@@ -17,6 +17,8 @@ const cacheControl = {
|
|
|
17
17
|
web: "web",
|
|
18
18
|
cacheOnly: "cacheOnly"
|
|
19
19
|
};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/index.tsx
|
|
20
22
|
const resolveDefaultSource = (defaultSource) => {
|
|
21
23
|
if (!defaultSource) return null;
|
|
22
24
|
if (Platform.OS === "android") {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { FastImageBackgroundProps, FastImageProps, FastImageStaticProperties } from './index';
|
|
3
|
+
export type * from './index';
|
|
4
|
+
declare const FastImage: React.ComponentType<FastImageProps> & FastImageStaticProperties;
|
|
5
|
+
export declare const FastImageBackground: React.ComponentType<FastImageBackgroundProps>;
|
|
6
|
+
export default FastImage;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import React, { forwardRef, memo, useRef } from "react";
|
|
2
|
+
import { Image, StyleSheet, View } from "react-native";
|
|
3
|
+
//#region src/constants.ts
|
|
4
|
+
const resizeMode = {
|
|
5
|
+
contain: "contain",
|
|
6
|
+
cover: "cover",
|
|
7
|
+
stretch: "stretch",
|
|
8
|
+
center: "center"
|
|
9
|
+
};
|
|
10
|
+
const priority = {
|
|
11
|
+
low: "low",
|
|
12
|
+
normal: "normal",
|
|
13
|
+
high: "high"
|
|
14
|
+
};
|
|
15
|
+
const cacheControl = {
|
|
16
|
+
immutable: "immutable",
|
|
17
|
+
web: "web",
|
|
18
|
+
cacheOnly: "cacheOnly"
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/index.web.tsx
|
|
22
|
+
const notSupported = {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: "Not supported on the web"
|
|
25
|
+
};
|
|
26
|
+
function FastImageBase({ source, defaultSource, tintColor, blurRadius, onLoadStart, onLoad, onError, onLoadEnd, style, children, resizeMode: mode = "cover", forwardedRef, onLayout, pointerEvents, fallback: _fallback, recyclingKey: _recyclingKey, loop: _loop, imageRendering: _imageRendering, paused: _paused, transition: _transition, downsample: _downsample, onProgress: _onProgress, ...props }) {
|
|
27
|
+
const result = useRef(void 0);
|
|
28
|
+
const sent = useRef(Promise.resolve());
|
|
29
|
+
const image = useRef(null);
|
|
30
|
+
const uri = typeof source === "object" && source ? source.uri : void 0;
|
|
31
|
+
const webSource = typeof source === "number" ? source : uri ? { uri } : void 0;
|
|
32
|
+
return /* @__PURE__ */ React.createElement(View, {
|
|
33
|
+
style: [styles.container, style],
|
|
34
|
+
onLayout,
|
|
35
|
+
pointerEvents,
|
|
36
|
+
ref: forwardedRef
|
|
37
|
+
}, /* @__PURE__ */ React.createElement(Image, {
|
|
38
|
+
...props,
|
|
39
|
+
style: styles.image,
|
|
40
|
+
source: webSource,
|
|
41
|
+
defaultSource,
|
|
42
|
+
resizeMode: mode,
|
|
43
|
+
tintColor,
|
|
44
|
+
blurRadius,
|
|
45
|
+
ref: image,
|
|
46
|
+
onLoadStart,
|
|
47
|
+
onLoad: (event) => {
|
|
48
|
+
var _event$nativeEvent;
|
|
49
|
+
sent.current = loadedSize(event === null || event === void 0 || (_event$nativeEvent = event.nativeEvent) === null || _event$nativeEvent === void 0 ? void 0 : _event$nativeEvent.target, image.current, uri).then(({ width, height }) => {
|
|
50
|
+
result.current = {
|
|
51
|
+
ok: true,
|
|
52
|
+
width,
|
|
53
|
+
height
|
|
54
|
+
};
|
|
55
|
+
onLoad === null || onLoad === void 0 || onLoad({ nativeEvent: {
|
|
56
|
+
width,
|
|
57
|
+
height
|
|
58
|
+
} });
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
onError: (event) => {
|
|
62
|
+
var _event$nativeEvent$er, _event$nativeEvent2;
|
|
63
|
+
const error = String((_event$nativeEvent$er = event === null || event === void 0 || (_event$nativeEvent2 = event.nativeEvent) === null || _event$nativeEvent2 === void 0 ? void 0 : _event$nativeEvent2.error) !== null && _event$nativeEvent$er !== void 0 ? _event$nativeEvent$er : "Failed to load the image");
|
|
64
|
+
result.current = {
|
|
65
|
+
ok: false,
|
|
66
|
+
error
|
|
67
|
+
};
|
|
68
|
+
sent.current = Promise.resolve();
|
|
69
|
+
onError === null || onError === void 0 || onError({ nativeEvent: { error } });
|
|
70
|
+
},
|
|
71
|
+
onLoadEnd: () => {
|
|
72
|
+
sent.current.then(() => {
|
|
73
|
+
var _result$current;
|
|
74
|
+
onLoadEnd === null || onLoadEnd === void 0 || onLoadEnd((_result$current = result.current) !== null && _result$current !== void 0 ? _result$current : {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: "Failed to load the image"
|
|
77
|
+
});
|
|
78
|
+
result.current = void 0;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}), children);
|
|
82
|
+
}
|
|
83
|
+
const FastImageMemo = memo(FastImageBase);
|
|
84
|
+
const FastImageComponent = forwardRef((props, ref) => /* @__PURE__ */ React.createElement(FastImageMemo, {
|
|
85
|
+
forwardedRef: ref,
|
|
86
|
+
...props
|
|
87
|
+
}));
|
|
88
|
+
FastImageComponent.displayName = "FastImage";
|
|
89
|
+
async function loadedSize(loaded, view, uri) {
|
|
90
|
+
var _view$querySelector;
|
|
91
|
+
if (loaded === null || loaded === void 0 ? void 0 : loaded.naturalWidth) return {
|
|
92
|
+
width: loaded.naturalWidth,
|
|
93
|
+
height: loaded.naturalHeight
|
|
94
|
+
};
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
96
|
+
const shown = view === null || view === void 0 || (_view$querySelector = view.querySelector) === null || _view$querySelector === void 0 ? void 0 : _view$querySelector.call(view, "img");
|
|
97
|
+
if (shown === null || shown === void 0 ? void 0 : shown.naturalWidth) return {
|
|
98
|
+
width: shown.naturalWidth,
|
|
99
|
+
height: shown.naturalHeight
|
|
100
|
+
};
|
|
101
|
+
const src = uri !== null && uri !== void 0 ? uri : shown === null || shown === void 0 ? void 0 : shown.src;
|
|
102
|
+
return src ? getSize(src) : {
|
|
103
|
+
width: 0,
|
|
104
|
+
height: 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function getSize(uri) {
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
Image.getSize(uri, (width, height) => resolve({
|
|
110
|
+
width,
|
|
111
|
+
height
|
|
112
|
+
}), () => resolve({
|
|
113
|
+
width: 0,
|
|
114
|
+
height: 0
|
|
115
|
+
}));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const FastImage = FastImageComponent;
|
|
119
|
+
FastImage.resizeMode = resizeMode;
|
|
120
|
+
FastImage.cacheControl = cacheControl;
|
|
121
|
+
FastImage.priority = priority;
|
|
122
|
+
FastImage.preload = (sources) => Promise.all(sources.map((source) => {
|
|
123
|
+
const uri = source ? source.uri : void 0;
|
|
124
|
+
if (typeof uri !== "string" || !uri) return Promise.resolve({
|
|
125
|
+
ok: false,
|
|
126
|
+
error: "Invalid source: no uri",
|
|
127
|
+
uri
|
|
128
|
+
});
|
|
129
|
+
return Image.prefetch(uri).then(() => getSize(uri).then((size) => ({
|
|
130
|
+
ok: true,
|
|
131
|
+
uri,
|
|
132
|
+
...size
|
|
133
|
+
})), () => ({
|
|
134
|
+
ok: false,
|
|
135
|
+
error: "Failed to load the image",
|
|
136
|
+
uri
|
|
137
|
+
}));
|
|
138
|
+
}));
|
|
139
|
+
FastImage.clearMemoryCache = () => Promise.resolve();
|
|
140
|
+
FastImage.clearDiskCache = () => Promise.resolve();
|
|
141
|
+
FastImage.configureCache = () => Promise.resolve({});
|
|
142
|
+
FastImage.writeToCache = () => Promise.resolve(notSupported);
|
|
143
|
+
FastImage.getCachePath = () => Promise.resolve(notSupported);
|
|
144
|
+
const styles = StyleSheet.create({
|
|
145
|
+
container: { overflow: "hidden" },
|
|
146
|
+
image: {
|
|
147
|
+
position: "absolute",
|
|
148
|
+
top: 0,
|
|
149
|
+
left: 0,
|
|
150
|
+
width: "100%",
|
|
151
|
+
height: "100%"
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const FastImageWithRef = FastImageComponent;
|
|
155
|
+
const FastImageBackground = forwardRef(({ style, imageStyle, imageRef, children, ...props }, ref) => /* @__PURE__ */ React.createElement(View, {
|
|
156
|
+
style,
|
|
157
|
+
ref
|
|
158
|
+
}, /* @__PURE__ */ React.createElement(FastImageWithRef, {
|
|
159
|
+
...props,
|
|
160
|
+
style: [StyleSheet.absoluteFill, imageStyle],
|
|
161
|
+
ref: imageRef
|
|
162
|
+
}), children));
|
|
163
|
+
FastImageBackground.displayName = "FastImageBackground";
|
|
164
|
+
//#endregion
|
|
165
|
+
export { FastImageBackground, FastImage as default };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import type { ViewProps } from 'react-native/Libraries/Components/View/ViewPropTypes'
|
|
4
|
+
import type { SyntheticEvent } from 'react-native/Libraries/Types/CoreEventTypes'
|
|
5
|
+
|
|
6
|
+
export type OnLoadEvent = SyntheticEvent<
|
|
7
|
+
$ReadOnly<{
|
|
8
|
+
width: number,
|
|
9
|
+
height: number,
|
|
10
|
+
// The view's React tag. Missing on Android with the legacy architecture.
|
|
11
|
+
// TODO: make it required once the New Architecture is the minimum.
|
|
12
|
+
target?: number,
|
|
13
|
+
}>,
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
export type OnErrorEvent = SyntheticEvent<
|
|
17
|
+
$ReadOnly<{|
|
|
18
|
+
error: string,
|
|
19
|
+
|}>,
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
export type OnProgressEvent = SyntheticEvent<
|
|
23
|
+
$ReadOnly<{|
|
|
24
|
+
loaded: number,
|
|
25
|
+
total: number,
|
|
26
|
+
// loaded / total, from 0 to 1.
|
|
27
|
+
progress: number,
|
|
28
|
+
|}>,
|
|
29
|
+
>
|
|
30
|
+
|
|
31
|
+
export type ResizeMode = $ReadOnly<{|
|
|
32
|
+
contain: 'contain',
|
|
33
|
+
cover: 'cover',
|
|
34
|
+
stretch: 'stretch',
|
|
35
|
+
center: 'center',
|
|
36
|
+
|}>
|
|
37
|
+
|
|
38
|
+
export type Priority = $ReadOnly<{|
|
|
39
|
+
low: 'low',
|
|
40
|
+
normal: 'normal',
|
|
41
|
+
high: 'high',
|
|
42
|
+
|}>
|
|
43
|
+
|
|
44
|
+
export type CacheControl = $ReadOnly<{|
|
|
45
|
+
immutable: 'immutable',
|
|
46
|
+
web: 'web',
|
|
47
|
+
cacheOnly: 'cacheOnly',
|
|
48
|
+
|}>
|
|
49
|
+
|
|
50
|
+
export type ResizeModes = $Values<ResizeMode>
|
|
51
|
+
export type Priorities = $Values<Priority>
|
|
52
|
+
export type CacheControls = $Values<CacheControl>
|
|
53
|
+
|
|
54
|
+
export type LoadResult =
|
|
55
|
+
| {| ok: true, width: number, height: number |}
|
|
56
|
+
| {| ok: false, error: string |}
|
|
57
|
+
|
|
58
|
+
export type CacheLimits = {|
|
|
59
|
+
maxDiskSize?: ?number,
|
|
60
|
+
maxDiskAge?: ?number,
|
|
61
|
+
maxMemorySize?: ?number,
|
|
62
|
+
|}
|
|
63
|
+
|
|
64
|
+
export type CacheState = {|
|
|
65
|
+
maxDiskSize?: number,
|
|
66
|
+
maxDiskAge?: number,
|
|
67
|
+
maxMemorySize?: number,
|
|
68
|
+
diskSize?: number,
|
|
69
|
+
|}
|
|
70
|
+
|
|
71
|
+
export type CachePathResult =
|
|
72
|
+
| {| ok: true, path: string |}
|
|
73
|
+
| {| ok: false, error: string |}
|
|
74
|
+
|
|
75
|
+
export type Transition = $ReadOnly<{|
|
|
76
|
+
duration?: ?number,
|
|
77
|
+
betweenImages?: ?boolean,
|
|
78
|
+
skipOnCacheHit?: ?('none' | 'memory' | 'all'),
|
|
79
|
+
|}>
|
|
80
|
+
|
|
81
|
+
export type PreloadSuccess = {|
|
|
82
|
+
uri: string,
|
|
83
|
+
ok: true,
|
|
84
|
+
width: number,
|
|
85
|
+
height: number,
|
|
86
|
+
|}
|
|
87
|
+
|
|
88
|
+
export type PreloadFailure = {|
|
|
89
|
+
uri?: string,
|
|
90
|
+
ok: false,
|
|
91
|
+
error: string,
|
|
92
|
+
|}
|
|
93
|
+
|
|
94
|
+
export type PreloadResult = PreloadSuccess | PreloadFailure
|
|
95
|
+
|
|
96
|
+
export type PreloadFn = (
|
|
97
|
+
sources: Array<FastImageSource>,
|
|
98
|
+
) => Promise<Array<PreloadResult>>
|
|
99
|
+
export type FastImageSource = {
|
|
100
|
+
uri?: string,
|
|
101
|
+
headers?: Object,
|
|
102
|
+
priority?: Priorities,
|
|
103
|
+
cache?: CacheControls,
|
|
104
|
+
cacheKey?: string,
|
|
105
|
+
memoryCache?: boolean,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type FastImageProps = $ReadOnly<{|
|
|
109
|
+
...ViewProps,
|
|
110
|
+
onError?: ?(event: OnErrorEvent) => void,
|
|
111
|
+
onLoad?: ?(event: OnLoadEvent) => void,
|
|
112
|
+
onLoadEnd?: ?(result: LoadResult) => void,
|
|
113
|
+
onLoadStart?: ?() => void,
|
|
114
|
+
onProgress?: ?(event: OnProgressEvent) => void,
|
|
115
|
+
|
|
116
|
+
source?: ?(FastImageSource | number),
|
|
117
|
+
defaultSource?: ?number,
|
|
118
|
+
|
|
119
|
+
tintColor?: number | string,
|
|
120
|
+
resizeMode?: ?ResizeModes,
|
|
121
|
+
fallback?: ?boolean,
|
|
122
|
+
recyclingKey?: ?string,
|
|
123
|
+
loop?: ?(boolean | number),
|
|
124
|
+
imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
|
|
125
|
+
paused?: ?boolean,
|
|
126
|
+
transition?: ?(boolean | number | Transition),
|
|
127
|
+
downsample?: ?boolean,
|
|
128
|
+
blurRadius?: ?number,
|
|
129
|
+
testID?: ?string,
|
|
130
|
+
|}>
|
|
131
|
+
|
|
132
|
+
export type FastImageBackgroundProps = $ReadOnly<{|
|
|
133
|
+
...FastImageProps,
|
|
134
|
+
imageStyle?: $PropertyType<ViewProps, 'style'>,
|
|
135
|
+
imageRef?: React$Ref<any>,
|
|
136
|
+
children?: React$Node,
|
|
137
|
+
|}>
|
|
138
|
+
|
|
139
|
+
declare export var FastImageBackground: React$ComponentType<FastImageBackgroundProps>
|
|
140
|
+
|
|
141
|
+
declare export default class FastImage extends React$Component<FastImageProps> {
|
|
142
|
+
static resizeMode: ResizeMode;
|
|
143
|
+
static priority: Priority;
|
|
144
|
+
static cacheControl: CacheControl;
|
|
145
|
+
static preload: PreloadFn;
|
|
146
|
+
static clearMemoryCache: () => Promise<void>;
|
|
147
|
+
static clearDiskCache: () => Promise<void>;
|
|
148
|
+
static getCachePath: (source: FastImageSource) => Promise<CachePathResult>;
|
|
149
|
+
static configureCache: (limits?: CacheLimits) => Promise<CacheState>;
|
|
150
|
+
static writeToCache: (
|
|
151
|
+
source: FastImageSource,
|
|
152
|
+
file: string,
|
|
153
|
+
) => Promise<CachePathResult>;
|
|
154
|
+
}
|
|
@@ -89,7 +89,12 @@ static CGSize FFFPixelSize(NSData* data) {
|
|
|
89
89
|
// After a load that isn't downsampled (e.g. a preload), this class
|
|
90
90
|
// wouldn't be used, and the box would be decoded as SDWebImage does (to
|
|
91
91
|
// fill it, stretched). With its own url, a downsampled load only shares
|
|
92
|
-
// downloads with other downsampled ones.
|
|
92
|
+
// downloads with other downsampled ones. Not a photo library url, which
|
|
93
|
+
// isn't downloaded, and whose loader reads everything after ph:// as the
|
|
94
|
+
// photo's identifier.
|
|
95
|
+
if ([url.scheme isEqualToString: @"ph"]) {
|
|
96
|
+
return url;
|
|
97
|
+
}
|
|
93
98
|
NSURLComponents* components = [NSURLComponents componentsWithURL: url resolvingAgainstBaseURL: NO];
|
|
94
99
|
if (!components) {
|
|
95
100
|
return url;
|
|
@@ -53,9 +53,18 @@ typedef NS_ENUM(NSInteger, FFFCacheControl) {
|
|
|
53
53
|
- (SDWebImageOptions)cacheOptions;
|
|
54
54
|
|
|
55
55
|
// The loader for `cache: 'web'` images, whose HTTP cache is their own (see
|
|
56
|
-
// webURLCache),
|
|
56
|
+
// webURLCache), for photo library images (ph://, see FFFPhotosLoader), or nil
|
|
57
|
+
// for SDWebImage's shared one.
|
|
57
58
|
- (id<SDImageLoader>)imageLoader;
|
|
58
59
|
|
|
60
|
+
// Whether the source is a photo library image (ph://<localIdentifier>),
|
|
61
|
+
// which is decoded at about the view's size.
|
|
62
|
+
- (BOOL)isPhotoLibrary;
|
|
63
|
+
|
|
64
|
+
// A photo library image's own size in pixels (for onLoad when it comes from
|
|
65
|
+
// a cache, where the size it was decoded from isn't recorded), or zero.
|
|
66
|
+
- (CGSize)photoPixelSize;
|
|
67
|
+
|
|
59
68
|
// The HTTP cache of `cache: 'web'` images: their own rather than the app's
|
|
60
69
|
// shared NSURLCache, so clearDiskCache can empty it without the app's other
|
|
61
70
|
// responses. 50 MB, as on Android.
|
|
@@ -5,9 +5,150 @@
|
|
|
5
5
|
#import <SDWebImage/SDWebImageError.h>
|
|
6
6
|
#import <SDWebImage/SDWebImageDownloaderOperation.h>
|
|
7
7
|
#import <SDWebImage/NSData+ImageContentType.h>
|
|
8
|
+
#import <objc/message.h>
|
|
8
9
|
|
|
9
10
|
static NSUInteger const FFFWebCacheSize = 50 * 1024 * 1024;
|
|
10
11
|
|
|
12
|
+
// The photo's own size in pixels, looked up by its identifier (the url after
|
|
13
|
+
// ph://), or zero. Through the runtime, as the Photos framework is only there
|
|
14
|
+
// when the app links it (with SDWebImagePhotosPlugin). Kept once looked up.
|
|
15
|
+
static CGSize FFFLookUpPhotoPixelSize(NSURL *url)
|
|
16
|
+
{
|
|
17
|
+
Class assetClass = NSClassFromString(@"PHAsset");
|
|
18
|
+
SEL fetch = NSSelectorFromString(@"fetchAssetsWithLocalIdentifiers:options:");
|
|
19
|
+
NSString *prefix = @"ph://";
|
|
20
|
+
NSString *string = url.absoluteString;
|
|
21
|
+
if (!assetClass || ![assetClass respondsToSelector:fetch] || ![string hasPrefix:prefix]) {
|
|
22
|
+
return CGSizeZero;
|
|
23
|
+
}
|
|
24
|
+
id (*fetchAssets)(id, SEL, NSArray *, id) = (void *)objc_msgSend;
|
|
25
|
+
id result = fetchAssets(assetClass, fetch, @[[string substringFromIndex:prefix.length]], nil);
|
|
26
|
+
id asset = [result respondsToSelector:@selector(firstObject)] ? [result firstObject] : nil;
|
|
27
|
+
if (!asset) {
|
|
28
|
+
return CGSizeZero;
|
|
29
|
+
}
|
|
30
|
+
return CGSizeMake([[asset valueForKey:@"pixelWidth"] doubleValue], [[asset valueForKey:@"pixelHeight"] doubleValue]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static NSCache<NSString *, NSValue *> *FFFPhotoSizes(void)
|
|
34
|
+
{
|
|
35
|
+
static NSCache *sizes;
|
|
36
|
+
static dispatch_once_t once;
|
|
37
|
+
dispatch_once(&once, ^{
|
|
38
|
+
sizes = [NSCache new];
|
|
39
|
+
});
|
|
40
|
+
return sizes;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static CGSize FFFPhotoPixelSize(NSURL *url)
|
|
44
|
+
{
|
|
45
|
+
NSString *key = url.absoluteString;
|
|
46
|
+
NSValue *known = key ? [FFFPhotoSizes() objectForKey:key] : nil;
|
|
47
|
+
if (known) {
|
|
48
|
+
return known.CGSizeValue;
|
|
49
|
+
}
|
|
50
|
+
CGSize size = FFFLookUpPhotoPixelSize(url);
|
|
51
|
+
if (key && size.width > 0 && size.height > 0) {
|
|
52
|
+
[FFFPhotoSizes() setObject:[NSValue valueWithCGSize:size] forKey:key];
|
|
53
|
+
}
|
|
54
|
+
return size;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Loads photo library images (ph://<localIdentifier>) with
|
|
58
|
+
// SDWebImagePhotosPlugin's loader when the app has it (the pod): FastImage
|
|
59
|
+
// finds it at runtime, so it doesn't depend on it or on the Photos framework.
|
|
60
|
+
// Its own instance, so the app's use of the plugin is left as it is. Without
|
|
61
|
+
// the plugin, or for a source it can't load (assets-library://), the load
|
|
62
|
+
// fails with `failure`.
|
|
63
|
+
@interface FFFPhotosLoader : NSObject <SDImageLoader>
|
|
64
|
+
@property (nonatomic, strong) id<SDImageLoader> plugin;
|
|
65
|
+
@property (nonatomic, copy) NSString *failure;
|
|
66
|
+
@end
|
|
67
|
+
|
|
68
|
+
@implementation FFFPhotosLoader
|
|
69
|
+
|
|
70
|
+
+ (FFFPhotosLoader *)photosLoader
|
|
71
|
+
{
|
|
72
|
+
static FFFPhotosLoader *loader;
|
|
73
|
+
static dispatch_once_t once;
|
|
74
|
+
dispatch_once(&once, ^{
|
|
75
|
+
loader = [FFFPhotosLoader new];
|
|
76
|
+
Class pluginClass = NSClassFromString(@"SDImagePhotosLoader");
|
|
77
|
+
id<SDImageLoader> plugin = pluginClass ? [pluginClass new] : nil;
|
|
78
|
+
if ([plugin conformsToProtocol:@protocol(SDImageLoader)]) {
|
|
79
|
+
// One image per load, at full quality. By default the plugin
|
|
80
|
+
// first sends a quick low quality one, which a view would show
|
|
81
|
+
// (and fade in) before the one it asked for.
|
|
82
|
+
// PHImageRequestOptionsDeliveryModeHighQualityFormat. Skipped if
|
|
83
|
+
// a later version of the plugin renames these.
|
|
84
|
+
NSObject *requestOptions = [(NSObject *)plugin respondsToSelector:NSSelectorFromString(@"imageRequestOptions")]
|
|
85
|
+
? [(NSObject *)plugin valueForKey:@"imageRequestOptions"]
|
|
86
|
+
: nil;
|
|
87
|
+
if ([requestOptions respondsToSelector:NSSelectorFromString(@"setDeliveryMode:")]) {
|
|
88
|
+
[requestOptions setValue:@1 forKey:@"deliveryMode"];
|
|
89
|
+
}
|
|
90
|
+
loader.plugin = plugin;
|
|
91
|
+
} else {
|
|
92
|
+
loader.failure = @"Photo library images (ph://) need SDWebImagePhotosPlugin: add pod 'SDWebImagePhotosPlugin' to the app's Podfile";
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return loader;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
+ (FFFPhotosLoader *)assetsLibraryLoader
|
|
99
|
+
{
|
|
100
|
+
static FFFPhotosLoader *loader;
|
|
101
|
+
static dispatch_once_t once;
|
|
102
|
+
dispatch_once(&once, ^{
|
|
103
|
+
loader = [FFFPhotosLoader new];
|
|
104
|
+
loader.failure = @"assets-library:// urls aren't supported: use the photo's ph:// url (with SDWebImagePhotosPlugin)";
|
|
105
|
+
});
|
|
106
|
+
return loader;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
- (BOOL)canRequestImageForURL:(NSURL *)url
|
|
110
|
+
{
|
|
111
|
+
return YES;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url
|
|
115
|
+
options:(SDWebImageOptions)options
|
|
116
|
+
context:(SDWebImageContext *)context
|
|
117
|
+
progress:(SDImageLoaderProgressBlock)progressBlock
|
|
118
|
+
completed:(SDImageLoaderCompletedBlock)completedBlock
|
|
119
|
+
{
|
|
120
|
+
if (!self.plugin) {
|
|
121
|
+
if (completedBlock) {
|
|
122
|
+
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey: self.failure}];
|
|
123
|
+
completedBlock(nil, nil, error, YES);
|
|
124
|
+
}
|
|
125
|
+
return nil;
|
|
126
|
+
}
|
|
127
|
+
return [self.plugin requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
|
|
128
|
+
if (!image || !completedBlock) {
|
|
129
|
+
if (completedBlock) {
|
|
130
|
+
completedBlock(image, data, error, finished);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Looks up the photo's own size for onLoad here, off the main thread
|
|
135
|
+
// (the image is decoded at about the view's size).
|
|
136
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
137
|
+
FFFPhotoPixelSize(url);
|
|
138
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
139
|
+
completedBlock(image, data, error, finished);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
- (BOOL)shouldBlockFailedURLWithURL:(NSURL *)url error:(NSError *)error
|
|
146
|
+
{
|
|
147
|
+
return self.plugin ? [self.plugin shouldBlockFailedURLWithURL:url error:error] : NO;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@end
|
|
151
|
+
|
|
11
152
|
// Downloads `cache: 'web'` images. The HTTP cache only stores a response whose
|
|
12
153
|
// body is an image: a page sent instead (e.g. a captive portal's, with status
|
|
13
154
|
// 200) would be served from there until it expired, and each load would fail.
|
|
@@ -75,8 +216,24 @@ static NSUInteger const FFFWebCacheSize = 50 * 1024 * 1024;
|
|
|
75
216
|
return 0;
|
|
76
217
|
}
|
|
77
218
|
|
|
219
|
+
- (BOOL)isPhotoLibrary
|
|
220
|
+
{
|
|
221
|
+
return [_url.scheme isEqualToString:@"ph"];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
- (CGSize)photoPixelSize
|
|
225
|
+
{
|
|
226
|
+
return [self isPhotoLibrary] ? FFFPhotoPixelSize(_url) : CGSizeZero;
|
|
227
|
+
}
|
|
228
|
+
|
|
78
229
|
- (id<SDImageLoader>)imageLoader
|
|
79
230
|
{
|
|
231
|
+
if ([self isPhotoLibrary]) {
|
|
232
|
+
return [FFFPhotosLoader photosLoader];
|
|
233
|
+
}
|
|
234
|
+
if ([_url.scheme isEqualToString:@"assets-library"]) {
|
|
235
|
+
return [FFFPhotosLoader assetsLibraryLoader];
|
|
236
|
+
}
|
|
80
237
|
if (_cacheControl != FFFCacheControlWeb) {
|
|
81
238
|
return nil;
|
|
82
239
|
}
|
|
@@ -570,7 +570,9 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
570
570
|
|
|
571
571
|
- (void) sendOnLoad: (UIImage*)image {
|
|
572
572
|
// The full image's size, also when it was decoded smaller.
|
|
573
|
-
|
|
573
|
+
// A photo library image's own size (it's decoded at about the view's
|
|
574
|
+
// size), looked up when it loaded.
|
|
575
|
+
CGSize size = [_source isPhotoLibrary] ? [_source photoPixelSize] : [FFFDownsampledImage sourceSizeOfImage: image];
|
|
574
576
|
if (CGSizeEqualToSize(size, CGSizeZero)) {
|
|
575
577
|
size = image.size;
|
|
576
578
|
}
|
|
@@ -646,11 +648,15 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
646
648
|
}
|
|
647
649
|
}
|
|
648
650
|
|
|
649
|
-
// Whether images are decoded at about the view's size (downsample
|
|
650
|
-
//
|
|
651
|
-
//
|
|
651
|
+
// Whether images are decoded at about the view's size (downsample, from
|
|
652
|
+
// SDWebImage 5.19, and always for photo library images, which are large and
|
|
653
|
+
// usually shown small: Photos makes them at the size asked for, on any
|
|
654
|
+
// version). Not for `repeat`, which tiles the image at its own size.
|
|
652
655
|
- (BOOL) downsamples {
|
|
653
|
-
|
|
656
|
+
if (_resizeMode == RCTResizeModeRepeat) {
|
|
657
|
+
return NO;
|
|
658
|
+
}
|
|
659
|
+
return [_source isPhotoLibrary] || (_downsample && [FFFDownsampledImage isSupported]);
|
|
654
660
|
}
|
|
655
661
|
|
|
656
662
|
// Whether the view has been laid out with an area. One that's 0 wide or tall
|
|
@@ -823,6 +829,13 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
823
829
|
}
|
|
824
830
|
NSString* key = _source.cacheKeyFilter ? _source.cacheKey : _source.url.absoluteString;
|
|
825
831
|
[FFFDownsampledImage addToContext: context forKey: key box: box cover: self.decodedCover];
|
|
832
|
+
if ([_source isPhotoLibrary]) {
|
|
833
|
+
// Photos makes the photo at the size asked for, with no file
|
|
834
|
+
// downloaded: SDWebImage would keep this smaller copy on disk as the
|
|
835
|
+
// photo's original, and a larger view would get it from there.
|
|
836
|
+
context[SDWebImageContextOriginalStoreCacheType] = @(SDImageCacheTypeNone);
|
|
837
|
+
context[SDWebImageContextOriginalQueryCacheType] = @(SDImageCacheTypeNone);
|
|
838
|
+
}
|
|
826
839
|
if (!_source.memoryCache) {
|
|
827
840
|
// The smaller copy is only kept in memory, so it isn't kept (the
|
|
828
841
|
// downloaded file is still on disk).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-fast-image",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.24.0",
|
|
4
4
|
"description": "🚩 FastImage, performant React Native image component.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
},
|
|
20
20
|
"license": "(MIT AND Apache-2.0)",
|
|
21
21
|
"author": "Dylan Vann <dylan@dylanvann.com> (https://dylanvann.com)",
|
|
22
|
-
"main": "dist/index.cjs
|
|
23
|
-
"module": "dist/index
|
|
22
|
+
"main": "dist/index.cjs",
|
|
23
|
+
"module": "dist/index",
|
|
24
|
+
"react-native": "dist/index.cjs",
|
|
25
|
+
"browser": "dist/index.web.js",
|
|
24
26
|
"typings": "dist/index.d.ts",
|
|
25
27
|
"files": [
|
|
26
28
|
"android/build.gradle",
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
"RNFastImage.podspec"
|
|
35
37
|
],
|
|
36
38
|
"scripts": {
|
|
37
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && rolldown -c &&
|
|
39
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && rolldown -c && for f in index index.cjs index.web index.cjs.web; do cp src/index.js.flow dist/$f.js.flow; done",
|
|
38
40
|
"lint": "oxlint",
|
|
39
41
|
"release": "semantic-release",
|
|
40
42
|
"test": "bun test",
|