@symbiote-native/image 0.0.0-stage → 0.0.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 +45 -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 +107 -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 +20 -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 +26 -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 +32 -0
- package/build/core/image-view.d.ts +14 -0
- package/build/core/image-view.js +111 -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 +62 -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 +59 -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 +66 -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 +21 -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 +27 -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 +33 -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 +112 -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 +63 -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 +60 -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 +78 -0
- package/src/angular/image-background/index.ts +69 -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 +27 -0
- package/src/core/image-api.ts +67 -0
- package/src/core/image-background.ts +42 -0
- package/src/core/image-loader.ts +65 -0
- package/src/core/image-ref-loader.ts +16 -0
- package/src/core/image-view-events.ts +41 -0
- package/src/core/image-view.ts +156 -0
- package/src/core/index.ts +50 -0
- package/src/core/native-module.ts +45 -0
- package/src/core/native-style.ts +89 -0
- package/src/core/props.ts +118 -0
- package/src/core/sources.ts +70 -0
- package/src/core/types.ts +278 -0
- package/src/react/image-background.ts +14 -0
- package/src/react/image.ts +14 -0
- package/src/react/index.ts +8 -0
- package/src/react/use-image.ts +29 -0
- package/src/solid/image-background.ts +13 -0
- package/src/solid/image.ts +8 -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 +34 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Platform } from 'expo-modules-core';
|
|
2
|
+
import { flattenStyle, processColor } from '@symbiote-native/engine';
|
|
3
|
+
import type { IColorValue, IViewStyle } from '@symbiote-native/engine';
|
|
4
|
+
import type { IImageResizeMode } from './types';
|
|
5
|
+
|
|
6
|
+
type IStyleBag = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
const SHADOW_KEYS = [
|
|
9
|
+
'shadowColor',
|
|
10
|
+
'shadowOffset',
|
|
11
|
+
'shadowOpacity',
|
|
12
|
+
'shadowRadius',
|
|
13
|
+
] as const satisfies readonly (keyof IViewStyle)[];
|
|
14
|
+
|
|
15
|
+
const BORDER_COLOR_KEYS = [
|
|
16
|
+
'borderColor',
|
|
17
|
+
'borderStartColor',
|
|
18
|
+
'borderEndColor',
|
|
19
|
+
'borderLeftColor',
|
|
20
|
+
'borderRightColor',
|
|
21
|
+
'borderTopColor',
|
|
22
|
+
'borderBottomColor',
|
|
23
|
+
] as const satisfies readonly (keyof IViewStyle)[];
|
|
24
|
+
|
|
25
|
+
const RESIZE_MODES = new Set<unknown>([
|
|
26
|
+
'cover',
|
|
27
|
+
'contain',
|
|
28
|
+
'stretch',
|
|
29
|
+
'repeat',
|
|
30
|
+
'center',
|
|
31
|
+
] as const satisfies readonly IImageResizeMode[]);
|
|
32
|
+
|
|
33
|
+
export type IImageStyleParts = {
|
|
34
|
+
resizeMode?: IImageResizeMode;
|
|
35
|
+
fontWeight?: string | number;
|
|
36
|
+
fontSize?: number;
|
|
37
|
+
color?: unknown;
|
|
38
|
+
/** What is left once the text-like keys are taken out */
|
|
39
|
+
rest: IStyleBag;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function isResizeMode(value: unknown): value is IImageResizeMode {
|
|
43
|
+
return RESIZE_MODES.has(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isColorValue(value: unknown): value is IColorValue {
|
|
47
|
+
return (
|
|
48
|
+
typeof value === 'string' ||
|
|
49
|
+
typeof value === 'number' ||
|
|
50
|
+
(typeof value === 'object' && value !== null)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** An Expo view has no attribute processor for colors, so they go through here */
|
|
55
|
+
export function toNativeColor(value: unknown): unknown {
|
|
56
|
+
return isColorValue(value) ? processColor(value) : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `resizeMode` and the SF Symbol keys are props to native, not style */
|
|
60
|
+
export function splitImageStyle(style: unknown): IImageStyleParts {
|
|
61
|
+
const { resizeMode, fontWeight, fontSize, color, ...rest } = flattenStyle(style);
|
|
62
|
+
const isWeight = typeof fontWeight === 'string' || typeof fontWeight === 'number';
|
|
63
|
+
return {
|
|
64
|
+
resizeMode: isResizeMode(resizeMode) ? resizeMode : undefined,
|
|
65
|
+
fontWeight: isWeight ? fontWeight : undefined,
|
|
66
|
+
fontSize: typeof fontSize === 'number' ? fontSize : undefined,
|
|
67
|
+
color,
|
|
68
|
+
rest,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function without(bag: IStyleBag, keys: readonly string[]): IStyleBag {
|
|
73
|
+
return Object.fromEntries(Object.entries(bag).filter(([key]) => !keys.includes(key)));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Android shadows by `elevation` and paints the background itself, iOS by `shadow*` */
|
|
77
|
+
export function platformStyle(style: IStyleBag): IStyleBag {
|
|
78
|
+
return Platform.select({
|
|
79
|
+
android: without(style, [...SHADOW_KEYS, 'backgroundColor']),
|
|
80
|
+
default: without(style, ['elevation']),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `backgroundColor` and the border colors in the form native reads */
|
|
85
|
+
export function nativeColorProps(style: IStyleBag): IStyleBag {
|
|
86
|
+
return Object.fromEntries(
|
|
87
|
+
['backgroundColor', ...BORDER_COLOR_KEYS].map(key => [key, toNativeColor(style[key])]),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IImageContentFit,
|
|
3
|
+
IImageContentPosition,
|
|
4
|
+
IImageContentPositionObject,
|
|
5
|
+
IImageContentPositionString,
|
|
6
|
+
IImageResizeMode,
|
|
7
|
+
IImageTransition,
|
|
8
|
+
IImageViewProps,
|
|
9
|
+
ISfSymbolEffectObject,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
const CENTER: IImageContentPositionObject = { top: '50%', left: '50%' };
|
|
13
|
+
|
|
14
|
+
const POSITION_BY_KEYWORD: Record<IImageContentPositionString, IImageContentPositionObject> = {
|
|
15
|
+
center: CENTER,
|
|
16
|
+
top: { top: 0, left: '50%' },
|
|
17
|
+
right: { top: '50%', right: 0 },
|
|
18
|
+
bottom: { bottom: 0, left: '50%' },
|
|
19
|
+
left: { top: '50%', left: 0 },
|
|
20
|
+
'top center': { top: 0, left: '50%' },
|
|
21
|
+
'top right': { top: 0, right: 0 },
|
|
22
|
+
'top left': { top: 0, left: 0 },
|
|
23
|
+
'right center': { top: '50%', right: 0 },
|
|
24
|
+
'right top': { top: 0, right: 0 },
|
|
25
|
+
'right bottom': { bottom: 0, right: 0 },
|
|
26
|
+
'bottom center': { bottom: 0, left: '50%' },
|
|
27
|
+
'bottom right': { bottom: 0, right: 0 },
|
|
28
|
+
'bottom left': { bottom: 0, left: 0 },
|
|
29
|
+
'left center': { top: '50%', left: 0 },
|
|
30
|
+
'left top': { top: 0, left: 0 },
|
|
31
|
+
'left bottom': { bottom: 0, left: 0 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const warned = new Set<string>();
|
|
35
|
+
|
|
36
|
+
export function warnOnce(message: string): void {
|
|
37
|
+
if (warned.has(message)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
warned.add(message);
|
|
41
|
+
console.warn(`[expo-image]: ${message}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resetDeprecationWarnings(): void {
|
|
45
|
+
warned.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fitFromResizeMode(resizeMode: IImageResizeMode): IImageContentFit {
|
|
49
|
+
warnOnce('Prop "resizeMode" is deprecated, use "contentFit" instead');
|
|
50
|
+
switch (resizeMode) {
|
|
51
|
+
case 'contain':
|
|
52
|
+
case 'cover':
|
|
53
|
+
return resizeMode;
|
|
54
|
+
case 'stretch':
|
|
55
|
+
return 'fill';
|
|
56
|
+
case 'center':
|
|
57
|
+
return 'scale-down';
|
|
58
|
+
case 'repeat':
|
|
59
|
+
warnOnce('Resize mode "repeat" is no longer supported');
|
|
60
|
+
return 'cover';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** An SF Symbol defaults to `contain` to keep its aspect ratio */
|
|
65
|
+
export function resolveContentFit(
|
|
66
|
+
contentFit?: IImageContentFit,
|
|
67
|
+
resizeMode?: IImageResizeMode,
|
|
68
|
+
isSfSymbol?: boolean,
|
|
69
|
+
): IImageContentFit {
|
|
70
|
+
if (contentFit) {
|
|
71
|
+
return contentFit;
|
|
72
|
+
}
|
|
73
|
+
if (resizeMode) {
|
|
74
|
+
return fitFromResizeMode(resizeMode);
|
|
75
|
+
}
|
|
76
|
+
return isSfSymbol ? 'contain' : 'cover';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Native reads only the two-edge object form */
|
|
80
|
+
export function resolveContentPosition(
|
|
81
|
+
contentPosition?: IImageContentPosition,
|
|
82
|
+
): IImageContentPositionObject {
|
|
83
|
+
if (typeof contentPosition !== 'string') {
|
|
84
|
+
return contentPosition ?? CENTER;
|
|
85
|
+
}
|
|
86
|
+
const resolved = POSITION_BY_KEYWORD[contentPosition];
|
|
87
|
+
if (!resolved) {
|
|
88
|
+
warnOnce(`Content position "${contentPosition}" is invalid`);
|
|
89
|
+
return CENTER;
|
|
90
|
+
}
|
|
91
|
+
return resolved;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Every form of `sfEffect` becomes a list of effect objects */
|
|
95
|
+
export function resolveSfEffect(
|
|
96
|
+
sfEffect: IImageViewProps['sfEffect'],
|
|
97
|
+
): ISfSymbolEffectObject[] | null {
|
|
98
|
+
if (sfEffect == null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const effects = Array.isArray(sfEffect) ? sfEffect : [sfEffect];
|
|
102
|
+
return effects.map(item => (typeof item === 'string' ? { effect: item } : item));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A number is the duration of a cross dissolve */
|
|
106
|
+
export function resolveTransition(
|
|
107
|
+
transition?: IImageViewProps['transition'],
|
|
108
|
+
fadeDuration?: IImageViewProps['fadeDuration'],
|
|
109
|
+
): IImageTransition | null {
|
|
110
|
+
if (typeof transition === 'number') {
|
|
111
|
+
return { duration: transition };
|
|
112
|
+
}
|
|
113
|
+
if (!transition && typeof fadeDuration === 'number') {
|
|
114
|
+
warnOnce('Prop "fadeDuration" is deprecated, use "transition" instead');
|
|
115
|
+
return { duration: fadeDuration };
|
|
116
|
+
}
|
|
117
|
+
return transition ?? null;
|
|
118
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { resolveAssetSource } from '@symbiote-native/engine';
|
|
2
|
+
import { SharedRef } from 'expo-modules-core';
|
|
3
|
+
import { SF_SYMBOL_PREFIX } from './constants';
|
|
4
|
+
import { resolveBlurhashString, resolveThumbhashString } from './hash-strings';
|
|
5
|
+
import type { IImageSource, IImageSources } from './types';
|
|
6
|
+
|
|
7
|
+
const BLURHASH_PATTERN = /^(blurhash:\/)+[\w#$%*+,\-.:;=?@[\]^_{}|~]+(\/[\d.]+)*$/;
|
|
8
|
+
const THUMBHASH_PREFIX = 'thumbhash:/';
|
|
9
|
+
|
|
10
|
+
type IImageSourceInput = IImageSource | string | number | null | undefined;
|
|
11
|
+
|
|
12
|
+
// Any native image reference, an `ImageRef` or a `VideoThumbnail`, the view takes both
|
|
13
|
+
export function isImageRef(value: unknown): value is SharedRef<'image'> {
|
|
14
|
+
return value instanceof SharedRef && value.nativeRefType === 'image';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sharedIdOf(ref: SharedRef<'image'>): number {
|
|
18
|
+
const id: unknown = Reflect.get(ref, '__expo_shared_object_id__');
|
|
19
|
+
return typeof id === 'number' ? id : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isImageSource(value: unknown): value is IImageSource {
|
|
23
|
+
return typeof value === 'object' && value !== null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function nativeAssetSource(id: number): IImageSource | null {
|
|
27
|
+
const asset = resolveAssetSource(id);
|
|
28
|
+
return isImageSource(asset) ? asset : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveSource(source: IImageSourceInput): IImageSource | null {
|
|
32
|
+
if (typeof source === 'string') {
|
|
33
|
+
if (BLURHASH_PATTERN.test(source)) {
|
|
34
|
+
return resolveBlurhashString(source);
|
|
35
|
+
}
|
|
36
|
+
if (source.startsWith(THUMBHASH_PREFIX)) {
|
|
37
|
+
return resolveThumbhashString(source);
|
|
38
|
+
}
|
|
39
|
+
if (source.startsWith(SF_SYMBOL_PREFIX)) {
|
|
40
|
+
return { uri: `${SF_SYMBOL_PREFIX}/${source.slice(SF_SYMBOL_PREFIX.length)}` };
|
|
41
|
+
}
|
|
42
|
+
return { uri: source };
|
|
43
|
+
}
|
|
44
|
+
if (typeof source === 'number') {
|
|
45
|
+
return nativeAssetSource(source);
|
|
46
|
+
}
|
|
47
|
+
if (source?.blurhash || source?.thumbhash) {
|
|
48
|
+
const { blurhash, thumbhash, ...rest } = source;
|
|
49
|
+
const resolved = thumbhash
|
|
50
|
+
? resolveThumbhashString(thumbhash)
|
|
51
|
+
: resolveBlurhashString(blurhash ?? '');
|
|
52
|
+
return { ...resolved, ...rest };
|
|
53
|
+
}
|
|
54
|
+
return source ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isPresent(source: IImageSource | null): source is IImageSource {
|
|
58
|
+
return source !== null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The shape the native view takes: a list of sources or a shared object id */
|
|
62
|
+
export function resolveSources(sources: IImageSources | undefined): IImageSource[] | number {
|
|
63
|
+
if (Array.isArray(sources)) {
|
|
64
|
+
return sources.map(resolveSource).filter(isPresent);
|
|
65
|
+
}
|
|
66
|
+
if (isImageRef(sources)) {
|
|
67
|
+
return sharedIdOf(sources);
|
|
68
|
+
}
|
|
69
|
+
return [resolveSource(sources)].filter(isPresent);
|
|
70
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IAccessibilityProps,
|
|
3
|
+
IAriaProps,
|
|
4
|
+
IResponderProps,
|
|
5
|
+
} from '@symbiote-native/components';
|
|
6
|
+
import type {
|
|
7
|
+
IColorValue,
|
|
8
|
+
IStyleProp,
|
|
9
|
+
ITextStyle,
|
|
10
|
+
IViewStyle,
|
|
11
|
+
} from '@symbiote-native/engine';
|
|
12
|
+
import type { SharedRef } from 'expo-modules-core';
|
|
13
|
+
import type { SFSymbol } from 'sf-symbols-typescript';
|
|
14
|
+
|
|
15
|
+
export type IImageSource = {
|
|
16
|
+
/** `http(s)`, `file`, `data`, an asset URI and the `sf:/` and `blurhash:/` schemes */
|
|
17
|
+
uri?: string;
|
|
18
|
+
/** Sent with the request */
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
/** Used for layout while the image loads, and the size of a hash placeholder */
|
|
21
|
+
width?: number | null;
|
|
22
|
+
height?: number | null;
|
|
23
|
+
/** A Blurhash string, the source becomes a `blurhash:/` uri */
|
|
24
|
+
blurhash?: string;
|
|
25
|
+
/** A Thumbhash string, the source becomes a `thumbhash:/` uri */
|
|
26
|
+
thumbhash?: string;
|
|
27
|
+
/** The key of the cache entry, the uri by default */
|
|
28
|
+
cacheKey?: string;
|
|
29
|
+
/** Web only */
|
|
30
|
+
webMaxViewportWidth?: number;
|
|
31
|
+
/** Plays the image as an animation, detected from the content by default */
|
|
32
|
+
isAnimated?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type IImageContentFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
|
|
36
|
+
|
|
37
|
+
export type IImageDecodeFormat = 'argb' | 'rgb';
|
|
38
|
+
|
|
39
|
+
export type IImageResizeMode = 'cover' | 'contain' | 'stretch' | 'repeat' | 'center';
|
|
40
|
+
|
|
41
|
+
export type IImageContentPositionValue =
|
|
42
|
+
| number
|
|
43
|
+
| string
|
|
44
|
+
| `${number}%`
|
|
45
|
+
| `${number}`
|
|
46
|
+
| 'center';
|
|
47
|
+
|
|
48
|
+
export type IImageContentPositionString =
|
|
49
|
+
| 'center'
|
|
50
|
+
| 'top'
|
|
51
|
+
| 'right'
|
|
52
|
+
| 'bottom'
|
|
53
|
+
| 'left'
|
|
54
|
+
| 'top center'
|
|
55
|
+
| 'top right'
|
|
56
|
+
| 'top left'
|
|
57
|
+
| 'right center'
|
|
58
|
+
| 'right top'
|
|
59
|
+
| 'right bottom'
|
|
60
|
+
| 'bottom center'
|
|
61
|
+
| 'bottom right'
|
|
62
|
+
| 'bottom left'
|
|
63
|
+
| 'left center'
|
|
64
|
+
| 'left top'
|
|
65
|
+
| 'left bottom';
|
|
66
|
+
|
|
67
|
+
/** Two edges, the native side takes only this form */
|
|
68
|
+
export type IImageContentPositionObject =
|
|
69
|
+
| { top?: IImageContentPositionValue; right?: IImageContentPositionValue }
|
|
70
|
+
| { top?: IImageContentPositionValue; left?: IImageContentPositionValue }
|
|
71
|
+
| { bottom?: IImageContentPositionValue; right?: IImageContentPositionValue }
|
|
72
|
+
| { bottom?: IImageContentPositionValue; left?: IImageContentPositionValue };
|
|
73
|
+
|
|
74
|
+
export type IImageContentPosition =
|
|
75
|
+
| IImageContentPositionObject
|
|
76
|
+
| IImageContentPositionString;
|
|
77
|
+
|
|
78
|
+
export type IImageTransition = {
|
|
79
|
+
/** In milliseconds */
|
|
80
|
+
duration?: number;
|
|
81
|
+
timing?: 'ease-in-out' | 'ease-in' | 'ease-out' | 'linear';
|
|
82
|
+
effect?:
|
|
83
|
+
| 'cross-dissolve'
|
|
84
|
+
| 'flip-from-top'
|
|
85
|
+
| 'flip-from-right'
|
|
86
|
+
| 'flip-from-bottom'
|
|
87
|
+
| 'flip-from-left'
|
|
88
|
+
| 'curl-up'
|
|
89
|
+
| 'curl-down'
|
|
90
|
+
| 'sf:replace'
|
|
91
|
+
| 'sf:down-up'
|
|
92
|
+
| 'sf:up-up'
|
|
93
|
+
| 'sf:off-up'
|
|
94
|
+
| null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type ISfSymbolEffectType =
|
|
98
|
+
| 'bounce'
|
|
99
|
+
| 'bounce/up'
|
|
100
|
+
| 'bounce/down'
|
|
101
|
+
| 'pulse'
|
|
102
|
+
| 'variable-color'
|
|
103
|
+
| 'variable-color/iterative'
|
|
104
|
+
| 'variable-color/cumulative'
|
|
105
|
+
| 'scale'
|
|
106
|
+
| 'scale/up'
|
|
107
|
+
| 'scale/down'
|
|
108
|
+
| 'appear'
|
|
109
|
+
| 'disappear'
|
|
110
|
+
| 'wiggle'
|
|
111
|
+
| 'rotate'
|
|
112
|
+
| 'breathe'
|
|
113
|
+
| 'draw/on'
|
|
114
|
+
| 'draw/off';
|
|
115
|
+
|
|
116
|
+
export type ISfSymbolEffectObject = {
|
|
117
|
+
effect: ISfSymbolEffectType;
|
|
118
|
+
/** `-1` repeats forever */
|
|
119
|
+
repeat?: number;
|
|
120
|
+
scope?: 'by-layer' | 'whole-symbol' | null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** iOS 17+ for an SF Symbol source */
|
|
124
|
+
export type ISfSymbolEffect =
|
|
125
|
+
| ISfSymbolEffectType
|
|
126
|
+
| ISfSymbolEffectObject
|
|
127
|
+
| (ISfSymbolEffectType | ISfSymbolEffectObject)[];
|
|
128
|
+
|
|
129
|
+
export type IImageLoadEventData = {
|
|
130
|
+
cacheType: 'none' | 'disk' | 'memory';
|
|
131
|
+
source: {
|
|
132
|
+
url: string;
|
|
133
|
+
width: number;
|
|
134
|
+
height: number;
|
|
135
|
+
mediaType: string | null;
|
|
136
|
+
isAnimated?: boolean;
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type IImageProgressEventData = { loaded: number; total: number };
|
|
141
|
+
|
|
142
|
+
export type IImageErrorEventData = { error: string };
|
|
143
|
+
|
|
144
|
+
export type IImagePrefetchOptions = {
|
|
145
|
+
/** `memory-disk` by default */
|
|
146
|
+
cachePolicy?: 'disk' | 'memory-disk' | 'memory';
|
|
147
|
+
headers?: Record<string, string>;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type IImageCacheConfig = {
|
|
151
|
+
/** In bytes */
|
|
152
|
+
maxDiskSize?: number;
|
|
153
|
+
/** In bytes */
|
|
154
|
+
maxMemoryCost?: number;
|
|
155
|
+
maxMemoryCount?: number;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type IImageLoadOptions = {
|
|
159
|
+
maxWidth?: number;
|
|
160
|
+
maxHeight?: number;
|
|
161
|
+
tintColor?: IColorValue | number;
|
|
162
|
+
/** `retry` loads again */
|
|
163
|
+
onError?(error: Error, retry: () => void): void;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** A reference to a native image, an image view takes it as its source */
|
|
167
|
+
export declare class ImageRef extends SharedRef<'image'> {
|
|
168
|
+
readonly width: number;
|
|
169
|
+
readonly height: number;
|
|
170
|
+
readonly scale: number;
|
|
171
|
+
readonly mediaType: string | null;
|
|
172
|
+
readonly isAnimated?: boolean;
|
|
173
|
+
/** The id the native view looks the shared object up by */
|
|
174
|
+
readonly __expo_shared_object_id__: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type IImageStyle = IViewStyle & {
|
|
178
|
+
tintColor?: IColorValue;
|
|
179
|
+
resizeMode?: IImageResizeMode;
|
|
180
|
+
/** With `color` and `fontSize` for an SF Symbol */
|
|
181
|
+
fontWeight?: ITextStyle['fontWeight'];
|
|
182
|
+
color?: IColorValue;
|
|
183
|
+
fontSize?: number;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export type IImageSources =
|
|
187
|
+
| IImageSource
|
|
188
|
+
| `sf:${SFSymbol}`
|
|
189
|
+
| (string & {})
|
|
190
|
+
| number
|
|
191
|
+
| IImageSource[]
|
|
192
|
+
| string[]
|
|
193
|
+
| SharedRef<'image'>
|
|
194
|
+
| null;
|
|
195
|
+
|
|
196
|
+
export type IImageViewProps = IAccessibilityProps &
|
|
197
|
+
IAriaProps &
|
|
198
|
+
IResponderProps & {
|
|
199
|
+
style?: IStyleProp<IImageStyle>;
|
|
200
|
+
/** A uri, an asset, a source object, a list of them or an `ImageRef` */
|
|
201
|
+
source?: IImageSources;
|
|
202
|
+
/** Shown while `source` loads */
|
|
203
|
+
placeholder?:
|
|
204
|
+
| IImageSource
|
|
205
|
+
| string
|
|
206
|
+
| number
|
|
207
|
+
| IImageSource[]
|
|
208
|
+
| string[]
|
|
209
|
+
| SharedRef<'image'>
|
|
210
|
+
| null;
|
|
211
|
+
/** `cover` by default, `contain` for an SF Symbol */
|
|
212
|
+
contentFit?: IImageContentFit;
|
|
213
|
+
placeholderContentFit?: IImageContentFit;
|
|
214
|
+
/** `center` by default */
|
|
215
|
+
contentPosition?: IImageContentPosition;
|
|
216
|
+
/** A number is the duration of a cross dissolve */
|
|
217
|
+
transition?: IImageTransition | number | null;
|
|
218
|
+
blurRadius?: number;
|
|
219
|
+
tintColor?: IColorValue | null;
|
|
220
|
+
priority?: 'low' | 'normal' | 'high' | null;
|
|
221
|
+
loading?: 'lazy' | 'eager';
|
|
222
|
+
cachePolicy?: 'none' | 'disk' | 'memory' | 'memory-disk' | null;
|
|
223
|
+
/** How the image follows a changing view size */
|
|
224
|
+
responsivePolicy?: 'live' | 'initial' | 'static';
|
|
225
|
+
/** Lets a recycled list cell tell its images apart */
|
|
226
|
+
recyclingKey?: string | null;
|
|
227
|
+
/** Plays an animated image on its own, `true` by default */
|
|
228
|
+
autoplay?: boolean;
|
|
229
|
+
/** An effect of an SF Symbol source, iOS 17+ */
|
|
230
|
+
sfEffect?: ISfSymbolEffect | null;
|
|
231
|
+
onLoadStart?: () => void;
|
|
232
|
+
onLoad?: (event: IImageLoadEventData) => void;
|
|
233
|
+
onProgress?: (event: IImageProgressEventData) => void;
|
|
234
|
+
onError?: (event: IImageErrorEventData) => void;
|
|
235
|
+
onLoadEnd?: () => void;
|
|
236
|
+
onDisplay?: () => void;
|
|
237
|
+
/** @deprecated Use `placeholder` */
|
|
238
|
+
defaultSource?: IImageSource | null;
|
|
239
|
+
/** @deprecated Use `placeholder` */
|
|
240
|
+
loadingIndicatorSource?: IImageSource | null;
|
|
241
|
+
/** @deprecated Use `contentFit` */
|
|
242
|
+
resizeMode?: IImageResizeMode;
|
|
243
|
+
/** @deprecated Use `transition` */
|
|
244
|
+
fadeDuration?: number;
|
|
245
|
+
focusable?: boolean;
|
|
246
|
+
accessible?: boolean;
|
|
247
|
+
accessibilityLabel?: string;
|
|
248
|
+
/** The accessibility label when none is given */
|
|
249
|
+
alt?: string;
|
|
250
|
+
/** iOS only, lets the user select text in the image */
|
|
251
|
+
enableLiveTextInteraction?: boolean;
|
|
252
|
+
/** Downscales a larger image to the view, `true` by default */
|
|
253
|
+
allowDownscaling?: boolean;
|
|
254
|
+
/** Android only, `argb` by default */
|
|
255
|
+
decodeFormat?: IImageDecodeFormat;
|
|
256
|
+
/** iOS only */
|
|
257
|
+
useAppleWebpCodec?: boolean;
|
|
258
|
+
/** Android only */
|
|
259
|
+
enforceEarlyResizing?: boolean;
|
|
260
|
+
/** iOS only */
|
|
261
|
+
preferHighDynamicRange?: boolean;
|
|
262
|
+
/** Web only */
|
|
263
|
+
draggable?: boolean;
|
|
264
|
+
testID?: string;
|
|
265
|
+
nativeID?: string;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** What a ref to the view hands out */
|
|
269
|
+
export type IImageViewHandle = {
|
|
270
|
+
/** Starts an animated image */
|
|
271
|
+
startAnimating(): Promise<void>;
|
|
272
|
+
stopAnimating(): Promise<void>;
|
|
273
|
+
/** Keeps the resource from being reloaded */
|
|
274
|
+
lockResourceAsync(): Promise<void>;
|
|
275
|
+
unlockResourceAsync(): Promise<void>;
|
|
276
|
+
/** Reloads the resource, the lock is ignored */
|
|
277
|
+
reloadAsync(): Promise<void>;
|
|
278
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
|
+
import { descriptorToReactWithChildren } from '@symbiote-native/react';
|
|
3
|
+
import { renderImageBackground } from '../core';
|
|
4
|
+
import type { IImageBackgroundProps } from '../core';
|
|
5
|
+
|
|
6
|
+
export type IImageBackgroundReactProps = IImageBackgroundProps & {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** React twin of `expo-image`'s `ImageBackground`, children paint over the image */
|
|
11
|
+
export function ImageBackground(props: IImageBackgroundReactProps): ReactElement {
|
|
12
|
+
const { children, ...rest } = props;
|
|
13
|
+
return descriptorToReactWithChildren(renderImageBackground(rest), children);
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactElement, Ref } from 'react';
|
|
2
|
+
import { useNativeViewController } from '@symbiote-native/react';
|
|
3
|
+
import { createImageView } from '../core';
|
|
4
|
+
import type { IImageViewHandle, IImageViewProps } from '../core';
|
|
5
|
+
|
|
6
|
+
export type IImageReactProps = IImageViewProps & {
|
|
7
|
+
ref?: Ref<IImageViewHandle>;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** React twin of `expo-image`'s `Image` */
|
|
12
|
+
export function Image({ ref, ...props }: IImageReactProps): ReactElement | null {
|
|
13
|
+
return useNativeViewController(ref, createImageView, props);
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @symbiote-native/image/react: the image view, its cache and hash functions on a shared core
|
|
2
|
+
|
|
3
|
+
export { Image } from './image';
|
|
4
|
+
export type { IImageReactProps } from './image';
|
|
5
|
+
export { ImageBackground } from './image-background';
|
|
6
|
+
export type { IImageBackgroundReactProps } from './image-background';
|
|
7
|
+
export { useImage } from './use-image';
|
|
8
|
+
export * from '../core';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { DependencyList } from 'react';
|
|
3
|
+
import { createImageRefLoader, resolveSource } from '../core';
|
|
4
|
+
import type { IImageLoadOptions, IImageSource, ImageRef } from '../core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Loads an image into a native `ImageRef`, `null` until the first one has loaded
|
|
8
|
+
*
|
|
9
|
+
* It loads again when the uri of the source or an item of `dependencies` changes
|
|
10
|
+
*/
|
|
11
|
+
export function useImage(
|
|
12
|
+
source: IImageSource | string | number,
|
|
13
|
+
options: IImageLoadOptions = {},
|
|
14
|
+
dependencies: DependencyList = [],
|
|
15
|
+
): ImageRef | null {
|
|
16
|
+
const [image, setImage] = useState<ImageRef | null>(null);
|
|
17
|
+
// The effect reads the options of the latest render, not the ones it started with
|
|
18
|
+
const latestOptions = useRef(options);
|
|
19
|
+
latestOptions.current = options;
|
|
20
|
+
const key = resolveSource(source)?.uri;
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const loader = createImageRefLoader(setImage, () => latestOptions.current);
|
|
24
|
+
loader.load(source);
|
|
25
|
+
return loader.cancel;
|
|
26
|
+
}, [key, ...dependencies]);
|
|
27
|
+
|
|
28
|
+
return image;
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js';
|
|
2
|
+
import { defineDescriptorComponent } from '@symbiote-native/solid';
|
|
3
|
+
import { renderImageBackground } from '../core';
|
|
4
|
+
import type { IImageBackgroundProps } from '../core';
|
|
5
|
+
|
|
6
|
+
export type IImageBackgroundSolidProps = IImageBackgroundProps & {
|
|
7
|
+
children?: JSX.Element;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Solid twin of `expo-image`'s `ImageBackground`, children paint over the image */
|
|
11
|
+
export const ImageBackground = defineDescriptorComponent<IImageBackgroundSolidProps>(
|
|
12
|
+
renderImageBackground,
|
|
13
|
+
);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineNativeViewComponent } from '@symbiote-native/solid';
|
|
2
|
+
import { createImageView } from '../core';
|
|
3
|
+
import type { IImageViewHandle, IImageViewProps } from '../core';
|
|
4
|
+
|
|
5
|
+
/** Solid twin of `expo-image`'s `Image`, `ref` gets the handle of the view */
|
|
6
|
+
export const Image = defineNativeViewComponent<IImageViewHandle, IImageViewProps>(
|
|
7
|
+
createImageView,
|
|
8
|
+
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @symbiote-native/image/solid: the image view, its cache and hash functions on a shared core
|
|
2
|
+
|
|
3
|
+
export { Image } from './image';
|
|
4
|
+
export { ImageBackground } from './image-background';
|
|
5
|
+
export type { IImageBackgroundSolidProps } from './image-background';
|
|
6
|
+
export { useImage } from './use-image';
|
|
7
|
+
export * from '../core';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createEffect, createSignal, on, onCleanup } from 'solid-js';
|
|
2
|
+
import type { Accessor } from 'solid-js';
|
|
3
|
+
import { createImageRefLoader, resolveSource } from '../core';
|
|
4
|
+
import type { IImageLoadOptions, IImageSource, ImageRef } from '../core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Solid twin of `expo-image`'s `useImage`, `null` until the first image has loaded
|
|
8
|
+
*
|
|
9
|
+
* It loads again when the uri of the source or a `dependencies` accessor changes
|
|
10
|
+
*/
|
|
11
|
+
export function useImage(
|
|
12
|
+
source: Accessor<IImageSource | string | number>,
|
|
13
|
+
options: Accessor<IImageLoadOptions> = () => ({}),
|
|
14
|
+
dependencies: Accessor<unknown>[] = [],
|
|
15
|
+
): Accessor<ImageRef | null> {
|
|
16
|
+
const [image, setImage] = createSignal<ImageRef | null>(null);
|
|
17
|
+
|
|
18
|
+
createEffect(
|
|
19
|
+
on(
|
|
20
|
+
() => [resolveSource(source())?.uri, ...dependencies.map(read => read())],
|
|
21
|
+
() => {
|
|
22
|
+
const loader = createImageRefLoader(setImage, options);
|
|
23
|
+
loader.load(source());
|
|
24
|
+
onCleanup(loader.cancel);
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return image;
|
|
30
|
+
}
|