@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.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -2
  3. package/build/angular/image/index.d.ts +20 -0
  4. package/build/angular/image/index.js +113 -0
  5. package/build/angular/image-background/index.d.ts +14 -0
  6. package/build/angular/image-background/index.js +107 -0
  7. package/build/angular/index.d.ts +4 -0
  8. package/build/angular/index.js +6 -0
  9. package/build/angular/inject-image.d.ts +8 -0
  10. package/build/angular/inject-image.js +20 -0
  11. package/build/core/constants.d.ts +3 -0
  12. package/build/core/constants.js +4 -0
  13. package/build/core/hash-strings.d.ts +5 -0
  14. package/build/core/hash-strings.js +20 -0
  15. package/build/core/image-api.d.ts +18 -0
  16. package/build/core/image-api.js +30 -0
  17. package/build/core/image-background.d.ts +11 -0
  18. package/build/core/image-background.js +26 -0
  19. package/build/core/image-loader.d.ts +20 -0
  20. package/build/core/image-loader.js +42 -0
  21. package/build/core/image-ref-loader.d.ts +4 -0
  22. package/build/core/image-ref-loader.js +10 -0
  23. package/build/core/image-view-events.d.ts +9 -0
  24. package/build/core/image-view-events.js +32 -0
  25. package/build/core/image-view.d.ts +14 -0
  26. package/build/core/image-view.js +111 -0
  27. package/build/core/index.d.ts +10 -0
  28. package/build/core/index.js +7 -0
  29. package/build/core/native-module.d.ts +27 -0
  30. package/build/core/native-module.js +3 -0
  31. package/build/core/native-style.d.ts +19 -0
  32. package/build/core/native-style.js +62 -0
  33. package/build/core/props.d.ts +11 -0
  34. package/build/core/props.js +87 -0
  35. package/build/core/sources.d.ts +8 -0
  36. package/build/core/sources.js +59 -0
  37. package/build/core/types.d.ts +186 -0
  38. package/build/core/types.js +1 -0
  39. package/build/react/image-background.d.ts +7 -0
  40. package/build/react/image-background.js +7 -0
  41. package/build/react/image.d.ts +8 -0
  42. package/build/react/image.js +6 -0
  43. package/build/react/index.d.ts +6 -0
  44. package/build/react/index.js +5 -0
  45. package/build/react/use-image.d.ts +8 -0
  46. package/build/react/use-image.js +20 -0
  47. package/build/solid/image-background.d.ts +7 -0
  48. package/build/solid/image-background.js +4 -0
  49. package/build/solid/image.d.ts +45 -0
  50. package/build/solid/image.js +4 -0
  51. package/build/solid/index.d.ts +5 -0
  52. package/build/solid/index.js +5 -0
  53. package/build/solid/use-image.d.ts +8 -0
  54. package/build/solid/use-image.js +16 -0
  55. package/build/svelte/image-background-props.d.ts +5 -0
  56. package/build/svelte/image-background-props.js +1 -0
  57. package/build/svelte/image-background.svelte +10 -0
  58. package/build/svelte/image-background.svelte.d.ts +4 -0
  59. package/build/svelte/image.svelte +26 -0
  60. package/build/svelte/image.svelte.d.ts +10 -0
  61. package/build/svelte/index.d.ts +6 -0
  62. package/build/svelte/index.js +5 -0
  63. package/build/svelte/use-image.svelte.d.ts +10 -0
  64. package/build/svelte/use-image.svelte.js +26 -0
  65. package/build/vue/image-background.d.ts +3 -0
  66. package/build/vue/image-background.js +12 -0
  67. package/build/vue/image.d.ts +2 -0
  68. package/build/vue/image.js +4 -0
  69. package/build/vue/index.d.ts +4 -0
  70. package/build/vue/index.js +5 -0
  71. package/build/vue/use-image.d.ts +8 -0
  72. package/build/vue/use-image.js +18 -0
  73. package/build-ngc/angular/image/index.d.ts +23 -0
  74. package/build-ngc/angular/image/index.js +72 -0
  75. package/build-ngc/angular/image/index.js.map +1 -0
  76. package/build-ngc/angular/image-background/index.d.ts +17 -0
  77. package/build-ngc/angular/image-background/index.js +66 -0
  78. package/build-ngc/angular/image-background/index.js.map +1 -0
  79. package/build-ngc/angular/index.d.ts +4 -0
  80. package/build-ngc/angular/index.js +7 -0
  81. package/build-ngc/angular/index.js.map +1 -0
  82. package/build-ngc/angular/inject-image.d.ts +8 -0
  83. package/build-ngc/angular/inject-image.js +23 -0
  84. package/build-ngc/angular/inject-image.js.map +1 -0
  85. package/build-ngc/core/constants.d.ts +3 -0
  86. package/build-ngc/core/constants.js +5 -0
  87. package/build-ngc/core/constants.js.map +1 -0
  88. package/build-ngc/core/hash-strings.d.ts +5 -0
  89. package/build-ngc/core/hash-strings.js +21 -0
  90. package/build-ngc/core/hash-strings.js.map +1 -0
  91. package/build-ngc/core/image-api.d.ts +18 -0
  92. package/build-ngc/core/image-api.js +31 -0
  93. package/build-ngc/core/image-api.js.map +1 -0
  94. package/build-ngc/core/image-background.d.ts +11 -0
  95. package/build-ngc/core/image-background.js +27 -0
  96. package/build-ngc/core/image-background.js.map +1 -0
  97. package/build-ngc/core/image-loader.d.ts +20 -0
  98. package/build-ngc/core/image-loader.js +43 -0
  99. package/build-ngc/core/image-loader.js.map +1 -0
  100. package/build-ngc/core/image-ref-loader.d.ts +4 -0
  101. package/build-ngc/core/image-ref-loader.js +11 -0
  102. package/build-ngc/core/image-ref-loader.js.map +1 -0
  103. package/build-ngc/core/image-view-events.d.ts +9 -0
  104. package/build-ngc/core/image-view-events.js +33 -0
  105. package/build-ngc/core/image-view-events.js.map +1 -0
  106. package/build-ngc/core/image-view.d.ts +14 -0
  107. package/build-ngc/core/image-view.js +112 -0
  108. package/build-ngc/core/image-view.js.map +1 -0
  109. package/build-ngc/core/index.d.ts +10 -0
  110. package/build-ngc/core/index.js +8 -0
  111. package/build-ngc/core/index.js.map +1 -0
  112. package/build-ngc/core/native-module.d.ts +27 -0
  113. package/build-ngc/core/native-module.js +4 -0
  114. package/build-ngc/core/native-module.js.map +1 -0
  115. package/build-ngc/core/native-style.d.ts +19 -0
  116. package/build-ngc/core/native-style.js +63 -0
  117. package/build-ngc/core/native-style.js.map +1 -0
  118. package/build-ngc/core/props.d.ts +11 -0
  119. package/build-ngc/core/props.js +88 -0
  120. package/build-ngc/core/props.js.map +1 -0
  121. package/build-ngc/core/sources.d.ts +8 -0
  122. package/build-ngc/core/sources.js +60 -0
  123. package/build-ngc/core/sources.js.map +1 -0
  124. package/build-ngc/core/types.d.ts +186 -0
  125. package/build-ngc/core/types.js +2 -0
  126. package/build-ngc/core/types.js.map +1 -0
  127. package/native-link.json +13 -0
  128. package/package.json +145 -3
  129. package/src/angular/image/index.ts +78 -0
  130. package/src/angular/image-background/index.ts +69 -0
  131. package/src/angular/index.ts +7 -0
  132. package/src/angular/inject-image.ts +29 -0
  133. package/src/core/constants.ts +6 -0
  134. package/src/core/hash-strings.ts +27 -0
  135. package/src/core/image-api.ts +67 -0
  136. package/src/core/image-background.ts +42 -0
  137. package/src/core/image-loader.ts +65 -0
  138. package/src/core/image-ref-loader.ts +16 -0
  139. package/src/core/image-view-events.ts +41 -0
  140. package/src/core/image-view.ts +156 -0
  141. package/src/core/index.ts +50 -0
  142. package/src/core/native-module.ts +45 -0
  143. package/src/core/native-style.ts +89 -0
  144. package/src/core/props.ts +118 -0
  145. package/src/core/sources.ts +70 -0
  146. package/src/core/types.ts +278 -0
  147. package/src/react/image-background.ts +14 -0
  148. package/src/react/image.ts +14 -0
  149. package/src/react/index.ts +8 -0
  150. package/src/react/use-image.ts +29 -0
  151. package/src/solid/image-background.ts +13 -0
  152. package/src/solid/image.ts +8 -0
  153. package/src/solid/index.ts +7 -0
  154. package/src/solid/use-image.ts +30 -0
  155. package/src/svelte/image-background-props.ts +6 -0
  156. package/src/svelte/image-background.svelte +10 -0
  157. package/src/svelte/image.svelte +26 -0
  158. package/src/svelte/index.ts +8 -0
  159. package/src/svelte/svelte-ambient.d.ts +3 -0
  160. package/src/svelte/svelte-compile.test-helper.ts +6 -0
  161. package/src/svelte/use-image.svelte.ts +38 -0
  162. package/src/vue/image-background.ts +17 -0
  163. package/src/vue/image.ts +5 -0
  164. package/src/vue/index.ts +6 -0
  165. package/src/vue/use-image.ts +34 -0
