@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.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -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 +109 -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 +22 -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 +29 -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 +34 -0
  25. package/build/core/image-view.d.ts +14 -0
  26. package/build/core/image-view.js +113 -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 +65 -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 +61 -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 +70 -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 +23 -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 +30 -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 +35 -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 +114 -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 +66 -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 +62 -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 +79 -0
  130. package/src/angular/image-background/index.ts +72 -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 +29 -0
  135. package/src/core/image-api.ts +77 -0
  136. package/src/core/image-background.ts +47 -0
  137. package/src/core/image-loader.ts +72 -0
  138. package/src/core/image-ref-loader.ts +16 -0
  139. package/src/core/image-view-events.ts +47 -0
  140. package/src/core/image-view.ts +173 -0
  141. package/src/core/index.ts +50 -0
  142. package/src/core/native-module.ts +44 -0
  143. package/src/core/native-style.ts +96 -0
  144. package/src/core/props.ts +123 -0
  145. package/src/core/sources.ts +75 -0
  146. package/src/core/types.ts +275 -0
  147. package/src/react/image-background.ts +16 -0
  148. package/src/react/image.ts +17 -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 +12 -0
  152. package/src/solid/image.ts +9 -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 +38 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ {
2
+ "android": {
3
+ "gradleProjectName": "expo-image",
4
+ "requiresKsp": true,
5
+ "modules": [
6
+ {
7
+ "importPath": "expo.modules.image.ExpoImageModule",
8
+ "className": "ExpoImageModule",
9
+ "nativeName": "ExpoImage"
10
+ }
11
+ ]
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,148 @@
1
1
  {
2
2
  "name": "@symbiote-native/image",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "expo-image wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. The image view with caching, placeholders and SF Symbols, plus the image cache and hash functions.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
9
+ "directory": "packages/image"
10
+ },
11
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/image#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
14
+ },
15
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
16
+ "keywords": [
17
+ "react-native",
18
+ "symbiote-native",
19
+ "expo-image",
20
+ "react",
21
+ "vue",
22
+ "svelte",
23
+ "solid",
24
+ "angular",
25
+ "image"
26
+ ],
27
+ "type": "module",
28
+ "main": "./build/core/index.js",
29
+ "module": "./build/core/index.js",
30
+ "types": "./build/core/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./build/core/index.d.ts",
34
+ "default": "./build/core/index.js"
35
+ },
36
+ "./vue": {
37
+ "types": "./build/vue/index.d.ts",
38
+ "default": "./build/vue/index.js"
39
+ },
40
+ "./react": {
41
+ "types": "./build/react/index.d.ts",
42
+ "default": "./build/react/index.js"
43
+ },
44
+ "./svelte": "./src/svelte/index.ts",
45
+ "./solid": {
46
+ "types": "./build/solid/index.d.ts",
47
+ "default": "./build/solid/index.js"
48
+ },
49
+ "./angular": {
50
+ "types": "./build-ngc/angular/index.d.ts",
51
+ "react-native": "./build-ngc/angular/index.js",
52
+ "default": "./src/angular/index.ts"
53
+ }
54
+ },
55
+ "files": [
56
+ "src",
57
+ "build",
58
+ "build-ngc",
59
+ "native-link.json",
60
+ "!src/**/*.test.*",
61
+ "!src/**/*.spec.*",
62
+ "!src/**/*.detox.*"
63
+ ],
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "dependencies": {
68
+ "expo-image": "57.0.5",
69
+ "expo-modules-core": "57.0.20",
70
+ "sf-symbols-typescript": "^2.0.0",
71
+ "@symbiote-native/components": "3.2.0"
72
+ },
73
+ "peerDependencies": {
74
+ "@angular/core": ">=20",
75
+ "@vue/runtime-core": "^3.5.13",
76
+ "react": ">=19.0.0",
77
+ "react-native": ">=0.86",
78
+ "solid-js": ">=1.9.0",
79
+ "svelte": ">=5.56.0",
80
+ "vue": ">=3.5.0",
81
+ "@symbiote-native/angular": "^3.3.0",
82
+ "@symbiote-native/engine": "^1.6.0",
83
+ "@symbiote-native/react": "^3.3.0",
84
+ "@symbiote-native/solid": "^3.2.0",
85
+ "@symbiote-native/svelte": "^3.2.0",
86
+ "@symbiote-native/vue": "^3.3.0"
87
+ },
88
+ "peerDependenciesMeta": {
89
+ "@symbiote-native/angular": {
90
+ "optional": true
91
+ },
92
+ "@symbiote-native/react": {
93
+ "optional": true
94
+ },
95
+ "@symbiote-native/solid": {
96
+ "optional": true
97
+ },
98
+ "@symbiote-native/svelte": {
99
+ "optional": true
100
+ },
101
+ "@symbiote-native/vue": {
102
+ "optional": true
103
+ },
104
+ "@angular/core": {
105
+ "optional": true
106
+ },
107
+ "@vue/runtime-core": {
108
+ "optional": true
109
+ },
110
+ "react": {
111
+ "optional": true
112
+ },
113
+ "solid-js": {
114
+ "optional": true
115
+ },
116
+ "svelte": {
117
+ "optional": true
118
+ },
119
+ "vue": {
120
+ "optional": true
121
+ }
122
+ },
123
+ "devDependencies": {
124
+ "@angular/compiler": "~22.0.8",
125
+ "@angular/compiler-cli": "~22.0.8",
126
+ "@angular/core": "~22.0.8",
127
+ "@types/node": "^26.0.0",
128
+ "@types/react": "^19.2.0",
129
+ "@vue/runtime-core": "^3.5.13",
130
+ "react": "19.2.3",
131
+ "solid-js": "^1.9.14",
132
+ "svelte": "^5.56.0",
133
+ "typescript": "~6.0.0",
134
+ "@symbiote-native/angular": "3.3.0",
135
+ "@symbiote-native/engine": "1.6.0",
136
+ "@symbiote-native/react": "3.3.0",
137
+ "@symbiote-native/solid": "3.2.0",
138
+ "@symbiote-native/svelte": "3.2.0",
139
+ "@symbiote-native/test-utils": "0.4.7",
140
+ "@symbiote-native/vue": "3.3.0"
141
+ },
142
+ "scripts": {
143
+ "typecheck": "tsc --build",
144
+ "clean": "rm -rf build-ngc",
145
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
146
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
147
+ }
6
148
  }
