@symbiote-native/image 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +260 -2
- package/build/angular/image/index.d.ts +20 -0
- package/build/angular/image/index.js +113 -0
- package/build/angular/image-background/index.d.ts +14 -0
- package/build/angular/image-background/index.js +109 -0
- package/build/angular/index.d.ts +4 -0
- package/build/angular/index.js +6 -0
- package/build/angular/inject-image.d.ts +8 -0
- package/build/angular/inject-image.js +20 -0
- package/build/core/constants.d.ts +3 -0
- package/build/core/constants.js +4 -0
- package/build/core/hash-strings.d.ts +5 -0
- package/build/core/hash-strings.js +22 -0
- package/build/core/image-api.d.ts +18 -0
- package/build/core/image-api.js +30 -0
- package/build/core/image-background.d.ts +11 -0
- package/build/core/image-background.js +29 -0
- package/build/core/image-loader.d.ts +20 -0
- package/build/core/image-loader.js +42 -0
- package/build/core/image-ref-loader.d.ts +4 -0
- package/build/core/image-ref-loader.js +10 -0
- package/build/core/image-view-events.d.ts +9 -0
- package/build/core/image-view-events.js +34 -0
- package/build/core/image-view.d.ts +14 -0
- package/build/core/image-view.js +113 -0
- package/build/core/index.d.ts +10 -0
- package/build/core/index.js +7 -0
- package/build/core/native-module.d.ts +27 -0
- package/build/core/native-module.js +3 -0
- package/build/core/native-style.d.ts +19 -0
- package/build/core/native-style.js +65 -0
- package/build/core/props.d.ts +11 -0
- package/build/core/props.js +87 -0
- package/build/core/sources.d.ts +8 -0
- package/build/core/sources.js +61 -0
- package/build/core/types.d.ts +186 -0
- package/build/core/types.js +1 -0
- package/build/react/image-background.d.ts +7 -0
- package/build/react/image-background.js +7 -0
- package/build/react/image.d.ts +8 -0
- package/build/react/image.js +6 -0
- package/build/react/index.d.ts +6 -0
- package/build/react/index.js +5 -0
- package/build/react/use-image.d.ts +8 -0
- package/build/react/use-image.js +20 -0
- package/build/solid/image-background.d.ts +7 -0
- package/build/solid/image-background.js +4 -0
- package/build/solid/image.d.ts +45 -0
- package/build/solid/image.js +4 -0
- package/build/solid/index.d.ts +5 -0
- package/build/solid/index.js +5 -0
- package/build/solid/use-image.d.ts +8 -0
- package/build/solid/use-image.js +16 -0
- package/build/svelte/image-background-props.d.ts +5 -0
- package/build/svelte/image-background-props.js +1 -0
- package/build/svelte/image-background.svelte +10 -0
- package/build/svelte/image-background.svelte.d.ts +4 -0
- package/build/svelte/image.svelte +26 -0
- package/build/svelte/image.svelte.d.ts +10 -0
- package/build/svelte/index.d.ts +6 -0
- package/build/svelte/index.js +5 -0
- package/build/svelte/use-image.svelte.d.ts +10 -0
- package/build/svelte/use-image.svelte.js +26 -0
- package/build/vue/image-background.d.ts +3 -0
- package/build/vue/image-background.js +12 -0
- package/build/vue/image.d.ts +2 -0
- package/build/vue/image.js +4 -0
- package/build/vue/index.d.ts +4 -0
- package/build/vue/index.js +5 -0
- package/build/vue/use-image.d.ts +8 -0
- package/build/vue/use-image.js +18 -0
- package/build-ngc/angular/image/index.d.ts +23 -0
- package/build-ngc/angular/image/index.js +72 -0
- package/build-ngc/angular/image/index.js.map +1 -0
- package/build-ngc/angular/image-background/index.d.ts +17 -0
- package/build-ngc/angular/image-background/index.js +70 -0
- package/build-ngc/angular/image-background/index.js.map +1 -0
- package/build-ngc/angular/index.d.ts +4 -0
- package/build-ngc/angular/index.js +7 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/inject-image.d.ts +8 -0
- package/build-ngc/angular/inject-image.js +23 -0
- package/build-ngc/angular/inject-image.js.map +1 -0
- package/build-ngc/core/constants.d.ts +3 -0
- package/build-ngc/core/constants.js +5 -0
- package/build-ngc/core/constants.js.map +1 -0
- package/build-ngc/core/hash-strings.d.ts +5 -0
- package/build-ngc/core/hash-strings.js +23 -0
- package/build-ngc/core/hash-strings.js.map +1 -0
- package/build-ngc/core/image-api.d.ts +18 -0
- package/build-ngc/core/image-api.js +31 -0
- package/build-ngc/core/image-api.js.map +1 -0
- package/build-ngc/core/image-background.d.ts +11 -0
- package/build-ngc/core/image-background.js +30 -0
- package/build-ngc/core/image-background.js.map +1 -0
- package/build-ngc/core/image-loader.d.ts +20 -0
- package/build-ngc/core/image-loader.js +43 -0
- package/build-ngc/core/image-loader.js.map +1 -0
- package/build-ngc/core/image-ref-loader.d.ts +4 -0
- package/build-ngc/core/image-ref-loader.js +11 -0
- package/build-ngc/core/image-ref-loader.js.map +1 -0
- package/build-ngc/core/image-view-events.d.ts +9 -0
- package/build-ngc/core/image-view-events.js +35 -0
- package/build-ngc/core/image-view-events.js.map +1 -0
- package/build-ngc/core/image-view.d.ts +14 -0
- package/build-ngc/core/image-view.js +114 -0
- package/build-ngc/core/image-view.js.map +1 -0
- package/build-ngc/core/index.d.ts +10 -0
- package/build-ngc/core/index.js +8 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +27 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/native-style.d.ts +19 -0
- package/build-ngc/core/native-style.js +66 -0
- package/build-ngc/core/native-style.js.map +1 -0
- package/build-ngc/core/props.d.ts +11 -0
- package/build-ngc/core/props.js +88 -0
- package/build-ngc/core/props.js.map +1 -0
- package/build-ngc/core/sources.d.ts +8 -0
- package/build-ngc/core/sources.js +62 -0
- package/build-ngc/core/sources.js.map +1 -0
- package/build-ngc/core/types.d.ts +186 -0
- package/build-ngc/core/types.js +2 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +13 -0
- package/package.json +145 -3
- package/src/angular/image/index.ts +79 -0
- package/src/angular/image-background/index.ts +72 -0
- package/src/angular/index.ts +7 -0
- package/src/angular/inject-image.ts +29 -0
- package/src/core/constants.ts +6 -0
- package/src/core/hash-strings.ts +29 -0
- package/src/core/image-api.ts +77 -0
- package/src/core/image-background.ts +47 -0
- package/src/core/image-loader.ts +72 -0
- package/src/core/image-ref-loader.ts +16 -0
- package/src/core/image-view-events.ts +47 -0
- package/src/core/image-view.ts +173 -0
- package/src/core/index.ts +50 -0
- package/src/core/native-module.ts +44 -0
- package/src/core/native-style.ts +96 -0
- package/src/core/props.ts +123 -0
- package/src/core/sources.ts +75 -0
- package/src/core/types.ts +275 -0
- package/src/react/image-background.ts +16 -0
- package/src/react/image.ts +17 -0
- package/src/react/index.ts +8 -0
- package/src/react/use-image.ts +29 -0
- package/src/solid/image-background.ts +12 -0
- package/src/solid/image.ts +9 -0
- package/src/solid/index.ts +7 -0
- package/src/solid/use-image.ts +30 -0
- package/src/svelte/image-background-props.ts +6 -0
- package/src/svelte/image-background.svelte +10 -0
- package/src/svelte/image.svelte +26 -0
- package/src/svelte/index.ts +8 -0
- package/src/svelte/svelte-ambient.d.ts +3 -0
- package/src/svelte/svelte-compile.test-helper.ts +6 -0
- package/src/svelte/use-image.svelte.ts +38 -0
- package/src/vue/image-background.ts +17 -0
- package/src/vue/image.ts +5 -0
- package/src/vue/index.ts +6 -0
- package/src/vue/use-image.ts +38 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { toNativeColor } from './native-style.js';
|
|
2
|
+
import { expoImage } from './native-module.js';
|
|
3
|
+
import { resolveSource } from './sources.js';
|
|
4
|
+
const DEFAULT_CACHE_POLICY = 'memory-disk';
|
|
5
|
+
/** The caching policy as a string, or with request headers as an object */
|
|
6
|
+
export function prefetchImages(urls, options) {
|
|
7
|
+
const { cachePolicy = DEFAULT_CACHE_POLICY, headers } = typeof options === 'string' ? { cachePolicy: options } : (options ?? {});
|
|
8
|
+
return expoImage.prefetch(Array.isArray(urls) ? urls : [urls], cachePolicy, headers);
|
|
9
|
+
}
|
|
10
|
+
export const clearMemoryCache = () => expoImage.clearMemoryCache();
|
|
11
|
+
export const clearDiskCache = () => expoImage.clearDiskCache();
|
|
12
|
+
export const getCachePathAsync = (cacheKey) => expoImage.getCachePathAsync(cacheKey);
|
|
13
|
+
export const writeToCacheAsync = (source, cacheKey) => expoImage.writeToCacheAsync(source, cacheKey);
|
|
14
|
+
export const readFromCacheAsync = (cacheKey) => expoImage.readFromCacheAsync(cacheKey);
|
|
15
|
+
export const configureCache = (config) => expoImage.configureCache(config);
|
|
16
|
+
export const generateBlurhashAsync = (source, numberOfComponents) => expoImage.generateBlurhashAsync(source, numberOfComponents);
|
|
17
|
+
export const generateThumbhashAsync = (source) => expoImage.generateThumbhashAsync(source);
|
|
18
|
+
/** Loads the image into a native `ImageRef`, the caller releases it */
|
|
19
|
+
export async function loadImageAsync(source, options = {}) {
|
|
20
|
+
const resolved = resolveSource(source);
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
throw new Error('[expo-image]: the image source could not be resolved');
|
|
23
|
+
}
|
|
24
|
+
const { maxWidth, maxHeight, tintColor } = options;
|
|
25
|
+
return expoImage.loadAsync(resolved, {
|
|
26
|
+
maxWidth,
|
|
27
|
+
maxHeight,
|
|
28
|
+
tintColor: toNativeColor(tintColor),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IDescriptor } from '@symbiote-native/components';
|
|
2
|
+
import type { IStyleProp } from '@symbiote-native/engine';
|
|
3
|
+
import type { IImageStyle, IImageViewProps } from './types';
|
|
4
|
+
export type IImageBackgroundProps = IImageViewProps & {
|
|
5
|
+
/** Style of the image itself, `style` goes to the wrapping view */
|
|
6
|
+
imageStyle?: IStyleProp<IImageStyle>;
|
|
7
|
+
/** Goes to the wrapping view, as `style` does */
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
/** A view with an image filling it, the children of the app go after this descriptor */
|
|
11
|
+
export declare function renderImageBackground(props: IImageBackgroundProps): IDescriptor;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { el } from '@symbiote-native/components';
|
|
2
|
+
import { createImageView } from './image-view.js';
|
|
3
|
+
const CLASS_KEYS = new Set([
|
|
4
|
+
'className',
|
|
5
|
+
'class',
|
|
6
|
+
]);
|
|
7
|
+
function isClassEntry([key]) {
|
|
8
|
+
return CLASS_KEYS.has(key);
|
|
9
|
+
}
|
|
10
|
+
const ABSOLUTE_FILL = {
|
|
11
|
+
position: 'absolute',
|
|
12
|
+
left: 0,
|
|
13
|
+
right: 0,
|
|
14
|
+
top: 0,
|
|
15
|
+
bottom: 0,
|
|
16
|
+
};
|
|
17
|
+
// The background image has no handle, so nobody asks it for a host node
|
|
18
|
+
const backgroundImage = createImageView(() => null);
|
|
19
|
+
/** A view with an image filling it, the children of the app go after this descriptor */
|
|
20
|
+
export function renderImageBackground(props) {
|
|
21
|
+
const { style, imageStyle, ...rest } = props;
|
|
22
|
+
const entries = Object.entries(rest);
|
|
23
|
+
const image = backgroundImage.render({
|
|
24
|
+
...Object.fromEntries(entries.filter(entry => !isClassEntry(entry))),
|
|
25
|
+
style: [ABSOLUTE_FILL, imageStyle],
|
|
26
|
+
});
|
|
27
|
+
const viewClass = Object.fromEntries(entries.filter(isClassEntry));
|
|
28
|
+
return el('view', { style, ...viewClass }, image ? [image] : []);
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IImageLoadOptions, IImageSource } from './types';
|
|
2
|
+
export type IImageLoaderConfig<TImage extends {
|
|
3
|
+
release(): void;
|
|
4
|
+
}> = {
|
|
5
|
+
load(source: IImageSource | string | number, options: IImageLoadOptions): Promise<TImage>;
|
|
6
|
+
/** Called with each image that finished loading */
|
|
7
|
+
onImage(image: TImage): void;
|
|
8
|
+
/** Read at the time of a failure, so the latest callback wins */
|
|
9
|
+
getOptions(): IImageLoadOptions;
|
|
10
|
+
};
|
|
11
|
+
export type IImageLoader = {
|
|
12
|
+
/** Starts a load and drops the result of the one before */
|
|
13
|
+
load(source: IImageSource | string | number): void;
|
|
14
|
+
/** Drops a load in flight and releases the image loaded last */
|
|
15
|
+
cancel(): void;
|
|
16
|
+
};
|
|
17
|
+
/** What a `useImage` hook drives with its own lifecycle */
|
|
18
|
+
export declare function createImageLoader<TImage extends {
|
|
19
|
+
release(): void;
|
|
20
|
+
}>(config: IImageLoaderConfig<TImage>): IImageLoader;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** What a `useImage` hook drives with its own lifecycle */
|
|
2
|
+
export function createImageLoader(config) {
|
|
3
|
+
let token = 0;
|
|
4
|
+
let loaded = null;
|
|
5
|
+
function release() {
|
|
6
|
+
loaded?.release();
|
|
7
|
+
loaded = null;
|
|
8
|
+
}
|
|
9
|
+
function fail(source, error, retry) {
|
|
10
|
+
const { onError } = config.getOptions();
|
|
11
|
+
if (onError) {
|
|
12
|
+
onError(error instanceof Error ? error : new Error(String(error)), retry);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
console.error(`Loading an image from '${JSON.stringify(source)}' failed, use 'onError' to handle errors`);
|
|
16
|
+
console.error(error);
|
|
17
|
+
}
|
|
18
|
+
function load(source) {
|
|
19
|
+
release();
|
|
20
|
+
token += 1;
|
|
21
|
+
const mine = token;
|
|
22
|
+
config
|
|
23
|
+
.load(source, config.getOptions())
|
|
24
|
+
.then(image => {
|
|
25
|
+
if (mine !== token)
|
|
26
|
+
return;
|
|
27
|
+
loaded = image;
|
|
28
|
+
config.onImage(image);
|
|
29
|
+
})
|
|
30
|
+
.catch((error) => {
|
|
31
|
+
if (mine === token)
|
|
32
|
+
fail(source, error, () => load(source));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
load,
|
|
37
|
+
cancel: () => {
|
|
38
|
+
token += 1;
|
|
39
|
+
release();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IImageLoader } from './image-loader';
|
|
2
|
+
import type { IImageLoadOptions, ImageRef } from './types';
|
|
3
|
+
/** The loader every `useImage` hook drives, `getOptions` is read at each load and failure */
|
|
4
|
+
export declare function createImageRefLoader(onImage: (image: ImageRef) => void, getOptions: () => IImageLoadOptions): IImageLoader;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { loadImageAsync } from './image-api.js';
|
|
2
|
+
import { createImageLoader } from './image-loader.js';
|
|
3
|
+
/** The loader every `useImage` hook drives, `getOptions` is read at each load and failure */
|
|
4
|
+
export function createImageRefLoader(onImage, getOptions) {
|
|
5
|
+
return createImageLoader({
|
|
6
|
+
load: loadImageAsync,
|
|
7
|
+
onImage,
|
|
8
|
+
getOptions,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
|
+
import type { IImageViewProps } from './types';
|
|
3
|
+
/** The native payload sits in `nativeEvent`, callbacks get it as the event itself */
|
|
4
|
+
export declare function toImageEvents(props: IImageViewProps): {
|
|
5
|
+
onLoadStart: () => void | undefined;
|
|
6
|
+
onLoad: (event: ISymbioteEvent) => void;
|
|
7
|
+
onProgress: (event: ISymbioteEvent) => void;
|
|
8
|
+
onError: (event: ISymbioteEvent) => void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function field(value, key) {
|
|
2
|
+
return Reflect.get(Object(value), key);
|
|
3
|
+
}
|
|
4
|
+
function isLoad(value) {
|
|
5
|
+
return (typeof field(value, 'cacheType') === 'string' &&
|
|
6
|
+
typeof field(value, 'source') === 'object');
|
|
7
|
+
}
|
|
8
|
+
function isProgress(value) {
|
|
9
|
+
return (typeof field(value, 'loaded') === 'number' &&
|
|
10
|
+
typeof field(value, 'total') === 'number');
|
|
11
|
+
}
|
|
12
|
+
function isError(value) {
|
|
13
|
+
return typeof field(value, 'error') === 'string';
|
|
14
|
+
}
|
|
15
|
+
/** The native payload sits in `nativeEvent`, callbacks get it as the event itself */
|
|
16
|
+
export function toImageEvents(props) {
|
|
17
|
+
return {
|
|
18
|
+
onLoadStart: () => props.onLoadStart?.(),
|
|
19
|
+
onLoad: (event) => {
|
|
20
|
+
if (isLoad(event.nativeEvent))
|
|
21
|
+
props.onLoad?.(event.nativeEvent);
|
|
22
|
+
props.onLoadEnd?.();
|
|
23
|
+
},
|
|
24
|
+
onProgress: (event) => {
|
|
25
|
+
if (isProgress(event.nativeEvent))
|
|
26
|
+
props.onProgress?.(event.nativeEvent);
|
|
27
|
+
},
|
|
28
|
+
onError: (event) => {
|
|
29
|
+
if (isError(event.nativeEvent))
|
|
30
|
+
props.onError?.(event.nativeEvent);
|
|
31
|
+
props.onLoadEnd?.();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IDescriptor } from '@symbiote-native/components';
|
|
2
|
+
import type { ISymbioteNode } from '@symbiote-native/engine';
|
|
3
|
+
import type { IImageViewHandle, IImageViewProps } from './types';
|
|
4
|
+
export declare const imageViewName: () => string;
|
|
5
|
+
export declare const ensureImageViewRegistered: () => boolean;
|
|
6
|
+
export type IImageView = {
|
|
7
|
+
handle: IImageViewHandle;
|
|
8
|
+
/** `null` means the view cannot register and the caller renders nothing */
|
|
9
|
+
render(props: IImageViewProps): IDescriptor | null;
|
|
10
|
+
};
|
|
11
|
+
type IGetNode = () => ISymbioteNode | null | undefined;
|
|
12
|
+
/** One per mounted view, `getNode` answers the host node once it exists */
|
|
13
|
+
export declare function createImageView(getNode: IGetNode): IImageView;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { requireNativeModule, requireNativeViewManager, } from 'expo-modules-core';
|
|
2
|
+
import { el } from '@symbiote-native/components';
|
|
3
|
+
import { defineExpoNativeView, defineExpoViewMethods, getNativeTag, isDevBuild, styleOfProps, } from '@symbiote-native/engine';
|
|
4
|
+
import { IMAGE_MODULE_NAME, SF_SYMBOL_PREFIX } from './constants.js';
|
|
5
|
+
import { toImageEvents } from './image-view-events.js';
|
|
6
|
+
import { nativeColorProps, platformStyle, splitImageStyle, toNativeColor, } from './native-style.js';
|
|
7
|
+
import { resolveContentFit, resolveContentPosition, resolveSfEffect, resolveTransition, warnOnce, } from './props.js';
|
|
8
|
+
import { resolveSources } from './sources.js';
|
|
9
|
+
// Registered on render: a barrel side effect is lost in a release build
|
|
10
|
+
const imageView = defineExpoNativeView(requireNativeViewManager, IMAGE_MODULE_NAME);
|
|
11
|
+
const callViewFunction = defineExpoViewMethods(requireNativeModule, IMAGE_MODULE_NAME);
|
|
12
|
+
export const imageViewName = imageView.name;
|
|
13
|
+
export const ensureImageViewRegistered = imageView.ensureRegistered;
|
|
14
|
+
// Props the view reads itself and native has no use for
|
|
15
|
+
const OWN_PROPS = [
|
|
16
|
+
'style',
|
|
17
|
+
'source',
|
|
18
|
+
'placeholder',
|
|
19
|
+
'contentFit',
|
|
20
|
+
'contentPosition',
|
|
21
|
+
'transition',
|
|
22
|
+
'fadeDuration',
|
|
23
|
+
'resizeMode',
|
|
24
|
+
'defaultSource',
|
|
25
|
+
'loadingIndicatorSource',
|
|
26
|
+
'sfEffect',
|
|
27
|
+
'alt',
|
|
28
|
+
];
|
|
29
|
+
function hasSfSymbol(sources) {
|
|
30
|
+
return (Array.isArray(sources) &&
|
|
31
|
+
sources.some(source => source.uri?.startsWith(`${SF_SYMBOL_PREFIX}/`)));
|
|
32
|
+
}
|
|
33
|
+
function withoutOwnProps(props) {
|
|
34
|
+
const own = OWN_PROPS;
|
|
35
|
+
return Object.fromEntries(Object.entries(props).filter(([key]) => !own.includes(key)));
|
|
36
|
+
}
|
|
37
|
+
function placeholderOf(props) {
|
|
38
|
+
const { defaultSource, loadingIndicatorSource } = props;
|
|
39
|
+
if (defaultSource || loadingIndicatorSource) {
|
|
40
|
+
warnOnce('`defaultSource` and `loadingIndicatorSource` props are deprecated, use `placeholder`');
|
|
41
|
+
}
|
|
42
|
+
return resolveSources(props.placeholder ?? defaultSource ?? loadingIndicatorSource);
|
|
43
|
+
}
|
|
44
|
+
// For an SF Symbol `fontSize` sizes both the symbol and its container
|
|
45
|
+
function symbolProps(parts, isSfSymbol) {
|
|
46
|
+
if (!isSfSymbol)
|
|
47
|
+
return { symbolWeight: null, symbolSize: null };
|
|
48
|
+
const { fontWeight, fontSize } = parts;
|
|
49
|
+
return {
|
|
50
|
+
symbolWeight: fontWeight === undefined ? null : String(fontWeight),
|
|
51
|
+
symbolSize: fontSize || null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function sizedStyle(parts, isSfSymbol) {
|
|
55
|
+
const { fontSize, rest } = parts;
|
|
56
|
+
return isSfSymbol && fontSize
|
|
57
|
+
? { width: fontSize, height: fontSize, ...rest }
|
|
58
|
+
: rest;
|
|
59
|
+
}
|
|
60
|
+
function nativeProps(props) {
|
|
61
|
+
const parts = splitImageStyle(styleOfProps(props));
|
|
62
|
+
const source = resolveSources(props.source);
|
|
63
|
+
const isSfSymbol = hasSfSymbol(source);
|
|
64
|
+
const sized = sizedStyle(parts, isSfSymbol);
|
|
65
|
+
const style = platformStyle(sized);
|
|
66
|
+
const symbolColor = isSfSymbol ? parts.color : undefined;
|
|
67
|
+
return {
|
|
68
|
+
...withoutOwnProps(props),
|
|
69
|
+
...style,
|
|
70
|
+
accessibilityLabel: props.accessibilityLabel ?? props.alt,
|
|
71
|
+
style,
|
|
72
|
+
source,
|
|
73
|
+
placeholder: placeholderOf(props),
|
|
74
|
+
contentFit: resolveContentFit(props.contentFit, props.resizeMode ?? parts.resizeMode, isSfSymbol),
|
|
75
|
+
contentPosition: resolveContentPosition(props.contentPosition),
|
|
76
|
+
transition: resolveTransition(props.transition, props.fadeDuration),
|
|
77
|
+
sfEffect: resolveSfEffect(props.sfEffect),
|
|
78
|
+
...symbolProps(parts, isSfSymbol),
|
|
79
|
+
...nativeColorProps(sized),
|
|
80
|
+
tintColor: toNativeColor(props.tintColor || symbolColor || style.tintColor),
|
|
81
|
+
...toImageEvents(props),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function createImageViewHandle(getNode) {
|
|
85
|
+
// Like upstream's `ref.current?.`, a view that has not committed yet answers nothing
|
|
86
|
+
async function call(method) {
|
|
87
|
+
const node = getNode();
|
|
88
|
+
if (!node || getNativeTag(node) === undefined)
|
|
89
|
+
return;
|
|
90
|
+
await callViewFunction(node, method, []);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
startAnimating: () => call('startAnimating'),
|
|
94
|
+
stopAnimating: () => call('stopAnimating'),
|
|
95
|
+
lockResourceAsync: () => call('lockResourceAsync'),
|
|
96
|
+
unlockResourceAsync: () => call('unlockResourceAsync'),
|
|
97
|
+
reloadAsync: () => call('reloadAsync'),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** One per mounted view, `getNode` answers the host node once it exists */
|
|
101
|
+
export function createImageView(getNode) {
|
|
102
|
+
return {
|
|
103
|
+
handle: createImageViewHandle(getNode),
|
|
104
|
+
render: props => {
|
|
105
|
+
if (!ensureImageViewRegistered()) {
|
|
106
|
+
if (isDevBuild())
|
|
107
|
+
console.warn("'Image' is not available.");
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return el(imageViewName(), nativeProps(props));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { IMAGE_MODULE_NAME } from './constants';
|
|
2
|
+
export { clearDiskCache, clearMemoryCache, configureCache, generateBlurhashAsync, generateThumbhashAsync, getCachePathAsync, loadImageAsync, prefetchImages, readFromCacheAsync, writeToCacheAsync, } from './image-api';
|
|
3
|
+
export { renderImageBackground } from './image-background';
|
|
4
|
+
export type { IImageBackgroundProps } from './image-background';
|
|
5
|
+
export { createImageRefLoader } from './image-ref-loader';
|
|
6
|
+
export { createImageLoader } from './image-loader';
|
|
7
|
+
export type { IImageLoader, IImageLoaderConfig } from './image-loader';
|
|
8
|
+
export { createImageView, ensureImageViewRegistered, imageViewName, type IImageView, } from './image-view';
|
|
9
|
+
export { isImageRef, resolveSource, resolveSources } from './sources';
|
|
10
|
+
export type { IImageCacheConfig, IImageContentFit, IImageContentPosition, IImageContentPositionObject, IImageContentPositionString, IImageContentPositionValue, IImageDecodeFormat, IImageErrorEventData, IImageLoadEventData, IImageLoadOptions, IImagePrefetchOptions, IImageProgressEventData, IImageResizeMode, IImageSource, IImageSources, IImageStyle, IImageTransition, IImageViewHandle, IImageViewProps, ISfSymbolEffect, ISfSymbolEffectObject, ISfSymbolEffectType, ImageRef, } from './types';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { IMAGE_MODULE_NAME } from './constants.js';
|
|
2
|
+
export { clearDiskCache, clearMemoryCache, configureCache, generateBlurhashAsync, generateThumbhashAsync, getCachePathAsync, loadImageAsync, prefetchImages, readFromCacheAsync, writeToCacheAsync, } from './image-api.js';
|
|
3
|
+
export { renderImageBackground } from './image-background.js';
|
|
4
|
+
export { createImageRefLoader } from './image-ref-loader.js';
|
|
5
|
+
export { createImageLoader } from './image-loader.js';
|
|
6
|
+
export { createImageView, ensureImageViewRegistered, imageViewName, } from './image-view.js';
|
|
7
|
+
export { isImageRef, resolveSource, resolveSources } from './sources.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import type { IImageCacheConfig, IImageLoadOptions, IImagePrefetchOptions, IImageSource, ImageRef } from './types';
|
|
3
|
+
export type INativeImageModule = {
|
|
4
|
+
Image: typeof ImageRef;
|
|
5
|
+
loadAsync(source: IImageSource, options?: Omit<IImageLoadOptions, 'onError' | 'tintColor'> & {
|
|
6
|
+
/** What `processColor` answers */
|
|
7
|
+
tintColor?: unknown;
|
|
8
|
+
}): Promise<ImageRef>;
|
|
9
|
+
prefetch(urls: string[], cachePolicy: IImagePrefetchOptions['cachePolicy'], headers?: Record<string, string>): Promise<boolean>;
|
|
10
|
+
clearMemoryCache(): Promise<boolean>;
|
|
11
|
+
clearDiskCache(): Promise<boolean>;
|
|
12
|
+
configureCache(config: IImageCacheConfig): void;
|
|
13
|
+
getCachePathAsync(cacheKey: string): Promise<string | null>;
|
|
14
|
+
writeToCacheAsync(source: string | ImageRef, cacheKey: string): Promise<void>;
|
|
15
|
+
readFromCacheAsync(cacheKey: string): Promise<ImageRef | null>;
|
|
16
|
+
generateBlurhashAsync(source: string | ImageRef, numberOfComponents: [number, number] | {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}): Promise<string | null>;
|
|
20
|
+
generateThumbhashAsync(source: string | ImageRef): Promise<string>;
|
|
21
|
+
addListener(eventName: string, listener: (event: {
|
|
22
|
+
url: string;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}) => void): EventSubscription;
|
|
26
|
+
};
|
|
27
|
+
export declare const expoImage: INativeImageModule;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IImageResizeMode } from './types';
|
|
2
|
+
type IStyleBag = Record<string, unknown>;
|
|
3
|
+
export type IImageStyleParts = {
|
|
4
|
+
resizeMode?: IImageResizeMode;
|
|
5
|
+
fontWeight?: string | number;
|
|
6
|
+
fontSize?: number;
|
|
7
|
+
color?: unknown;
|
|
8
|
+
/** What is left once the text-like keys are taken out */
|
|
9
|
+
rest: IStyleBag;
|
|
10
|
+
};
|
|
11
|
+
/** An Expo view has no attribute processor for colors, so they go through here */
|
|
12
|
+
export declare function toNativeColor(value: unknown): unknown;
|
|
13
|
+
/** `resizeMode` and the SF Symbol keys are props to native, not style */
|
|
14
|
+
export declare function splitImageStyle(style: unknown): IImageStyleParts;
|
|
15
|
+
/** Android shadows by `elevation` and paints the background itself, iOS by `shadow*` */
|
|
16
|
+
export declare function platformStyle(style: IStyleBag): IStyleBag;
|
|
17
|
+
/** `backgroundColor` and the border colors in the form native reads */
|
|
18
|
+
export declare function nativeColorProps(style: IStyleBag): IStyleBag;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Platform } from 'expo-modules-core';
|
|
2
|
+
import { flattenStyle, processColor } from '@symbiote-native/engine';
|
|
3
|
+
const SHADOW_KEYS = [
|
|
4
|
+
'shadowColor',
|
|
5
|
+
'shadowOffset',
|
|
6
|
+
'shadowOpacity',
|
|
7
|
+
'shadowRadius',
|
|
8
|
+
];
|
|
9
|
+
const BORDER_COLOR_KEYS = [
|
|
10
|
+
'borderColor',
|
|
11
|
+
'borderStartColor',
|
|
12
|
+
'borderEndColor',
|
|
13
|
+
'borderLeftColor',
|
|
14
|
+
'borderRightColor',
|
|
15
|
+
'borderTopColor',
|
|
16
|
+
'borderBottomColor',
|
|
17
|
+
];
|
|
18
|
+
const RESIZE_MODES = new Set([
|
|
19
|
+
'cover',
|
|
20
|
+
'contain',
|
|
21
|
+
'stretch',
|
|
22
|
+
'repeat',
|
|
23
|
+
'center',
|
|
24
|
+
]);
|
|
25
|
+
function isResizeMode(value) {
|
|
26
|
+
return RESIZE_MODES.has(value);
|
|
27
|
+
}
|
|
28
|
+
function isColorValue(value) {
|
|
29
|
+
return (typeof value === 'string' ||
|
|
30
|
+
typeof value === 'number' ||
|
|
31
|
+
(typeof value === 'object' && value !== null));
|
|
32
|
+
}
|
|
33
|
+
/** An Expo view has no attribute processor for colors, so they go through here */
|
|
34
|
+
export function toNativeColor(value) {
|
|
35
|
+
return isColorValue(value) ? processColor(value) : undefined;
|
|
36
|
+
}
|
|
37
|
+
/** `resizeMode` and the SF Symbol keys are props to native, not style */
|
|
38
|
+
export function splitImageStyle(style) {
|
|
39
|
+
const { resizeMode, fontWeight, fontSize, color, ...rest } = flattenStyle(style);
|
|
40
|
+
const isWeight = typeof fontWeight === 'string' || typeof fontWeight === 'number';
|
|
41
|
+
return {
|
|
42
|
+
resizeMode: isResizeMode(resizeMode) ? resizeMode : undefined,
|
|
43
|
+
fontWeight: isWeight ? fontWeight : undefined,
|
|
44
|
+
fontSize: typeof fontSize === 'number' ? fontSize : undefined,
|
|
45
|
+
color,
|
|
46
|
+
rest,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function without(bag, keys) {
|
|
50
|
+
return Object.fromEntries(Object.entries(bag).filter(([key]) => !keys.includes(key)));
|
|
51
|
+
}
|
|
52
|
+
/** Android shadows by `elevation` and paints the background itself, iOS by `shadow*` */
|
|
53
|
+
export function platformStyle(style) {
|
|
54
|
+
return Platform.select({
|
|
55
|
+
android: without(style, [...SHADOW_KEYS, 'backgroundColor']),
|
|
56
|
+
default: without(style, ['elevation']),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** `backgroundColor` and the border colors in the form native reads */
|
|
60
|
+
export function nativeColorProps(style) {
|
|
61
|
+
return Object.fromEntries(['backgroundColor', ...BORDER_COLOR_KEYS].map(key => [
|
|
62
|
+
key,
|
|
63
|
+
toNativeColor(style[key]),
|
|
64
|
+
]));
|
|
65
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IImageContentFit, IImageContentPosition, IImageContentPositionObject, IImageResizeMode, IImageTransition, IImageViewProps, ISfSymbolEffectObject } from './types';
|
|
2
|
+
export declare function warnOnce(message: string): void;
|
|
3
|
+
export declare function resetDeprecationWarnings(): void;
|
|
4
|
+
/** An SF Symbol defaults to `contain` to keep its aspect ratio */
|
|
5
|
+
export declare function resolveContentFit(contentFit?: IImageContentFit, resizeMode?: IImageResizeMode, isSfSymbol?: boolean): IImageContentFit;
|
|
6
|
+
/** Native reads only the two-edge object form */
|
|
7
|
+
export declare function resolveContentPosition(contentPosition?: IImageContentPosition): IImageContentPositionObject;
|
|
8
|
+
/** Every form of `sfEffect` becomes a list of effect objects */
|
|
9
|
+
export declare function resolveSfEffect(sfEffect: IImageViewProps['sfEffect']): ISfSymbolEffectObject[] | null;
|
|
10
|
+
/** A number is the duration of a cross dissolve */
|
|
11
|
+
export declare function resolveTransition(transition?: IImageViewProps['transition'], fadeDuration?: IImageViewProps['fadeDuration']): IImageTransition | null;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const CENTER = { top: '50%', left: '50%' };
|
|
2
|
+
const POSITION_BY_KEYWORD = {
|
|
3
|
+
center: CENTER,
|
|
4
|
+
top: { top: 0, left: '50%' },
|
|
5
|
+
right: { top: '50%', right: 0 },
|
|
6
|
+
bottom: { bottom: 0, left: '50%' },
|
|
7
|
+
left: { top: '50%', left: 0 },
|
|
8
|
+
'top center': { top: 0, left: '50%' },
|
|
9
|
+
'top right': { top: 0, right: 0 },
|
|
10
|
+
'top left': { top: 0, left: 0 },
|
|
11
|
+
'right center': { top: '50%', right: 0 },
|
|
12
|
+
'right top': { top: 0, right: 0 },
|
|
13
|
+
'right bottom': { bottom: 0, right: 0 },
|
|
14
|
+
'bottom center': { bottom: 0, left: '50%' },
|
|
15
|
+
'bottom right': { bottom: 0, right: 0 },
|
|
16
|
+
'bottom left': { bottom: 0, left: 0 },
|
|
17
|
+
'left center': { top: '50%', left: 0 },
|
|
18
|
+
'left top': { top: 0, left: 0 },
|
|
19
|
+
'left bottom': { bottom: 0, left: 0 },
|
|
20
|
+
};
|
|
21
|
+
const warned = new Set();
|
|
22
|
+
export function warnOnce(message) {
|
|
23
|
+
if (warned.has(message)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
warned.add(message);
|
|
27
|
+
console.warn(`[expo-image]: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
export function resetDeprecationWarnings() {
|
|
30
|
+
warned.clear();
|
|
31
|
+
}
|
|
32
|
+
function fitFromResizeMode(resizeMode) {
|
|
33
|
+
warnOnce('Prop "resizeMode" is deprecated, use "contentFit" instead');
|
|
34
|
+
switch (resizeMode) {
|
|
35
|
+
case 'contain':
|
|
36
|
+
case 'cover':
|
|
37
|
+
return resizeMode;
|
|
38
|
+
case 'stretch':
|
|
39
|
+
return 'fill';
|
|
40
|
+
case 'center':
|
|
41
|
+
return 'scale-down';
|
|
42
|
+
case 'repeat':
|
|
43
|
+
warnOnce('Resize mode "repeat" is no longer supported');
|
|
44
|
+
return 'cover';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** An SF Symbol defaults to `contain` to keep its aspect ratio */
|
|
48
|
+
export function resolveContentFit(contentFit, resizeMode, isSfSymbol) {
|
|
49
|
+
if (contentFit) {
|
|
50
|
+
return contentFit;
|
|
51
|
+
}
|
|
52
|
+
if (resizeMode) {
|
|
53
|
+
return fitFromResizeMode(resizeMode);
|
|
54
|
+
}
|
|
55
|
+
return isSfSymbol ? 'contain' : 'cover';
|
|
56
|
+
}
|
|
57
|
+
/** Native reads only the two-edge object form */
|
|
58
|
+
export function resolveContentPosition(contentPosition) {
|
|
59
|
+
if (typeof contentPosition !== 'string') {
|
|
60
|
+
return contentPosition ?? CENTER;
|
|
61
|
+
}
|
|
62
|
+
const resolved = POSITION_BY_KEYWORD[contentPosition];
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
warnOnce(`Content position "${contentPosition}" is invalid`);
|
|
65
|
+
return CENTER;
|
|
66
|
+
}
|
|
67
|
+
return resolved;
|
|
68
|
+
}
|
|
69
|
+
/** Every form of `sfEffect` becomes a list of effect objects */
|
|
70
|
+
export function resolveSfEffect(sfEffect) {
|
|
71
|
+
if (sfEffect == null) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const effects = Array.isArray(sfEffect) ? sfEffect : [sfEffect];
|
|
75
|
+
return effects.map(item => typeof item === 'string' ? { effect: item } : item);
|
|
76
|
+
}
|
|
77
|
+
/** A number is the duration of a cross dissolve */
|
|
78
|
+
export function resolveTransition(transition, fadeDuration) {
|
|
79
|
+
if (typeof transition === 'number') {
|
|
80
|
+
return { duration: transition };
|
|
81
|
+
}
|
|
82
|
+
if (!transition && typeof fadeDuration === 'number') {
|
|
83
|
+
warnOnce('Prop "fadeDuration" is deprecated, use "transition" instead');
|
|
84
|
+
return { duration: fadeDuration };
|
|
85
|
+
}
|
|
86
|
+
return transition ?? null;
|
|
87
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SharedRef } from 'expo-modules-core';
|
|
2
|
+
import type { IImageSource, IImageSources } from './types';
|
|
3
|
+
type IImageSourceInput = IImageSource | string | number | null | undefined;
|
|
4
|
+
export declare function isImageRef(value: unknown): value is SharedRef<'image'>;
|
|
5
|
+
export declare function resolveSource(source: IImageSourceInput): IImageSource | null;
|
|
6
|
+
/** The shape the native view takes: a list of sources or a shared object id */
|
|
7
|
+
export declare function resolveSources(sources: IImageSources | undefined): IImageSource[] | number;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { resolveAssetSource } from '@symbiote-native/engine';
|
|
2
|
+
import { SharedRef } from 'expo-modules-core';
|
|
3
|
+
import { SF_SYMBOL_PREFIX } from './constants.js';
|
|
4
|
+
import { resolveBlurhashString, resolveThumbhashString } from './hash-strings.js';
|
|
5
|
+
const BLURHASH_PATTERN = /^(blurhash:\/)+[\w#$%*+,\-.:;=?@[\]^_{}|~]+(\/[\d.]+)*$/;
|
|
6
|
+
const THUMBHASH_PREFIX = 'thumbhash:/';
|
|
7
|
+
// Any native image reference, an `ImageRef` or a `VideoThumbnail`, the view takes both
|
|
8
|
+
export function isImageRef(value) {
|
|
9
|
+
return value instanceof SharedRef && value.nativeRefType === 'image';
|
|
10
|
+
}
|
|
11
|
+
function sharedIdOf(ref) {
|
|
12
|
+
const id = Reflect.get(ref, '__expo_shared_object_id__');
|
|
13
|
+
return typeof id === 'number' ? id : 0;
|
|
14
|
+
}
|
|
15
|
+
function isImageSource(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null;
|
|
17
|
+
}
|
|
18
|
+
function nativeAssetSource(id) {
|
|
19
|
+
const asset = resolveAssetSource(id);
|
|
20
|
+
return isImageSource(asset) ? asset : null;
|
|
21
|
+
}
|
|
22
|
+
export function resolveSource(source) {
|
|
23
|
+
if (typeof source === 'string') {
|
|
24
|
+
if (BLURHASH_PATTERN.test(source)) {
|
|
25
|
+
return resolveBlurhashString(source);
|
|
26
|
+
}
|
|
27
|
+
if (source.startsWith(THUMBHASH_PREFIX)) {
|
|
28
|
+
return resolveThumbhashString(source);
|
|
29
|
+
}
|
|
30
|
+
if (source.startsWith(SF_SYMBOL_PREFIX)) {
|
|
31
|
+
return {
|
|
32
|
+
uri: `${SF_SYMBOL_PREFIX}/${source.slice(SF_SYMBOL_PREFIX.length)}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { uri: source };
|
|
36
|
+
}
|
|
37
|
+
if (typeof source === 'number') {
|
|
38
|
+
return nativeAssetSource(source);
|
|
39
|
+
}
|
|
40
|
+
if (source?.blurhash || source?.thumbhash) {
|
|
41
|
+
const { blurhash, thumbhash, ...rest } = source;
|
|
42
|
+
const resolved = thumbhash
|
|
43
|
+
? resolveThumbhashString(thumbhash)
|
|
44
|
+
: resolveBlurhashString(blurhash ?? '');
|
|
45
|
+
return { ...resolved, ...rest };
|
|
46
|
+
}
|
|
47
|
+
return source ?? null;
|
|
48
|
+
}
|
|
49
|
+
function isPresent(source) {
|
|
50
|
+
return source !== null;
|
|
51
|
+
}
|
|
52
|
+
/** The shape the native view takes: a list of sources or a shared object id */
|
|
53
|
+
export function resolveSources(sources) {
|
|
54
|
+
if (Array.isArray(sources)) {
|
|
55
|
+
return sources.map(resolveSource).filter(isPresent);
|
|
56
|
+
}
|
|
57
|
+
if (isImageRef(sources)) {
|
|
58
|
+
return sharedIdOf(sources);
|
|
59
|
+
}
|
|
60
|
+
return [resolveSource(sources)].filter(isPresent);
|
|
61
|
+
}
|