@@ -0,0 +1,69 @@
1
+ import { ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { DescriptorHost, NativeViewBase } from '@symbiote-native/angular';
3
+ import type { IDescriptor } from '@symbiote-native/components';
4
+ import { renderImageBackground } from '../../core';
5
+ import type { IStyleProp } from '@symbiote-native/engine';
6
+ import type { IImageBackgroundProps, IImageStyle } from '../../core';
7
+
8
+ // One list is the inputs of the component and the props it hands to the image
9
+ const IMAGE_BACKGROUND_INPUTS = [
10
+ 'source',
11
+ 'placeholder',
12
+ 'contentFit',
13
+ 'placeholderContentFit',
14
+ 'contentPosition',
15
+ 'transition',
16
+ 'blurRadius',
17
+ 'tintColor',
18
+ 'priority',
19
+ 'loading',
20
+ 'cachePolicy',
21
+ 'responsivePolicy',
22
+ 'recyclingKey',
23
+ 'autoplay',
24
+ 'sfEffect',
25
+ 'onLoadStart',
26
+ 'onLoad',
27
+ 'onProgress',
28
+ 'onError',
29
+ 'onLoadEnd',
30
+ 'onDisplay',
31
+ 'defaultSource',
32
+ 'loadingIndicatorSource',
33
+ 'resizeMode',
34
+ 'fadeDuration',
35
+ 'alt',
36
+ 'enableLiveTextInteraction',
37
+ 'allowDownscaling',
38
+ 'decodeFormat',
39
+ 'useAppleWebpCodec',
40
+ 'enforceEarlyResizing',
41
+ 'preferHighDynamicRange',
42
+ 'draggable',
43
+ 'imageStyle',
44
+ ] as const;
45
+
46
+ export interface ExpoImageBackground
47
+ extends Partial<Pick<IImageBackgroundProps, (typeof IMAGE_BACKGROUND_INPUTS)[number]>> {}
48
+
49
+ /** Angular twin of `expo-image`'s `ImageBackground`, the content paints over the image */
50
+ @Component({
51
+ selector: 'ExpoImageBackground',
52
+ standalone: true,
53
+ imports: [DescriptorHost],
54
+ inputs: [...IMAGE_BACKGROUND_INPUTS],
55
+ changeDetection: ChangeDetectionStrategy.OnPush,
56
+ template: `@if (descriptor; as node) {
57
+ <symbiote-descriptor-host [node]="node"><ng-content /></symbiote-descriptor-host>
58
+ }`,
59
+ })
60
+ export class ExpoImageBackground extends NativeViewBase {
61
+ // The `style` input of the base, typed like the one of `ExpoImage`
62
+ declare style?: IStyleProp<IImageStyle>;
63
+
64
+ protected override readonly propNames = IMAGE_BACKGROUND_INPUTS;
65
+
66
+ protected override renderView(props: object): IDescriptor {
67
+ return renderImageBackground(props);
68
+ }
69
+ }
@@ -0,0 +1,7 @@
1
+ // @symbiote-native/image/angular: the image view, its cache and hash functions on a shared core
2
+ // The selectors are `ExpoImage*`, `Image` and `ImageBackground` are tags of the adapter
3
+
4
+ export { ExpoImage } from './image';
5
+ export { ExpoImageBackground } from './image-background';
6
+ export { injectImage } from './inject-image';
7
+ export * from '../core';
@@ -0,0 +1,29 @@
1
+ import { computed, effect, signal, untracked } from '@angular/core';
2
+ import type { Signal } from '@angular/core';
3
+ import { createImageRefLoader, resolveSource } from '../core';
4
+ import type { IImageLoadOptions, IImageSource, ImageRef } from '../core';
5
+
6
+ /**
7
+ * Angular twin of `useImage`, `null` until the first image has loaded
8
+ *
9
+ * It loads again when the uri of the source or a signal read by `dependencies` changes
10
+ */
11
+ export function injectImage(
12
+ source: () => IImageSource | string | number,
13
+ options: () => IImageLoadOptions = () => ({}),
14
+ dependencies: () => unknown[] = () => [],
15
+ ): Signal<ImageRef | null> {
16
+ const image = signal<ImageRef | null>(null);
17
+ const key = computed(() => resolveSource(source())?.uri);
18
+
19
+ effect(onCleanup => {
20
+ // Read so the effect tracks them, the source itself is not a trigger
21
+ key();
22
+ dependencies();
23
+ const loader = createImageRefLoader(loaded => image.set(loaded), options);
24
+ untracked(() => loader.load(source()));
25
+ onCleanup(loader.cancel);
26
+ });
27
+
28
+ return image.asReadonly();
29
+ }
@@ -0,0 +1,6 @@
1
+ // Apart from `native-module`, which resolves the module on import: the view needs the name too
2
+
3
+ export const IMAGE_MODULE_NAME = 'ExpoImage';
4
+
5
+ /** `sf:name` in a source string, the native side reads `sf:/name` */
6
+ export const SF_SYMBOL_PREFIX = 'sf:';
@@ -0,0 +1,27 @@
1
+ import type { IImageSource } from './types';
2
+
3
+ type IImageHashType = 'blurhash' | 'thumbhash';
4
+
5
+ const DEFAULT_HASH_SIZE = 16;
6
+
7
+ function hashToUri(type: IImageHashType, hash: string): string {
8
+ const encoded = encodeURI(hash).replace(/#/g, '%23').replace(/\?/g, '%3F');
9
+ return `${type}:/${encoded}`;
10
+ }
11
+
12
+ /** `blurhash:/<hash>/<width>/<height>` or `<hash>/<width>/<height>` */
13
+ export function resolveBlurhashString(str: string): IImageSource {
14
+ const [hash = '', width = '', height = ''] = str.replace(/^blurhash:\//, '').split('/');
15
+ return {
16
+ uri: hashToUri('blurhash', hash),
17
+ width: parseInt(width, 10) || DEFAULT_HASH_SIZE,
18
+ height: parseInt(height, 10) || DEFAULT_HASH_SIZE,
19
+ };
20
+ }
21
+
22
+ /** `thumbhash:/<hash>` or `<hash>` */
23
+ export function resolveThumbhashString(str: string): IImageSource {
24
+ // A slash at the start of the hash would break the uri path
25
+ const hash = str.replace(/^thumbhash:\//, '').replace(/\//g, '\\');
26
+ return { uri: hashToUri('thumbhash', hash) };
27
+ }
@@ -0,0 +1,67 @@
1
+ import { toNativeColor } from './native-style';
2
+ import { expoImage } from './native-module';
3
+ import { resolveSource } from './sources';
4
+ import type {
5
+ IImageCacheConfig,
6
+ IImageLoadOptions,
7
+ IImagePrefetchOptions,
8
+ IImageSource,
9
+ ImageRef,
10
+ } from './types';
11
+
12
+ const DEFAULT_CACHE_POLICY = 'memory-disk';
13
+
14
+ type IPrefetchOptions = IImagePrefetchOptions['cachePolicy'] | IImagePrefetchOptions;
15
+
16
+ /** The caching policy as a string, or with request headers as an object */
17
+ export function prefetchImages(
18
+ urls: string | string[],
19
+ options?: IPrefetchOptions,
20
+ ): Promise<boolean> {
21
+ const { cachePolicy = DEFAULT_CACHE_POLICY, headers } =
22
+ typeof options === 'string' ? { cachePolicy: options } : (options ?? {});
23
+ return expoImage.prefetch(Array.isArray(urls) ? urls : [urls], cachePolicy, headers);
24
+ }
25
+
26
+ export const clearMemoryCache = (): Promise<boolean> => expoImage.clearMemoryCache();
27
+
28
+ export const clearDiskCache = (): Promise<boolean> => expoImage.clearDiskCache();
29
+
30
+ export const getCachePathAsync = (cacheKey: string): Promise<string | null> =>
31
+ expoImage.getCachePathAsync(cacheKey);
32
+
33
+ export const writeToCacheAsync = (
34
+ source: string | ImageRef,
35
+ cacheKey: string,
36
+ ): Promise<void> => expoImage.writeToCacheAsync(source, cacheKey);
37
+
38
+ export const readFromCacheAsync = (cacheKey: string): Promise<ImageRef | null> =>
39
+ expoImage.readFromCacheAsync(cacheKey);
40
+
41
+ export const configureCache = (config: IImageCacheConfig): void =>
42
+ expoImage.configureCache(config);
43
+
44
+ export const generateBlurhashAsync = (
45
+ source: string | ImageRef,
46
+ numberOfComponents: [number, number] | { width: number; height: number },
47
+ ): Promise<string | null> => expoImage.generateBlurhashAsync(source, numberOfComponents);
48
+
49
+ export const generateThumbhashAsync = (source: string | ImageRef): Promise<string> =>
50
+ expoImage.generateThumbhashAsync(source);
51
+
52
+ /** Loads the image into a native `ImageRef`, the caller releases it */
53
+ export async function loadImageAsync(
54
+ source: IImageSource | string | number,
55
+ options: IImageLoadOptions = {},
56
+ ): Promise<ImageRef> {
57
+ const resolved = resolveSource(source);
58
+ if (!resolved) {
59
+ throw new Error('[expo-image]: the image source could not be resolved');
60
+ }
61
+ const { maxWidth, maxHeight, tintColor } = options;
62
+ return expoImage.loadAsync(resolved, {
63
+ maxWidth,
64
+ maxHeight,
65
+ tintColor: toNativeColor(tintColor),
66
+ });
67
+ }
@@ -0,0 +1,42 @@
1
+ import { el } from '@symbiote-native/components';
2
+ import type { IDescriptor } from '@symbiote-native/components';
3
+ import type { IStyleProp } from '@symbiote-native/engine';
4
+ import { createImageView } from './image-view';
5
+ import type { IImageStyle, IImageViewProps } from './types';
6
+
7
+ export type IImageBackgroundProps = IImageViewProps & {
8
+ /** Style of the image itself, `style` goes to the wrapping view */
9
+ imageStyle?: IStyleProp<IImageStyle>;
10
+ /** Goes to the wrapping view, as `style` does */
11
+ className?: string;
12
+ };
13
+
14
+ type IClassKey = 'className' | 'class';
15
+ const CLASS_KEYS: ReadonlySet<string> = new Set<IClassKey>(['className', 'class']);
16
+
17
+ function isClassEntry([key]: [string, unknown]): boolean {
18
+ return CLASS_KEYS.has(key);
19
+ }
20
+
21
+ const ABSOLUTE_FILL = {
22
+ position: 'absolute',
23
+ left: 0,
24
+ right: 0,
25
+ top: 0,
26
+ bottom: 0,
27
+ } as const satisfies IImageStyle;
28
+
29
+ // The background image has no handle, so nobody asks it for a host node
30
+ const backgroundImage = createImageView(() => null);
31
+
32
+ /** A view with an image filling it, the children of the app go after this descriptor */
33
+ export function renderImageBackground(props: IImageBackgroundProps): IDescriptor {
34
+ const { style, imageStyle, ...rest } = props;
35
+ const entries = Object.entries(rest);
36
+ const image = backgroundImage.render({
37
+ ...Object.fromEntries(entries.filter(entry => !isClassEntry(entry))),
38
+ style: [ABSOLUTE_FILL, imageStyle],
39
+ });
40
+ const viewClass = Object.fromEntries(entries.filter(isClassEntry));
41
+ return el('view', { style, ...viewClass }, image ? [image] : []);
42
+ }
@@ -0,0 +1,65 @@
1
+ import type { IImageLoadOptions, IImageSource } from './types';
2
+
3
+ export type IImageLoaderConfig<TImage extends { release(): void }> = {
4
+ load(source: IImageSource | string | number, options: IImageLoadOptions): Promise<TImage>;
5
+ /** Called with each image that finished loading */
6
+ onImage(image: TImage): void;
7
+ /** Read at the time of a failure, so the latest callback wins */
8
+ getOptions(): IImageLoadOptions;
9
+ };
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
+
18
+ /** What a `useImage` hook drives with its own lifecycle */
19
+ export function createImageLoader<TImage extends { release(): void }>(
20
+ config: IImageLoaderConfig<TImage>,
21
+ ): IImageLoader {
22
+ let token = 0;
23
+ let loaded: TImage | null = null;
24
+
25
+ function release(): void {
26
+ loaded?.release();
27
+ loaded = null;
28
+ }
29
+
30
+ function fail(source: IImageSource | string | number, error: unknown, retry: () => void): void {
31
+ const { onError } = config.getOptions();
32
+ if (onError) {
33
+ onError(error instanceof Error ? error : new Error(String(error)), retry);
34
+ return;
35
+ }
36
+ console.error(
37
+ `Loading an image from '${JSON.stringify(source)}' failed, use 'onError' to handle errors`,
38
+ );
39
+ console.error(error);
40
+ }
41
+
42
+ function load(source: IImageSource | string | number): void {
43
+ release();
44
+ token += 1;
45
+ const mine = token;
46
+ config
47
+ .load(source, config.getOptions())
48
+ .then(image => {
49
+ if (mine !== token) return;
50
+ loaded = image;
51
+ config.onImage(image);
52
+ })
53
+ .catch((error: unknown) => {
54
+ if (mine === token) fail(source, error, () => load(source));
55
+ });
56
+ }
57
+
58
+ return {
59
+ load,
60
+ cancel: () => {
61
+ token += 1;
62
+ release();
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,16 @@
1
+ import { loadImageAsync } from './image-api';
2
+ import { createImageLoader } from './image-loader';
3
+ import type { IImageLoader } from './image-loader';
4
+ import type { IImageLoadOptions, ImageRef } from './types';
5
+
6
+ /** The loader every `useImage` hook drives, `getOptions` is read at each load and failure */
7
+ export function createImageRefLoader(
8
+ onImage: (image: ImageRef) => void,
9
+ getOptions: () => IImageLoadOptions,
10
+ ): IImageLoader {
11
+ return createImageLoader<ImageRef>({
12
+ load: loadImageAsync,
13
+ onImage,
14
+ getOptions,
15
+ });
16
+ }
@@ -0,0 +1,41 @@
1
+ import type { ISymbioteEvent } from '@symbiote-native/engine';
2
+ import type {
3
+ IImageErrorEventData,
4
+ IImageLoadEventData,
5
+ IImageProgressEventData,
6
+ IImageViewProps,
7
+ } from './types';
8
+
9
+ function field(value: unknown, key: string): unknown {
10
+ return Reflect.get(Object(value), key);
11
+ }
12
+
13
+ function isLoad(value: unknown): value is IImageLoadEventData {
14
+ return typeof field(value, 'cacheType') === 'string' && typeof field(value, 'source') === 'object';
15
+ }
16
+
17
+ function isProgress(value: unknown): value is IImageProgressEventData {
18
+ return typeof field(value, 'loaded') === 'number' && typeof field(value, 'total') === 'number';
19
+ }
20
+
21
+ function isError(value: unknown): value is IImageErrorEventData {
22
+ return typeof field(value, 'error') === 'string';
23
+ }
24
+
25
+ /** The native payload sits in `nativeEvent`, callbacks get it as the event itself */
26
+ export function toImageEvents(props: IImageViewProps) {
27
+ return {
28
+ onLoadStart: () => props.onLoadStart?.(),
29
+ onLoad: (event: ISymbioteEvent) => {
30
+ if (isLoad(event.nativeEvent)) props.onLoad?.(event.nativeEvent);
31
+ props.onLoadEnd?.();
32
+ },
33
+ onProgress: (event: ISymbioteEvent) => {
34
+ if (isProgress(event.nativeEvent)) props.onProgress?.(event.nativeEvent);
35
+ },
36
+ onError: (event: ISymbioteEvent) => {
37
+ if (isError(event.nativeEvent)) props.onError?.(event.nativeEvent);
38
+ props.onLoadEnd?.();
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,156 @@
1
+ import { requireNativeModule, requireNativeViewManager } from 'expo-modules-core';
2
+ import { el } from '@symbiote-native/components';
3
+ import type { IDescriptor } from '@symbiote-native/components';
4
+ import {
5
+ defineExpoNativeView,
6
+ defineExpoViewMethods,
7
+ getNativeTag,
8
+ isDevBuild,
9
+ styleOfProps,
10
+ } from '@symbiote-native/engine';
11
+ import type { ISymbioteNode } from '@symbiote-native/engine';
12
+ import { IMAGE_MODULE_NAME, SF_SYMBOL_PREFIX } from './constants';
13
+ import { toImageEvents } from './image-view-events';
14
+ import {
15
+ nativeColorProps,
16
+ platformStyle,
17
+ splitImageStyle,
18
+ toNativeColor,
19
+ } from './native-style';
20
+ import type { IImageStyleParts } from './native-style';
21
+ import {
22
+ resolveContentFit,
23
+ resolveContentPosition,
24
+ resolveSfEffect,
25
+ resolveTransition,
26
+ warnOnce,
27
+ } from './props';
28
+ import { resolveSources } from './sources';
29
+ import type { IImageViewHandle, IImageViewProps } from './types';
30
+
31
+ // Registered on render: a barrel side effect is lost in a release build
32
+ const imageView = defineExpoNativeView(requireNativeViewManager, IMAGE_MODULE_NAME);
33
+
34
+ const callViewFunction = defineExpoViewMethods(requireNativeModule, IMAGE_MODULE_NAME);
35
+
36
+ export const imageViewName = imageView.name;
37
+
38
+ export const ensureImageViewRegistered = imageView.ensureRegistered;
39
+
40
+ export type IImageView = {
41
+ handle: IImageViewHandle;
42
+ /** `null` means the view cannot register and the caller renders nothing */
43
+ render(props: IImageViewProps): IDescriptor | null;
44
+ };
45
+
46
+ type IGetNode = () => ISymbioteNode | null | undefined;
47
+
48
+ // Props the view reads itself and native has no use for
49
+ const OWN_PROPS = [
50
+ 'style',
51
+ 'source',
52
+ 'placeholder',
53
+ 'contentFit',
54
+ 'contentPosition',
55
+ 'transition',
56
+ 'fadeDuration',
57
+ 'resizeMode',
58
+ 'defaultSource',
59
+ 'loadingIndicatorSource',
60
+ 'sfEffect',
61
+ 'alt',
62
+ ] as const satisfies readonly (keyof IImageViewProps)[];
63
+
64
+ function hasSfSymbol(sources: ReturnType<typeof resolveSources>): boolean {
65
+ return (
66
+ Array.isArray(sources) &&
67
+ sources.some(source => source.uri?.startsWith(`${SF_SYMBOL_PREFIX}/`))
68
+ );
69
+ }
70
+
71
+ function withoutOwnProps(props: IImageViewProps): Record<string, unknown> {
72
+ const own: readonly string[] = OWN_PROPS;
73
+ return Object.fromEntries(Object.entries(props).filter(([key]) => !own.includes(key)));
74
+ }
75
+
76
+ function placeholderOf(props: IImageViewProps) {
77
+ const { defaultSource, loadingIndicatorSource } = props;
78
+ if (defaultSource || loadingIndicatorSource) {
79
+ warnOnce('`defaultSource` and `loadingIndicatorSource` props are deprecated, use `placeholder`');
80
+ }
81
+ return resolveSources(props.placeholder ?? defaultSource ?? loadingIndicatorSource);
82
+ }
83
+
84
+ // For an SF Symbol `fontSize` sizes both the symbol and its container
85
+ function symbolProps(parts: IImageStyleParts, isSfSymbol: boolean) {
86
+ if (!isSfSymbol) return { symbolWeight: null, symbolSize: null };
87
+ const { fontWeight, fontSize } = parts;
88
+ return {
89
+ symbolWeight: fontWeight === undefined ? null : String(fontWeight),
90
+ symbolSize: fontSize || null,
91
+ };
92
+ }
93
+
94
+ function sizedStyle(parts: IImageStyleParts, isSfSymbol: boolean) {
95
+ const { fontSize, rest } = parts;
96
+ return isSfSymbol && fontSize ? { width: fontSize, height: fontSize, ...rest } : rest;
97
+ }
98
+
99
+ function nativeProps(props: IImageViewProps): Record<string, unknown> {
100
+ const parts = splitImageStyle(styleOfProps(props));
101
+ const source = resolveSources(props.source);
102
+ const isSfSymbol = hasSfSymbol(source);
103
+ const sized = sizedStyle(parts, isSfSymbol);
104
+ const style = platformStyle(sized);
105
+ const symbolColor = isSfSymbol ? parts.color : undefined;
106
+ return {
107
+ ...withoutOwnProps(props),
108
+ ...style,
109
+ accessibilityLabel: props.accessibilityLabel ?? props.alt,
110
+ style,
111
+ source,
112
+ placeholder: placeholderOf(props),
113
+ contentFit: resolveContentFit(
114
+ props.contentFit,
115
+ props.resizeMode ?? parts.resizeMode,
116
+ isSfSymbol,
117
+ ),
118
+ contentPosition: resolveContentPosition(props.contentPosition),
119
+ transition: resolveTransition(props.transition, props.fadeDuration),
120
+ sfEffect: resolveSfEffect(props.sfEffect),
121
+ ...symbolProps(parts, isSfSymbol),
122
+ ...nativeColorProps(sized),
123
+ tintColor: toNativeColor(props.tintColor || symbolColor || style.tintColor),
124
+ ...toImageEvents(props),
125
+ };
126
+ }
127
+
128
+ function createImageViewHandle(getNode: IGetNode): IImageViewHandle {
129
+ // Like upstream's `ref.current?.`, a view that has not committed yet answers nothing
130
+ async function call(method: string): Promise<void> {
131
+ const node = getNode();
132
+ if (!node || getNativeTag(node) === undefined) return;
133
+ await callViewFunction(node, method, []);
134
+ }
135
+ return {
136
+ startAnimating: () => call('startAnimating'),
137
+ stopAnimating: () => call('stopAnimating'),
138
+ lockResourceAsync: () => call('lockResourceAsync'),
139
+ unlockResourceAsync: () => call('unlockResourceAsync'),
140
+ reloadAsync: () => call('reloadAsync'),
141
+ };
142
+ }
143
+
144
+ /** One per mounted view, `getNode` answers the host node once it exists */
145
+ export function createImageView(getNode: IGetNode): IImageView {
146
+ return {
147
+ handle: createImageViewHandle(getNode),
148
+ render: props => {
149
+ if (!ensureImageViewRegistered()) {
150
+ if (isDevBuild()) console.warn("'Image' is not available.");
151
+ return null;
152
+ }
153
+ return el(imageViewName(), nativeProps(props));
154
+ },
155
+ };
156
+ }
@@ -0,0 +1,50 @@
1
+ export { IMAGE_MODULE_NAME } from './constants';
2
+ export {
3
+ clearDiskCache,
4
+ clearMemoryCache,
5
+ configureCache,
6
+ generateBlurhashAsync,
7
+ generateThumbhashAsync,
8
+ getCachePathAsync,
9
+ loadImageAsync,
10
+ prefetchImages,
11
+ readFromCacheAsync,
12
+ writeToCacheAsync,
13
+ } from './image-api';
14
+ export { renderImageBackground } from './image-background';
15
+ export type { IImageBackgroundProps } from './image-background';
16
+ export { createImageRefLoader } from './image-ref-loader';
17
+ export { createImageLoader } from './image-loader';
18
+ export type { IImageLoader, IImageLoaderConfig } from './image-loader';
19
+ export {
20
+ createImageView,
21
+ ensureImageViewRegistered,
22
+ imageViewName,
23
+ type IImageView,
24
+ } from './image-view';
25
+ export { isImageRef, resolveSource, resolveSources } from './sources';
26
+ export type {
27
+ IImageCacheConfig,
28
+ IImageContentFit,
29
+ IImageContentPosition,
30
+ IImageContentPositionObject,
31
+ IImageContentPositionString,
32
+ IImageContentPositionValue,
33
+ IImageDecodeFormat,
34
+ IImageErrorEventData,
35
+ IImageLoadEventData,
36
+ IImageLoadOptions,
37
+ IImagePrefetchOptions,
38
+ IImageProgressEventData,
39
+ IImageResizeMode,
40
+ IImageSource,
41
+ IImageSources,
42
+ IImageStyle,
43
+ IImageTransition,
44
+ IImageViewHandle,
45
+ IImageViewProps,
46
+ ISfSymbolEffect,
47
+ ISfSymbolEffectObject,
48
+ ISfSymbolEffectType,
49
+ ImageRef,
50
+ } from './types';
@@ -0,0 +1,45 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import type { EventSubscription } from 'expo-modules-core';
3
+ import { IMAGE_MODULE_NAME } from './constants';
4
+ import type {
5
+ IImageCacheConfig,
6
+ IImageLoadOptions,
7
+ IImagePrefetchOptions,
8
+ IImageSource,
9
+ ImageRef,
10
+ } from './types';
11
+
12
+ export type INativeImageModule = {
13
+ Image: typeof ImageRef;
14
+ loadAsync(
15
+ source: IImageSource,
16
+ options?: Omit<IImageLoadOptions, 'onError' | 'tintColor'> & {
17
+ /** What `processColor` answers */
18
+ tintColor?: unknown;
19
+ },
20
+ ): Promise<ImageRef>;
21
+ prefetch(
22
+ urls: string[],
23
+ cachePolicy: IImagePrefetchOptions['cachePolicy'],
24
+ headers?: Record<string, string>,
25
+ ): Promise<boolean>;
26
+ clearMemoryCache(): Promise<boolean>;
27
+ clearDiskCache(): Promise<boolean>;
28
+ configureCache(config: IImageCacheConfig): void;
29
+ getCachePathAsync(cacheKey: string): Promise<string | null>;
30
+ writeToCacheAsync(source: string | ImageRef, cacheKey: string): Promise<void>;
31
+ readFromCacheAsync(cacheKey: string): Promise<ImageRef | null>;
32
+ generateBlurhashAsync(
33
+ source: string | ImageRef,
34
+ numberOfComponents: [number, number] | { width: number; height: number },
35
+ ): Promise<string | null>;
36
+ generateThumbhashAsync(source: string | ImageRef): Promise<string>;
37
+ addListener(
38
+ eventName: string,
39
+ listener: (event: { url: string; width: number; height: number }) => void,
40
+ ): EventSubscription;
41
+ };
42
+
43
+ export const expoImage = requireNativeModule<INativeImageModule>(
44
+ IMAGE_MODULE_NAME,
45
+ );