react-native-fast-image 8.18.2 → 8.19.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.
@@ -4,6 +4,7 @@ import android.app.Activity;
4
4
  import android.content.Context;
5
5
  import android.graphics.BitmapFactory;
6
6
  import android.graphics.drawable.Drawable;
7
+ import android.net.Uri;
7
8
 
8
9
  import androidx.annotation.NonNull;
9
10
  import androidx.annotation.Nullable;
@@ -13,7 +14,9 @@ import com.bumptech.glide.Priority;
13
14
  import com.bumptech.glide.load.DataSource;
14
15
  import com.bumptech.glide.load.ImageHeaderParser;
15
16
  import com.bumptech.glide.load.ImageHeaderParserUtils;
17
+ import com.bumptech.glide.load.engine.DiskCacheStrategy;
16
18
  import com.bumptech.glide.load.engine.GlideException;
19
+ import com.bumptech.glide.load.model.GlideUrl;
17
20
  import com.bumptech.glide.request.RequestListener;
18
21
  import com.bumptech.glide.request.RequestOptions;
19
22
  import com.bumptech.glide.request.target.Target;
@@ -149,13 +152,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
149
152
  finishOne.run();
150
153
  continue;
151
154
  }
152
- RequestOptions options = FastImageViewConverter.getOptions(context, imageSource, source);
153
- // Low priority unless the source sets one, as on iOS (the
154
- // prefetcher's options), so the images the app shows load
155
- // first.
156
- final RequestOptions preloadOptions = source.hasKey("priority") && !source.isNull("priority")
157
- ? options
158
- : options.priority(Priority.LOW);
155
+ final RequestOptions preloadOptions = preloadOptions(context, imageSource, source);
159
156
  // A source's result, which frees its slot.
