react-native-fast-image 8.18.1 → 8.19.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 +67 -2
- package/android/build.gradle +1 -0
- package/android/consumer-rules.pro +30 -4
- package/android/src/main/java/com/dylanvann/fastimage/FastImageCacheLimits.java +101 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageCacheWrite.java +111 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageGlideModule.java +14 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageKeyedGlideUrl.java +7 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +4 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +10 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +207 -81
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java +2 -0
- package/app.plugin.js +62 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.flow +18 -0
- package/dist/index.d.ts +31 -1
- package/dist/index.js +2 -0
- package/dist/index.js.flow +18 -0
- package/ios/FastImage/FFFastImageSource.h +6 -3
- package/ios/FastImage/FFFastImageSource.m +30 -0
- package/ios/FastImage/FFFastImageViewManager.m +294 -79
- package/package.json +2 -1
|
@@ -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
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
311
|
-
|
|
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,
|
|
337
|
-
: downloadToDiskCache(context, imageSource,
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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(
|
|
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: {
|
package/dist/index.cjs.js.flow
CHANGED
|
@@ -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
|
|
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: {
|
package/dist/index.js.flow
CHANGED
|
@@ -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
|
|
66
|
-
//
|
|
67
|
-
//
|
|
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
|