@@ -0,0 +1,79 @@
1
+ import { ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { DescriptorOutlet, NativeViewBase } from '@symbiote-native/angular';
3
+ import type { IDescriptor } from '@symbiote-native/components';
4
+ import type { IStyleProp } from '@symbiote-native/engine';
5
+ import { createImageView } from '../../core';
6
+ import type { IImageStyle, IImageViewProps } from '../../core';
7
+
8
+ // One list is the inputs of the component and the props it hands to the native view
9
+ const IMAGE_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
+ ] as const;
44
+
45
+ // Types the inputs for the template, the decorators that would do it are replaced by `inputs`
46
+ export interface ExpoImage extends Partial<
47
+ Pick<IImageViewProps, (typeof IMAGE_INPUTS)[number]>
48
+ > {}
49
+
50
+ /** Angular twin of `expo-image`'s `Image`, `Image` itself is a primitive tag of the adapter */
51
+ @Component({
52
+ selector: 'ExpoImage',
53
+ standalone: true,
54
+ imports: [DescriptorOutlet],
55
+ inputs: [...IMAGE_INPUTS],
56
+ changeDetection: ChangeDetectionStrategy.OnPush,
57
+ template: `@if (descriptor; as node) {
58
+ <symbiote-descriptor-outlet [node]="node" />
59
+ }`,
60
+ })
61
+ export class ExpoImage extends NativeViewBase {
62
+ // The `style` input of the base, typed with the image keys such as `tintColor`
63
+ declare style?: IStyleProp<IImageStyle>;
64
+
65
+ private readonly view = createImageView(() => this.hostNode());
66
+
67
+ // The functions of the view are members of the component, a `@ViewChild` reaches them
68
+ readonly startAnimating = this.view.handle.startAnimating;
69
+ readonly stopAnimating = this.view.handle.stopAnimating;
70
+ readonly lockResourceAsync = this.view.handle.lockResourceAsync;
71
+ readonly unlockResourceAsync = this.view.handle.unlockResourceAsync;
72
+ readonly reloadAsync = this.view.handle.reloadAsync;
73
+
74
+ protected override readonly propNames = IMAGE_INPUTS;
75
+
76
+ protected override renderView(props: object): IDescriptor | null {
77
+ return this.view.render(props);
78
+ }
79
+ }
@@ -0,0 +1,72 @@
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 extends Partial<
47
+ Pick<IImageBackgroundProps, (typeof IMAGE_BACKGROUND_INPUTS)[number]>
48
+ > {}
49
+
50
+ /** Angular twin of `expo-image`'s `ImageBackground`, the content paints over the image */
51
+ @Component({
52
+ selector: 'ExpoImageBackground',
53
+ standalone: true,
54
+ imports: [DescriptorHost],
55
+ inputs: [...IMAGE_BACKGROUND_INPUTS],
56
+ changeDetection: ChangeDetectionStrategy.OnPush,
57
+ template: `@if (descriptor; as node) {
58
+ <symbiote-descriptor-host [node]="node"
59
+ ><ng-content
60
+ /></symbiote-descriptor-host>
61
+ }`,
62
+ })
63
+ export class ExpoImageBackground extends NativeViewBase {
64
+ // The `style` input of the base, typed like the one of `ExpoImage`
65
+ declare style?: IStyleProp<IImageStyle>;
66
+
67
+ protected override readonly propNames = IMAGE_BACKGROUND_INPUTS;
68
+
69
+ protected override renderView(props: object): IDescriptor {
70
+ return renderImageBackground(props);
71
+ }
72
+ }
@@ -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,29 @@
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
15
+ .replace(/^blurhash:\//, '')
16
+ .split('/');
17
+ return {
18
+ uri: hashToUri('blurhash', hash),
19
+ width: parseInt(width, 10) || DEFAULT_HASH_SIZE,
20
+ height: parseInt(height, 10) || DEFAULT_HASH_SIZE,
21
+ };
22
+ }
23
+
24
+ /** `thumbhash:/<hash>` or `<hash>` */
25
+ export function resolveThumbhashString(str: string): IImageSource {
26
+ // A slash at the start of the hash would break the uri path
27
+ const hash = str.replace(/^thumbhash:\//, '').replace(/\//g, '\\');
28
+ return { uri: hashToUri('thumbhash', hash) };
29
+ }
@@ -0,0 +1,77 @@
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 =
15
+ IImagePrefetchOptions['cachePolicy'] | IImagePrefetchOptions;
16
+
17
+ /** The caching policy as a string, or with request headers as an object */
18
+ export function prefetchImages(
19
+ urls: string | string[],
20
+ options?: IPrefetchOptions,
21
+ ): Promise<boolean> {
22
+ const { cachePolicy = DEFAULT_CACHE_POLICY, headers } =
23
+ typeof options === 'string' ? { cachePolicy: options } : (options ?? {});
24
+ return expoImage.prefetch(
25
+ Array.isArray(urls) ? urls : [urls],
26
+ cachePolicy,
27
+ headers,
28
+ );
29
+ }
30
+
31
+ export const clearMemoryCache = (): Promise<boolean> =>
32
+ expoImage.clearMemoryCache();
33
+
34
+ export const clearDiskCache = (): Promise<boolean> =>
35
+ expoImage.clearDiskCache();
36
+
37
+ export const getCachePathAsync = (cacheKey: string): Promise<string | null> =>
38
+ expoImage.getCachePathAsync(cacheKey);
39
+
40
+ export const writeToCacheAsync = (
41
+ source: string | ImageRef,
42
+ cacheKey: string,
43
+ ): Promise<void> => expoImage.writeToCacheAsync(source, cacheKey);
44
+
45
+ export const readFromCacheAsync = (
46
+ cacheKey: string,
47
+ ): Promise<ImageRef | null> => expoImage.readFromCacheAsync(cacheKey);
48
+
49
+ export const configureCache = (config: IImageCacheConfig): void =>
50
+ expoImage.configureCache(config);
51
+
52
+ export const generateBlurhashAsync = (
53
+ source: string | ImageRef,
54
+ numberOfComponents: [number, number] | { width: number; height: number },
55
+ ): Promise<string | null> =>
56
+ expoImage.generateBlurhashAsync(source, numberOfComponents);
57
+
58
+ export const generateThumbhashAsync = (
59
+ source: string | ImageRef,
60
+ ): Promise<string> => expoImage.generateThumbhashAsync(source);
61
+
62
+ /** Loads the image into a native `ImageRef`, the caller releases it */
63
+ export async function loadImageAsync(
64
+ source: IImageSource | string | number,
65
+ options: IImageLoadOptions = {},
66
+ ): Promise<ImageRef> {
67
+ const resolved = resolveSource(source);
68
+ if (!resolved) {
69
+ throw new Error('[expo-image]: the image source could not be resolved');
70
+ }
71
+ const { maxWidth, maxHeight, tintColor } = options;
72
+ return expoImage.loadAsync(resolved, {
73
+ maxWidth,
74
+ maxHeight,
75
+ tintColor: toNativeColor(tintColor),
76
+ });
77
+ }
@@ -0,0 +1,47 @@
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>([
16
+ 'className',
17
+ 'class',
18
+ ]);
19
+
20
+ function isClassEntry([key]: [string, unknown]): boolean {
21
+ return CLASS_KEYS.has(key);
22
+ }
23
+
24
+ const ABSOLUTE_FILL = {
25
+ position: 'absolute',
26
+ left: 0,
27
+ right: 0,
28
+ top: 0,
29
+ bottom: 0,
30
+ } as const satisfies IImageStyle;
31
+
32
+ // The background image has no handle, so nobody asks it for a host node
33
+ const backgroundImage = createImageView(() => null);
34
+
35
+ /** A view with an image filling it, the children of the app go after this descriptor */
36
+ export function renderImageBackground(
37
+ props: IImageBackgroundProps,
38
+ ): IDescriptor {
39
+ const { style, imageStyle, ...rest } = props;
40
+ const entries = Object.entries(rest);
41
+ const image = backgroundImage.render({
42
+ ...Object.fromEntries(entries.filter(entry => !isClassEntry(entry))),
43
+ style: [ABSOLUTE_FILL, imageStyle],
44
+ });
45
+ const viewClass = Object.fromEntries(entries.filter(isClassEntry));
46
+ return el('view', { style, ...viewClass }, image ? [image] : []);
47
+ }
@@ -0,0 +1,72 @@
1
+ import type { IImageLoadOptions, IImageSource } from './types';
2
+
3
+ export type IImageLoaderConfig<TImage extends { release(): void }> = {
4
+ load(
5
+ source: IImageSource | string | number,
6
+ options: IImageLoadOptions,
7
+ ): Promise<TImage>;
8
+ /** Called with each image that finished loading */
9
+ onImage(image: TImage): void;
10
+ /** Read at the time of a failure, so the latest callback wins */
11
+ getOptions(): IImageLoadOptions;
12
+ };
13
+
14
+ export type IImageLoader = {
15
+ /** Starts a load and drops the result of the one before */
16
+ load(source: IImageSource | string | number): void;
17
+ /** Drops a load in flight and releases the image loaded last */
18
+ cancel(): void;
19
+ };
20
+
21
+ /** What a `useImage` hook drives with its own lifecycle */
22
+ export function createImageLoader<TImage extends { release(): void }>(
23
+ config: IImageLoaderConfig<TImage>,
24
+ ): IImageLoader {
25
+ let token = 0;
26
+ let loaded: TImage | null = null;
27
+
28
+ function release(): void {
29
+ loaded?.release();
30
+ loaded = null;
31
+ }
32
+
33
+ function fail(
34
+ source: IImageSource | string | number,
35
+ error: unknown,
36
+ retry: () => void,
37
+ ): void {
38
+ const { onError } = config.getOptions();
39
+ if (onError) {
40
+ onError(error instanceof Error ? error : new Error(String(error)), retry);
41
+ return;
42
+ }
43
+ console.error(
44
+ `Loading an image from '${JSON.stringify(source)}' failed, use 'onError' to handle errors`,
45
+ );
46
+ console.error(error);
47
+ }
48
+
49
+ function load(source: IImageSource | string | number): void {
50
+ release();
51
+ token += 1;
52
+ const mine = token;
53
+ config
54
+ .load(source, config.getOptions())
55
+ .then(image => {
56
+ if (mine !== token) return;
57
+ loaded = image;
58
+ config.onImage(image);
59
+ })
60
+ .catch((error: unknown) => {
61
+ if (mine === token) fail(source, error, () => load(source));
62
+ });
63
+ }
64
+
65
+ return {
66
+ load,
67
+ cancel: () => {
68
+ token += 1;
69
+ release();
70
+ },
71
+ };
72
+ }
@@ -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,47 @@
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 (
15
+ typeof field(value, 'cacheType') === 'string' &&
16
+ typeof field(value, 'source') === 'object'
17
+ );
18
+ }
19
+
20
+ function isProgress(value: unknown): value is IImageProgressEventData {
21
+ return (
22
+ typeof field(value, 'loaded') === 'number' &&
23
+ typeof field(value, 'total') === 'number'
24
+ );
25
+ }
26
+
27
+ function isError(value: unknown): value is IImageErrorEventData {
28
+ return typeof field(value, 'error') === 'string';
29
+ }
30
+
31
+ /** The native payload sits in `nativeEvent`, callbacks get it as the event itself */
32
+ export function toImageEvents(props: IImageViewProps) {
33
+ return {
34
+ onLoadStart: () => props.onLoadStart?.(),
35
+ onLoad: (event: ISymbioteEvent) => {
36
+ if (isLoad(event.nativeEvent)) props.onLoad?.(event.nativeEvent);
37
+ props.onLoadEnd?.();
38
+ },
39
+ onProgress: (event: ISymbioteEvent) => {
40
+ if (isProgress(event.nativeEvent)) props.onProgress?.(event.nativeEvent);
41
+ },
42
+ onError: (event: ISymbioteEvent) => {
43
+ if (isError(event.nativeEvent)) props.onError?.(event.nativeEvent);
44
+ props.onLoadEnd?.();
45
+ },
46
+ };
47
+ }