160
157
  final ResultCallback done = new ResultCallback() {
161
158
  @Override
@@ -172,38 +169,29 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
172
169
  pendingPreloads.add(new Runnable() {
173
170
  @Override
174
171
  public void run() {
175
- Glide
176
- .with(context)
177
- .asFile()
178
- .load(imageSource.getSourceForLoad())
179
- .apply(preloadOptions)
180
- .listener(new RequestListener<File>() {
181
- @Override
182
- public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
183
- done.run(failure(FastImageRequestListener.errorMessage(e)));
184
- return false;
185
- }
186
-
172
+ loadFile(context, imageSource.getSourceForLoad(), preloadOptions, new FileCallback() {
173
+ @Override
174
+ public void run(@Nullable final File file, @Nullable String error) {
175
+ if (file == null) {
176
+ done.run(failure(error));
177
+ return;
178
+ }
179
+ sizeExecutor.execute(new Runnable() {
187
180
  @Override
188
- public boolean onResourceReady(final File file, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
189
- sizeExecutor.execute(new Runnable() {
181
+ public void run() {
182
+ final int[] size = imageSize(context, file);
183
+ UiThreadUtil.runOnUiThread(new Runnable() {
190
184
  @Override
191
185
  public void run() {
192
- final int[] size = imageSize(context, file);
193
- UiThreadUtil.runOnUiThread(new Runnable() {
194
- @Override
195
- public void run() {
196
- done.run(size != null
197
- ? success(size[0], size[1])
198
- : failure("Failed to read the image's size"));
199
- }
200
- });
186
+ done.run(size != null
187
+ ? success(size[0], size[1])
188
+ : failure("Failed to read the image's size"));
201
189
  }
202
190
  });
203
- return false;
204
191
  }
205
- })
206
- .preload();
192
+ });
193
+ }
194
+ });
207
195
  }
208
196
  });
209
197
  continue;
@@ -249,6 +237,57 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
249
237
  void run(WritableMap result);
250
238
  }
251
239
 
240
+ // A preload's options: low priority unless the source sets one, as on
241
+ // iOS (the prefetcher's options), so the images the app shows load first.
242
+ private static RequestOptions preloadOptions(Context context, FastImageSource imageSource, ReadableMap source) {
243
+ RequestOptions options = FastImageViewConverter.getOptions(context, imageSource, source);
244
+ return source.hasKey("priority") && !source.isNull("priority") ? options : options.priority(Priority.LOW);
245
+ }
246
+
247
+ private interface FileCallback {
248
+ void run(@Nullable File file, @Nullable String error);
249
+ }
250
+
251
+ // Downloads the image into Glide's disk cache without decoding it (if it
252
+ // isn't there), then calls back with its file or the error, on the UI
253
+ // thread.
254
+ private static void loadFile(Context context, Object model, RequestOptions options, final FileCallback callback) {
255
+ Glide.with(context)
256
+ .asFile()
257
+ .load(model)
258
+ .apply(options)
259
+ .listener(new RequestListener<File>() {
260
+ @Override
261
+ public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
262
+ callback.run(null, FastImageRequestListener.errorMessage(e));
263
+ return false;
264
+ }
265
+
266
+ @Override
267
+ public boolean onResourceReady(File file, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
268
+ callback.run(file, null);
269
+ return false;
270
+ }
271
+ })
272
+ .preload();
273
+ }
274
+
275
+ // The file of the image in Glide's disk cache, or null. Blocking.
276
+ @Nullable
277
+ private static File cachedFile(Context context, Object model, RequestOptions options) {
278
+ try {
279
+ return Glide.with(context)
280
+ .asFile()
281
+ .load(model)
282
+ .apply(options)
283
+ .onlyRetrieveFromCache(true)
284
+ .submit()
285
+ .get();
286
+ } catch (Exception e) {
287
+ return null;
288
+ }
289
+ }
290
+
252
291
  private static WritableMap failure(String error) {
253
292
  WritableMap result = Arguments.createMap();
254
293
  result.putBoolean("ok", false);
@@ -270,12 +309,27 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
270
309
  // The source's downloaded file in the disk cache, downloading it first if
271
310
  // it isn't there (without decoding it or keeping it in memory, and in the
272
311
  // preloads' queue): { ok, path } or { ok: false, error }. Never rejects.
273
- // With `cacheOnly` it doesn't download. A local file is its own path.
312
+ // With `cacheOnly`, or a cacheKey without a uri, it doesn't download. A
313
+ // local file is its own path.
274
314
  @ReactMethod
275
315
  public void getCachePath(final ReadableMap source, final Promise promise) {
276
316
  final ReactApplicationContext context = getReactApplicationContext();
277
317
  if (!FastImageViewConverter.hasUri(source)) {
278
- promise.resolve(failure("Invalid source: no uri"));
318
+ // `web` images ignore cacheKey.
319
+ final String cacheKey = FastImageViewConverter.getCacheControl(source) == FastImageCacheControl.WEB
320
+ ? null
321
+ : FastImageViewConverter.getCacheKey(source);
322
+ if (cacheKey == null) {
323
+ promise.resolve(failure("Invalid source: no uri or cacheKey"));
324
+ return;
325
+ }
326
+ cachePathExecutor.execute(new Runnable() {
327
+ @Override
328
+ public void run() {
329
+ File file = cachedFile(context, FastImageKeyedGlideUrl.forKey(cacheKey), new RequestOptions());
330
+ promise.resolve(file != null ? pathResult(file) : failure("Not in the disk cache"));
331
+ }
332
+ });
279
333
  return;
280
334
  }
281
335
  final FastImageSource imageSource = FastImageViewConverter.getImageSource(context, source);
@@ -294,33 +348,16 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
294
348
  return;
295
349
  }
296
350
  final boolean cacheOnly = FastImageViewConverter.getCacheControl(source) == FastImageCacheControl.CACHE_ONLY;
297
- final RequestOptions options = FastImageViewConverter.getOptions(context, imageSource, source);
298
- final RequestOptions preloadOptions = source.hasKey("priority") && !source.isNull("priority")
299
- ? options
300
- : options.priority(Priority.LOW);
301
- final String url = imageSource.getUri().toString();
351
+ final RequestOptions options = preloadOptions(context, imageSource, source);
302
352
  cachePathExecutor.execute(new Runnable() {
303
353
  @Override
304
354
  public void run() {
305
355
  // On disk already: in Glide's cache (not for `web` images,
306
356
  // which it doesn't cache), or in the HTTP cache of `web`
307
357
  // images (set up by Glide, which is started first).
308
- File file = null;
309
358
  Glide.get(context);
310
- if (!imageSource.isWebCache()) {
311
- try {
312
- file = Glide.with(context)
313
- .asFile()
314
- .load(imageSource.getSourceForLoad())
315
- .apply(options)
316
- .onlyRetrieveFromCache(true)
317
- .submit()
318
- .get();
319
- } catch (Exception e) {
320
- // Not in Glide's cache.
321
- }
322
- }
323
- if (file == null) file = FastImageOkHttpProgressGlideModule.webCacheFile(url);
359
+ File file = imageSource.isWebCache() ? null : cachedFile(context, imageSource.getSourceForLoad(), options);
360
+ if (file == null) file = FastImageOkHttpProgressGlideModule.webCacheFile(imageSource.getUri().toString());
324
361
  if (file != null) {
325
362
  promise.resolve(pathResult(file));
326
363
  return;
@@ -333,8 +370,8 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
333
370
  @Override
334
371
  public void run() {
335
372
  pendingPreloads.add(imageSource.isWebCache()
336
- ? downloadToWebCache(imageSource, url, promise)
337
- : downloadToDiskCache(context, imageSource, preloadOptions, promise));
373
+ ? downloadToWebCache(imageSource, promise)
374
+ : downloadToDiskCache(context, imageSource, options, promise));
338
375
  startPendingPreloads();
339
376
  }
340
377
  });
@@ -363,35 +400,19 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
363
400
  return new Runnable() {
364
401
  @Override
365
402
  public void run() {
366
- Glide
367
- .with(context)
368
- .asFile()
369
- .load(imageSource.getSourceForLoad())
370
- .apply(options)
371
- .listener(new RequestListener<File>() {
372
- @Override
373
- public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<File> target, boolean isFirstResource) {
374
- finishDownload(promise, failure(FastImageRequestListener.errorMessage(e)));
375
- return false;
376
- }
377
-
378
- @Override
379
- public boolean onResourceReady(File file, Object model, Target<File> target, DataSource dataSource, boolean isFirstResource) {
380
- finishDownload(promise, pathResult(file));
381
- return false;
382
- }
383
- })
384
- .preload();
403
+ loadFile(context, imageSource.getSourceForLoad(), options, new FileCallback() {
404
+ @Override
405
+ public void run(@Nullable File file, @Nullable String error) {
406
+ finishDownload(promise, file != null ? pathResult(file) : failure(error));
407
+ }
408
+ });
385
409
  }
386
410
  };
387
411
  }
388
412
 
389
413
  // Downloads a `web` image into its HTTP cache, which keeps it only if the
390
414
  // server allows caching it.
391
- private static Runnable downloadToWebCache(
392
- final FastImageSource imageSource,
393
- final String url,
394
- final Promise promise) {
415
+ private static Runnable downloadToWebCache(final FastImageSource imageSource, final Promise promise) {
395
416
  return new Runnable() {
396
417
  @Override
397
418
  public void run() {
@@ -401,7 +422,7 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
401
422
  WritableMap result;
402
423
  try {
403
424
  FastImageOkHttpProgressGlideModule.downloadToWebCache(imageSource.getGlideUrl());
404
- File file = FastImageOkHttpProgressGlideModule.webCacheFile(url);
425
+ File file = FastImageOkHttpProgressGlideModule.webCacheFile(imageSource.getUri().toString());
405
426
  result = file != null
406
427
  ? pathResult(file)
407
428
  : failure("Not stored in the disk cache (the server doesn't allow caching it)");
@@ -415,6 +436,111 @@ class FastImageViewModule extends ReactContextBaseJavaModule {
415
436
  };
416
437
  }
417
438
 
439
+ // Stores a local image file (file:// or content://) as the source's image
440
+ // in Glide's disk cache, so views and preloads of the source load it
441
+ // without downloading it, and resolves with its cached file: { ok, path }
442
+ // or { ok: false, error }. Never rejects. It doesn't replace an image
443
+ // that's already cached (Glide can't: a new image should get a new
444
+ // cacheKey), and doesn't store `web` sources (kept only in an HTTP cache,
445
+ // which can't be added to).
446
+ @ReactMethod
447
+ public void writeToCache(final ReadableMap source, final String file, final Promise promise) {
448
+ final ReactApplicationContext context = getReactApplicationContext();
449
+ if (FastImageViewConverter.getCacheControl(source) == FastImageCacheControl.WEB) {
450
+ promise.resolve(failure("Can't store cache: 'web' images (they're kept in an HTTP cache)"));
451
+ return;
452
+ }
453
+ final FastImageSource imageSource = FastImageViewConverter.hasUri(source)
454
+ ? FastImageViewConverter.getImageSource(context, source)
455
+ : null;
456
+ // A cacheKey is enough (the image doesn't need a url yet); otherwise a
457
+ // remote url.
458
+ final String cacheKey = FastImageViewConverter.getCacheKey(source);
459
+ if (cacheKey == null && (imageSource == null || !imageSource.isRemote())) {
460
+ promise.resolve(failure("Invalid source: no remote uri or cacheKey"));
461
+ return;
462
+ }
463
+ // What the image is cached under, and how the source looks it up.
464
+ final GlideUrl key = imageSource != null && imageSource.isRemote()
465
+ ? imageSource.getGlideUrl()
466
+ : FastImageKeyedGlideUrl.forKey(cacheKey);
467
+ final Object lookUp = imageSource != null && imageSource.isRemote() ? imageSource.getSourceForLoad() : key;
468
+ // A file:// or content:// uri, or a path.
469
+ Uri parsed = Uri.parse(file);
470
+ final Uri fileUri = parsed.getScheme() == null ? Uri.fromFile(new File(file)) : parsed;
471
+ if (!"file".equalsIgnoreCase(fileUri.getScheme()) && !"content".equalsIgnoreCase(fileUri.getScheme())) {
472
+ promise.resolve(failure("Not a local file"));
473
+ return;
474
+ }
475
+ cachePathExecutor.execute(new Runnable() {
476
+ @Override
477
+ public void run() {
478
+ if (cachedFile(context, lookUp, new RequestOptions()) != null) {
479
+ promise.resolve(failure("Already in the disk cache"));
480
+ return;
481
+ }
482
+ BitmapFactory.Options bounds = new BitmapFactory.Options();
483
+ bounds.inJustDecodeBounds = true;
484
+ try (InputStream stream = context.getContentResolver().openInputStream(fileUri)) {
485
+ if (stream == null) throw new IOException();
486
+ BitmapFactory.decodeStream(stream, null, bounds);
487
+ } catch (IOException | SecurityException e) {
488
+ promise.resolve(failure("Can't read the file"));
489
+ return;
490
+ }
491
+ // The platform's decoders (so HEIF too, where Android has
492
+ // them), as a view's would decode it.
493
+ if (bounds.outWidth <= 0 || bounds.outHeight <= 0) {
494
+ promise.resolve(failure("Not an image"));
495
+ return;
496
+ }
497
+ try {
498
+ File cached = Glide.with(context)
499
+ .asFile()
500
+ .load(new FastImageCacheWrite(key, fileUri))
501
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
502
+ .skipMemoryCache(true)
503
+ .submit()
504
+ .get();
505
+ promise.resolve(pathResult(cached));
506
+ } catch (Exception e) {
507
+ promise.resolve(failure("Couldn't store it: " + e));
508
+ }
509
+ }
510
+ });
511
+ }
512
+
513
+ // Saves maxDiskSize (bytes, 0 for no limit; null goes back to the app's
514
+ // manifest or Glide's default), which FastImageGlideModule applies when
515
+ // Glide starts: now if it hasn't started yet (this starts it), or on the
516
+ // next launch. Resolves with the limit in effect and the
517
+ // bytes the disk cache uses now: { maxDiskSize, diskSize }, or {} if the
518
+ // app starts Glide with its own AppGlideModule (its size and folder are
519
+ // the app's). maxDiskAge and maxMemorySize are iOS only (Glide has no
520
+ // age limit, and sizes its memory cache from the screen).
521
+ @ReactMethod
522
+ public void configureCache(final ReadableMap limits, final Promise promise) {
523
+ final ReactApplicationContext context = getReactApplicationContext();
524
+ if (limits.hasKey("maxDiskSize")) {
525
+ FastImageCacheLimits.saveMaxDiskSize(context, limits.isNull("maxDiskSize")
526
+ ? null
527
+ : (Long) (long) Math.max(limits.getDouble("maxDiskSize"), 0));
528
+ }
529
+ cachePathExecutor.execute(new Runnable() {
530
+ @Override
531
+ public void run() {
532
+ Glide.get(context);
533
+ WritableMap result = Arguments.createMap();
534
+ long maxDiskSize = FastImageCacheLimits.startedMaxDiskSize;
535
+ if (maxDiskSize >= 0) {
536
+ result.putDouble("maxDiskSize", maxDiskSize);
537
+ result.putDouble("diskSize", FastImageCacheLimits.diskSize(context));
538
+ }
539
+ promise.resolve(result);
540
+ }
541
+ });
542
+ }
543
+
418
544
  @ReactMethod
419
545
  public void clearMemoryCache(final Promise promise) {
420
546
  final Activity activity = getCurrentActivity();
@@ -14,12 +14,14 @@ public class FastImageViewPackage implements ReactPackage {
14
14
  @NonNull
15
15
  @Override
16
16
  public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
17
+ FastImageCacheLimits.loadInBackground(reactContext);
17
18
  return Collections.<NativeModule>singletonList(new FastImageViewModule(reactContext));
18
19
  }
19
20
 
20
21
  @NonNull
21
22
  @Override
22
23
  public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
24
+ FastImageCacheLimits.loadInBackground(reactContext);
23
25
  return Collections.<ViewManager>singletonList(new FastImageViewManager());
24
26
  }
25
27
  }
package/app.plugin.js ADDED
@@ -0,0 +1,62 @@
1
+ // Expo config plugin: sets FastImage's cache limits in the native projects, so
2
+ // they're in effect from the first image. In app.json:
3
+ //
4
+ // "plugins": [["react-native-fast-image", { "maxDiskSize": 209715200 }]]
5
+ //
6
+ // maxDiskSize (bytes), maxDiskAge (seconds, iOS) and maxMemorySize (bytes,
7
+ // iOS); 0 means no limit. See the README's configureCache section.
8
+
9
+ const INFO_PLIST_KEYS = {
10
+ maxDiskSize: 'FastImageMaxDiskSize',
11
+ maxDiskAge: 'FastImageMaxDiskAge',
12
+ maxMemorySize: 'FastImageMaxMemorySize',
13
+ }
14
+ const ANDROID_MAX_DISK_SIZE = 'fastimage.MAX_DISK_SIZE'
15
+
16
+ // Sets the limits given in the iOS Info.plist.
17
+ function setInfoPlist(infoPlist, options = {}) {
18
+ for (const [name, key] of Object.entries(INFO_PLIST_KEYS)) {
19
+ if (typeof options[name] === 'number') infoPlist[key] = options[name]
20
+ }
21
+ return infoPlist
22
+ }
23
+
24
+ // Sets maxDiskSize in the Android manifest's application meta-data (Android
25
+ // only has that limit).
26
+ function setAndroidManifest(manifest, options = {}) {
27
+ if (typeof options.maxDiskSize !== 'number') return manifest
28
+ const application = manifest.manifest.application[0]
29
+ const metaData = (application['meta-data'] || []).filter(
30
+ (item) => item.$['android:name'] !== ANDROID_MAX_DISK_SIZE,
31
+ )
32
+ metaData.push({
33
+ $: {
34
+ 'android:name': ANDROID_MAX_DISK_SIZE,
35
+ 'android:value': String(options.maxDiskSize),
36
+ },
37
+ })
38
+ application['meta-data'] = metaData
39
+ return manifest
40
+ }
41
+
42
+ function withFastImage(config, options) {
43
+ // From the Expo project, which has it (older SDKs only as the package).
44
+ let plugins
45
+ try {
46
+ plugins = require('expo/config-plugins')
47
+ } catch {
48
+ plugins = require('@expo/config-plugins')
49
+ }
50
+ config = plugins.withInfoPlist(config, (mod) => {
51
+ mod.modResults = setInfoPlist(mod.modResults, options)
52
+ return mod
53
+ })
54
+ return plugins.withAndroidManifest(config, (mod) => {
55
+ mod.modResults = setAndroidManifest(mod.modResults, options)
56
+ return mod
57
+ })
58
+ }
59
+
60
+ module.exports = withFastImage
61
+ module.exports.setInfoPlist = setInfoPlist
62
+ module.exports.setAndroidManifest = setAndroidManifest
package/dist/index.cjs.js CHANGED
@@ -195,6 +195,8 @@ FastImage.preload = (sources) => Promise.resolve(react_native.NativeModules.Fast
195
195
  }));
196
196
  FastImage.clearMemoryCache = () => react_native.NativeModules.FastImageView.clearMemoryCache();
197
197
  FastImage.clearDiskCache = () => react_native.NativeModules.FastImageView.clearDiskCache();
198
+ FastImage.configureCache = (limits = {}) => Promise.resolve(react_native.NativeModules.FastImageView.configureCache(limits));
199
+ FastImage.writeToCache = (source, file) => Promise.resolve(react_native.NativeModules.FastImageView.writeToCache(source || {}, file));
198
200
  FastImage.getCachePath = (source) => Promise.resolve(react_native.NativeModules.FastImageView.getCachePath(source || {}));
199
201
  const styles = react_native.StyleSheet.create({
200
202
  fallbackImage: {
@@ -53,6 +53,19 @@ export type LoadResult =
53
53
  | {| ok: true, width: number, height: number |}
54
54
  | {| ok: false, error: string |}
55
55
 
56
+ export type CacheLimits = {|
57
+ maxDiskSize?: ?number,
58
+ maxDiskAge?: ?number,
59
+ maxMemorySize?: ?number,
60
+ |}
61
+
62
+ export type CacheState = {|
63
+ maxDiskSize?: number,
64
+ maxDiskAge?: number,
65
+ maxMemorySize?: number,
66
+ diskSize?: number,
67
+ |}
68
+
56
69
  export type CachePathResult =
57
70
  | {| ok: true, path: string |}
58
71
  | {| ok: false, error: string |}
@@ -123,4 +136,9 @@ declare export default class FastImage extends React$Component<FastImageProps> {
123
136
  static clearMemoryCache: () => Promise<void>;
124
137
  static clearDiskCache: () => Promise<void>;
125
138
  static getCachePath: (source: FastImageSource) => Promise<CachePathResult>;
139
+ static configureCache: (limits?: CacheLimits) => Promise<CacheState>;
140
+ static writeToCache: (
141
+ source: FastImageSource,
142
+ file: string,
143
+ ) => Promise<CachePathResult>;
126
144
  }
package/dist/index.d.ts CHANGED
@@ -62,6 +62,17 @@ export type LoadResult = {
62
62
  ok: false;
63
63
  error: string;
64
64
  };
65
+ export interface CacheLimits {
66
+ maxDiskSize?: number | null;
67
+ maxDiskAge?: number | null;
68
+ maxMemorySize?: number | null;
69
+ }
70
+ export interface CacheState {
71
+ maxDiskSize?: number;
72
+ maxDiskAge?: number;
73
+ maxMemorySize?: number;
74
+ diskSize?: number;
75
+ }
65
76
  export type CachePathResult = {
66
77
  ok: true;
67
78
  path: string;
@@ -189,9 +200,28 @@ export interface FastImageStaticProperties {
189
200
  /**
190
201
  * The path of the source's downloaded file in the disk cache, downloading
191
202
  * it first if it isn't there (without decoding it). With
192
- * `cache: 'cacheOnly'` it doesn't download. Never rejects.
203
+ * `cache: 'cacheOnly'`, or a `cacheKey` without a `uri`, it doesn't
204
+ * download. Never rejects.
193
205
  */
194
206
  getCachePath: (source: Source) => Promise<CachePathResult>;
207
+ /**
208
+ * Stores a local image file (a `file://` uri or a path, or on Android a
209
+ * `content://` uri) as the source's image in the disk cache, so views and
210
+ * preloads of the source show it without downloading it. A source with a
211
+ * `cacheKey` doesn't need a `uri`. Resolves with its cached file. Doesn't
212
+ * replace an image that's already cached, and isn't for `cache: 'web'`
213
+ * sources. Never rejects.
214
+ */
215
+ writeToCache: (source: Source, file: string) => Promise<CachePathResult>;
216
+ /**
217
+ * Changes the cache's limits at runtime (only those given; 0 for no
218
+ * limit, null to go back to the app's native config) and saves them, then
219
+ * resolves with the limits in effect and the disk cache's size. Without
220
+ * limits, only resolves. iOS applies changes at once. Android only has
221
+ * `maxDiskSize`, applied when Glide starts: at once if no image has
222
+ * loaded yet, otherwise from the next launch.
223
+ */
224
+ configureCache: (limits?: CacheLimits) => Promise<CacheState>;
195
225
  }
196
226
  declare const FastImage: React.ComponentType<FastImageProps> & FastImageStaticProperties;
197
227
  export interface FastImageBackgroundProps extends Omit<FastImageProps, 'style' | 'children'> {
package/dist/index.js CHANGED
@@ -168,6 +168,8 @@ FastImage.preload = (sources) => Promise.resolve(NativeModules.FastImageView.pre
168
168
  }));
169
169
  FastImage.clearMemoryCache = () => NativeModules.FastImageView.clearMemoryCache();
170
170
  FastImage.clearDiskCache = () => NativeModules.FastImageView.clearDiskCache();
171
+ FastImage.configureCache = (limits = {}) => Promise.resolve(NativeModules.FastImageView.configureCache(limits));
172
+ FastImage.writeToCache = (source, file) => Promise.resolve(NativeModules.FastImageView.writeToCache(source || {}, file));
171
173
  FastImage.getCachePath = (source) => Promise.resolve(NativeModules.FastImageView.getCachePath(source || {}));
172
174
  const styles = StyleSheet.create({
173
175
  fallbackImage: {
@@ -53,6 +53,19 @@ export type LoadResult =
53
53
  | {| ok: true, width: number, height: number |}
54
54
  | {| ok: false, error: string |}
55
55
 
56
+ export type CacheLimits = {|
57
+ maxDiskSize?: ?number,
58
+ maxDiskAge?: ?number,
59
+ maxMemorySize?: ?number,
60
+ |}
61
+
62
+ export type CacheState = {|
63
+ maxDiskSize?: number,
64
+ maxDiskAge?: number,
65
+ maxMemorySize?: number,
66
+ diskSize?: number,
67
+ |}
68
+
56
69
  export type CachePathResult =
57
70
  | {| ok: true, path: string |}
58
71
  | {| ok: false, error: string |}
@@ -123,4 +136,9 @@ declare export default class FastImage extends React$Component<FastImageProps> {
123
136
  static clearMemoryCache: () => Promise<void>;
124
137
  static clearDiskCache: () => Promise<void>;
125
138
  static getCachePath: (source: FastImageSource) => Promise<CachePathResult>;
139
+ static configureCache: (limits?: CacheLimits) => Promise<CacheState>;
140
+ static writeToCache: (
141
+ source: FastImageSource,
142
+ file: string,
143
+ ) => Promise<CachePathResult>;
126
144
  }
@@ -62,9 +62,12 @@ typedef NS_ENUM(NSInteger, FFFCacheControl) {
62
62
  + (NSURLCache *)webURLCache;
63
63
 
64
64
  // After a load of this source failed: for a `cache: 'web'` image whose data
65
- // isn't an image (e.g. a captive portal's HTML page), removes the response
66
- // from the HTTP cache, which stored it as it downloaded. Otherwise the next
67
- // loads would get it from there until it expires, and fail again.
65
+ // isn't an image, removes the response from the HTTP cache, which stored it as
66
+ // it downloaded. Otherwise the next loads would get it from there until it
67
+ // expires, and fail again. The web loader already doesn't store responses
68
+ // that aren't images (e.g. a captive portal's HTML page); this is for data
69
+ // that looks like an image but can't be decoded, or an app's own operation
70
+ // class.
68
71
  - (void)forgetResponseAfterError:(NSError *)error;
69
72
 
70
73
  @end
@@ -3,9 +3,35 @@
3
3
  #import <SDWebImage/SDWebImageDownloaderResponseModifier.h>
4
4
  #import <SDWebImage/SDWebImageDownloaderDecryptor.h>
5
5
  #import <SDWebImage/SDWebImageError.h>
6
+ #import <SDWebImage/SDWebImageDownloaderOperation.h>
7
+ #import <SDWebImage/NSData+ImageContentType.h>
6
8
 
7
9
  static NSUInteger const FFFWebCacheSize = 50 * 1024 * 1024;
8
10
 
11
+ // Downloads `cache: 'web'` images. The HTTP cache only stores a response whose
12
+ // body is an image: a page sent instead (e.g. a captive portal's, with status
13
+ // 200) would be served from there until it expired, and each load would fail.
14
+ // Removing it after a load failed (forgetResponseAfterError:) could still be
15
+ // in progress when the next load started.
16
+ @interface FFFWebDownloaderOperation : SDWebImageDownloaderOperation
17
+ @end
18
+
19
+ @implementation FFFWebDownloaderOperation
20
+
21
+ - (void)URLSession:(NSURLSession *)session
22
+ dataTask:(NSURLSessionDataTask *)dataTask
23
+ willCacheResponse:(NSCachedURLResponse *)proposedResponse
24
+ completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
25
+ {
26
+ if ([NSData sd_imageFormatForImageData:proposedResponse.data] == SDImageFormatUndefined) {
27
+ completionHandler(nil);
28
+ return;
29
+ }
30
+ [super URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
31
+ }
32
+
33
+ @end
34
+
9
35
  @implementation FFFastImageSource
10
36
 
11
37
  - (instancetype)initWithURL:(NSURL *)url
@@ -62,6 +88,10 @@ static NSUInteger const FFFWebCacheSize = 50 * 1024 * 1024;
62
88
  NSURLSessionConfiguration *session = [(config.sessionConfiguration ?: NSURLSessionConfiguration.defaultSessionConfiguration) copy];
63
89
  session.URLCache = [FFFastImageSource webURLCache];
64
90
  config.sessionConfiguration = session;
91
+ // Unless the app set its own operation class.
92
+ if (!config.operationClass || config.operationClass == [SDWebImageDownloaderOperation class]) {
93
+ config.operationClass = [FFFWebDownloaderOperation class];
94
+ }
65
95
  downloader = [[SDWebImageDownloader alloc] initWithConfig:config];
66
96
  // The shared downloader's response modifier and decryptor, when it
67
97
  // has them (as they are at each download). Its request modifier