@symbiote-native/live-photo 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -4,44 +4,223 @@
|
|
|
4
4
|
[SymbioteNative](../../README.md) adapter: React, Vue, Svelte, Solid and Angular. Shows a Live
|
|
5
5
|
Photo, iOS only.
|
|
6
6
|
|
|
7
|
+
Built the same way as [`@symbiote-native/clipboard`](../clipboard)'s `ClipboardPasteButton`, an
|
|
8
|
+
`expo-modules-core` native view reached through `requireNativeViewManager` (see the
|
|
9
|
+
`symbiote-expo-native-module` project skill: why `expo-modules-core` is depended on directly and never
|
|
10
|
+
the `expo` meta-package, why the upstream JS is hand-ported into `core/`, and how autolinking finds the
|
|
11
|
+
native module). One render function in `core/` returns a descriptor and every adapter only turns it into
|
|
12
|
+
its own element.
|
|
13
|
+
|
|
7
14
|
## Install
|
|
8
15
|
|
|
16
|
+
**New app:**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @symbiote-native/cli new my-app --live-photo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Existing SymbioteNative app:**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @symbiote-native/cli add --live-photo
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Either way: installs `@symbiote-native/live-photo` and wires the native autolinking automatically,
|
|
29
|
+
see [`@symbiote-native/cli`](../cli).
|
|
30
|
+
|
|
31
|
+
<details>
|
|
32
|
+
<summary>Manual install (no CLI - installing and wiring native autolinking by hand)</summary>
|
|
33
|
+
|
|
9
34
|
```bash
|
|
10
|
-
|
|
11
|
-
|
|
35
|
+
npm install @symbiote-native/live-photo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Depends on `expo-live-photo` and `expo-modules-core` directly (regular dependencies, pinned to exact
|
|
39
|
+
versions, since this package's `core/` is hand-ported against one specific native API shape).
|
|
40
|
+
Never install `expo-live-photo` yourself, and never add the `expo` package to this project: it bundles
|
|
41
|
+
its own Metro/Babel pipeline that conflicts with this project's own.
|
|
42
|
+
|
|
43
|
+
### Required one-time step: native autolinking wiring
|
|
44
|
+
|
|
45
|
+
`expo-live-photo`'s native code is discovered by `expo-modules-autolinking`, not by RN's own
|
|
46
|
+
`react-native.config.cjs` mechanism. Wire it into the native host app **once**, it covers this package
|
|
47
|
+
and every other `expo-modules-core` package with zero further changes. The steps and the
|
|
48
|
+
mechanics behind them are in the `symbiote-expo-native-module` skill and in
|
|
49
|
+
[`@symbiote-native/network`](../network)'s README; the reference app is `examples/expo-react`.
|
|
50
|
+
|
|
51
|
+
</details>
|
|
52
|
+
|
|
53
|
+
## Shape
|
|
54
|
+
|
|
12
55
|
```
|
|
56
|
+
src/core/ live-photo-view.ts - createLivePhotoViewHandle(getNode) and the render function.
|
|
57
|
+
types.ts - ILivePhotoAsset, ILivePhotoViewHandle, ILivePhotoPlaybackStyle.
|
|
58
|
+
src/react/ @symbiote-native/live-photo/react - LivePhotoView (ref carries the handle)
|
|
59
|
+
src/vue/ @symbiote-native/live-photo/vue - LivePhotoView (a template ref, the functions are exposed)
|
|
60
|
+
src/svelte/ @symbiote-native/live-photo/svelte - LivePhotoView (bind:this)
|
|
61
|
+
src/solid/ @symbiote-native/live-photo/solid - LivePhotoView (ref={fn})
|
|
62
|
+
src/angular/ @symbiote-native/live-photo/angular - LivePhotoView (the functions are methods)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A native Expo view keeps its functions on its module's view prototypes and native finds the view by its
|
|
66
|
+
native tag, so the engine's `defineExpoViewMethods` makes that call. Each adapter only hands over its host
|
|
67
|
+
node. `native-link.json` carries only `ios.upstreamPackage`, upstream has no Android folder.
|
|
13
68
|
|
|
14
|
-
|
|
15
|
-
app. Never install `expo-live-photo` or the `expo` meta-package yourself.
|
|
69
|
+
## Use it
|
|
16
70
|
|
|
17
|
-
|
|
71
|
+
### React
|
|
18
72
|
|
|
19
73
|
```tsx
|
|
74
|
+
import { useRef } from 'react';
|
|
20
75
|
import { LivePhotoView, type ILivePhotoViewHandle } from '@symbiote-native/live-photo/react';
|
|
21
76
|
|
|
22
|
-
const
|
|
77
|
+
const handle = useRef<ILivePhotoViewHandle>(null);
|
|
23
78
|
|
|
24
79
|
<LivePhotoView
|
|
25
|
-
ref={
|
|
80
|
+
ref={handle}
|
|
26
81
|
style={{ width: 300, height: 400 }}
|
|
27
82
|
source={{ photoUri, pairedVideoUri }}
|
|
28
|
-
contentFit="cover"
|
|
29
83
|
onLoadError={error => console.warn(error.message)}
|
|
30
84
|
/>;
|
|
85
|
+
<button title="Play" onPress={() => handle.current?.startPlayback('hint')} />;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Vue
|
|
89
|
+
|
|
90
|
+
```vue
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { ref } from 'vue';
|
|
93
|
+
import { LivePhotoView, type ILivePhotoViewHandle } from '@symbiote-native/live-photo/vue';
|
|
94
|
+
|
|
95
|
+
const handle = ref<ILivePhotoViewHandle | null>(null);
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<template>
|
|
99
|
+
<LivePhotoView
|
|
100
|
+
ref="handle"
|
|
101
|
+
:style="{ width: 300, height: 400 }"
|
|
102
|
+
:source="{ photoUri, pairedVideoUri }"
|
|
103
|
+
@loadError="error => console.warn(error.message)"
|
|
104
|
+
/>
|
|
105
|
+
<button title="Play" @press="handle?.startPlayback('hint')" />
|
|
106
|
+
</template>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Angular
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { Component } from '@angular/core';
|
|
113
|
+
import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';
|
|
114
|
+
import { LivePhotoView } from '@symbiote-native/live-photo/angular';
|
|
31
115
|
|
|
32
|
-
|
|
116
|
+
@Component({
|
|
117
|
+
standalone: true,
|
|
118
|
+
imports: [LivePhotoView, SYMBIOTE_ELEMENTS],
|
|
119
|
+
template: `
|
|
120
|
+
<LivePhotoView
|
|
121
|
+
#view
|
|
122
|
+
[style]="{ width: 300, height: 400 }"
|
|
123
|
+
[source]="{ photoUri, pairedVideoUri }"
|
|
124
|
+
[onLoadError]="onLoadError"
|
|
125
|
+
/>
|
|
126
|
+
<button title="Play" (press)="view.startPlayback('hint')" />
|
|
127
|
+
`,
|
|
128
|
+
})
|
|
129
|
+
export class LivePhotoCard {
|
|
130
|
+
readonly photoUri = '...';
|
|
131
|
+
readonly pairedVideoUri = '...';
|
|
132
|
+
readonly onLoadError = (error: { message: string }): void => console.warn(error.message);
|
|
133
|
+
}
|
|
33
134
|
```
|
|
34
135
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
136
|
+
### Svelte
|
|
137
|
+
|
|
138
|
+
```svelte
|
|
139
|
+
<script lang="ts">
|
|
140
|
+
import { LivePhotoView, type ILivePhotoViewHandle } from '@symbiote-native/live-photo/svelte';
|
|
141
|
+
|
|
142
|
+
let handle = $state<ILivePhotoViewHandle | undefined>();
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<LivePhotoView
|
|
146
|
+
bind:this={handle}
|
|
147
|
+
style={{ width: 300, height: 400 }}
|
|
148
|
+
source={{ photoUri, pairedVideoUri }}
|
|
149
|
+
onLoadError={error => console.warn(error.message)}
|
|
150
|
+
/>
|
|
151
|
+
<button title="Play" onPress={() => handle?.startPlayback('hint')} />
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Solid
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { createSignal } from 'solid-js';
|
|
158
|
+
import { LivePhotoView, type ILivePhotoViewHandle } from '@symbiote-native/live-photo/solid';
|
|
159
|
+
|
|
160
|
+
const [handle, setHandle] = createSignal<ILivePhotoViewHandle>();
|
|
161
|
+
|
|
162
|
+
<LivePhotoView
|
|
163
|
+
ref={setHandle}
|
|
164
|
+
style={{ width: 300, height: 400 }}
|
|
165
|
+
source={{ photoUri, pairedVideoUri }}
|
|
166
|
+
onLoadError={error => console.warn(error.message)}
|
|
167
|
+
/>;
|
|
168
|
+
<button title="Play" onPress={() => handle()?.startPlayback('hint')} />;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## API
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
<LivePhotoView source={{ photoUri, pairedVideoUri }} isMuted?={true} contentFit?={'contain' | 'cover'}
|
|
175
|
+
useDefaultGestureRecognizer?={true}
|
|
176
|
+
onLoadStart? onPreviewPhotoLoad? onLoadComplete? onLoadError?({ message }) onPlaybackStart? onPlaybackStop? />
|
|
177
|
+
// handle:
|
|
178
|
+
startPlayback(style?: 'full' | 'hint'): void // default 'full'
|
|
179
|
+
stopPlayback(): void
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Off iOS the view renders nothing, with a warning in development, and the handle throws
|
|
183
|
+
`UnavailabilityError`. Ported from upstream's `LivePhotoView.tsx`.
|
|
184
|
+
|
|
185
|
+
## Notes
|
|
186
|
+
|
|
187
|
+
- **iOS only.** Off iOS the view renders nothing, with a warning in development, and the handle
|
|
188
|
+
throws `UnavailabilityError`. Check `Platform.OS` before you render it.
|
|
189
|
+
- **A Live Photo is two files.** The still photo and the paired video, and they must stay a pair:
|
|
190
|
+
the pairing lives in their metadata, so editing or recompressing either one breaks it.
|
|
191
|
+
- **Playback style.** `startPlayback()` plays the full clip, `startPlayback('hint')` plays the short
|
|
192
|
+
preview you see when you press a photo in the Photos app.
|
|
193
|
+
- **Muted by default.** `isMuted` is `true`, unmute it when the sound is part of the content.
|
|
194
|
+
- **Press and hold plays** while `useDefaultGestureRecognizer` is `true`. Turn it off and call
|
|
195
|
+
`startPlayback` yourself if the photo has its own gestures.
|
|
196
|
+
- **Not ported.** Nothing. Upstream ships no tests and no config plugin, the suites here are written against its behavior.
|
|
197
|
+
|
|
198
|
+
## Common questions
|
|
199
|
+
|
|
200
|
+
- **Where do the two URIs come from?** `@symbiote-native/image-picker` with
|
|
201
|
+
`mediaTypes: ['livePhotos']`: `assets[0].uri` and `assets[0].pairedVideoAsset?.uri`. A photo in
|
|
202
|
+
iCloud needs `shouldDownloadFromNetwork`.
|
|
203
|
+
- **No Live Photo in the Simulator.** The Simulator cannot make one, use a real device.
|
|
204
|
+
- **`onLoadError` or no playback.** The files are not one pair, or one was edited. Pass the two URIs
|
|
205
|
+
of one pick, untouched.
|
|
206
|
+
- **Media library.** Resolving the still half was reported to throw for some assets, prefer the
|
|
207
|
+
picker.
|
|
208
|
+
- **No sound.** `isMuted` defaults to `true`.
|
|
209
|
+
- **Playback styles.** `startPlayback('full')` or `startPlayback('hint')`.
|
|
210
|
+
- **Android.** Not available, the view renders nothing.
|
|
40
211
|
|
|
41
|
-
|
|
42
|
-
|
|
212
|
+
Sources: [Expo docs: LivePhoto](https://docs.expo.dev/versions/latest/sdk/live-photo/),
|
|
213
|
+
[expo/expo#47730](https://github.com/expo/expo/issues/47730),
|
|
214
|
+
[expo/expo#46512](https://github.com/expo/expo/issues/46512),
|
|
215
|
+
[expo/expo#48658](https://github.com/expo/expo/issues/48658).
|
|
43
216
|
|
|
44
|
-
##
|
|
217
|
+
## Test it
|
|
45
218
|
|
|
46
|
-
|
|
47
|
-
|
|
219
|
+
No device is needed for the logic. The core tests (`src/core/*.test.ts`) replace `expo-modules-core`
|
|
220
|
+
(`requireNativeViewManager`, `Platform`) and assert the descriptor a render function returns. The
|
|
221
|
+
adapter tests (`src/{react,vue,solid,angular}/**/*.test.*`, `vitest`) render the real component over the
|
|
222
|
+
recording Fabric with an injected view config and assert the committed payload, and Svelte's
|
|
223
|
+
`*.smoke.test.ts` compile the `.svelte` files and mount them. Painting itself is verified on a device in
|
|
224
|
+
the six `examples/expo-*` canary apps (`examples/expo-react`, `examples/expo-vue-sfc`,
|
|
225
|
+
`examples/expo-vue-tsx`, `examples/expo-svelte`, `examples/expo-solid`, `examples/expo-angular`), see the
|
|
226
|
+
parent [README](../../README.md).
|
|
@@ -5,7 +5,9 @@ import { createLivePhotoViewHandle, renderLivePhotoView } from '../core/index.js
|
|
|
5
5
|
/** React twin of `expo-live-photo`'s `LivePhotoView`, iOS only */
|
|
6
6
|
export function LivePhotoView({ ref, ...props }) {
|
|
7
7
|
const [host] = useState(createHostNodeHolder);
|
|
8
|
-
useImperativeHandle(ref, () => createLivePhotoViewHandle(host.getNode), [
|
|
8
|
+
useImperativeHandle(ref, () => createLivePhotoViewHandle(host.getNode), [
|
|
9
|
+
host,
|
|
10
|
+
]);
|
|
9
11
|
const descriptor = renderLivePhotoView(props);
|
|
10
12
|
return descriptor ? descriptorToReact(host.capture(descriptor)) : null;
|
|
11
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/angular/live-photo-view/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE5E,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/angular/live-photo-view/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE5E,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;;AAG5E,oEAAoE;AAUpE,MAAM,OAAO,aAAc,SAAQ,cAAc;IACtC,MAAM,CAAiC;IACvC,OAAO,CAAkC;IACzC,UAAU,CAAqC;IAExD,2BAA2B,CAAsD;IACxE,eAAe,CAA0C;IACzD,cAAc,CAAyC;IACvD,WAAW,CAAsC;IACjD,kBAAkB,CAA6C;IAC/D,cAAc,CAAyC;IACvD,WAAW,CAAsC;IAEzC,MAAM,GAAG,yBAAyB,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE/C,SAAS,GAAG;QACtC,QAAQ;QACR,SAAS;QACT,YAAY;QACZ,6BAA6B;QAC7B,iBAAiB;QACjB,gBAAgB;QAChB,aAAa;QACb,oBAAoB;QACpB,gBAAgB;QAChB,aAAa;KACsC,CAAC;IAEnC,UAAU,CAAC,KAAa;QACzC,OAAO,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,aAAa,CAAC,aAAuC;QACnD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;IAC3C,CAAC;IAED,YAAY;QACV,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;IAC7B,CAAC;uGAtCU,aAAa;2FAAb,aAAa,kbAJd;;IAER,4DAJQ,gBAAgB;;2FAMf,aAAa;kBATzB,SAAS;mBAAC;oBACT,QAAQ,EAAE,eAAe;oBACzB,UAAU,EAAE,IAAI;oBAChB,OAAO,EAAE,CAAC,gBAAgB,CAAC;oBAC3B,eAAe,EAAE,uBAAuB,CAAC,MAAM;oBAC/C,QAAQ,EAAE;;IAER;iBACH;;sBAEE,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBAEL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbiote-native/live-photo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "expo-live-photo wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Displays a Live Photo, iOS only.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"expo-live-photo": "57.0.2",
|
|
69
69
|
"expo-modules-core": "57.0.20",
|
|
70
|
-
"@symbiote-native/components": "3.
|
|
70
|
+
"@symbiote-native/components": "3.2.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"@angular/core": ">=20",
|
|
@@ -77,12 +77,12 @@
|
|
|
77
77
|
"solid-js": ">=1.9.0",
|
|
78
78
|
"svelte": ">=5.56.0",
|
|
79
79
|
"vue": ">=3.5.0",
|
|
80
|
-
"@symbiote-native/angular": "^3.
|
|
81
|
-
"@symbiote-native/engine": "^1.
|
|
82
|
-
"@symbiote-native/react": "^3.
|
|
83
|
-
"@symbiote-native/
|
|
84
|
-
"@symbiote-native/
|
|
85
|
-
"@symbiote-native/vue": "^3.
|
|
80
|
+
"@symbiote-native/angular": "^3.3.0",
|
|
81
|
+
"@symbiote-native/engine": "^1.6.0",
|
|
82
|
+
"@symbiote-native/react": "^3.3.0",
|
|
83
|
+
"@symbiote-native/solid": "^3.2.0",
|
|
84
|
+
"@symbiote-native/svelte": "^3.2.0",
|
|
85
|
+
"@symbiote-native/vue": "^3.3.0"
|
|
86
86
|
},
|
|
87
87
|
"peerDependenciesMeta": {
|
|
88
88
|
"@symbiote-native/angular": {
|
|
@@ -130,13 +130,13 @@
|
|
|
130
130
|
"solid-js": "^1.9.14",
|
|
131
131
|
"svelte": "^5.56.0",
|
|
132
132
|
"typescript": "~6.0.0",
|
|
133
|
-
"@symbiote-native/angular": "3.
|
|
134
|
-
"@symbiote-native/engine": "1.
|
|
135
|
-
"@symbiote-native/
|
|
136
|
-
"@symbiote-native/
|
|
137
|
-
"@symbiote-native/
|
|
138
|
-
"@symbiote-native/
|
|
139
|
-
"@symbiote-native/
|
|
133
|
+
"@symbiote-native/angular": "3.3.0",
|
|
134
|
+
"@symbiote-native/engine": "1.6.0",
|
|
135
|
+
"@symbiote-native/react": "3.3.0",
|
|
136
|
+
"@symbiote-native/solid": "3.2.0",
|
|
137
|
+
"@symbiote-native/svelte": "3.2.0",
|
|
138
|
+
"@symbiote-native/test-utils": "0.4.7",
|
|
139
|
+
"@symbiote-native/vue": "3.3.0"
|
|
140
140
|
},
|
|
141
141
|
"scripts": {
|
|
142
142
|
"typecheck": "tsc --build",
|
|
@@ -2,10 +2,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|
|
2
2
|
import { DescriptorOutlet, NativeViewBase } from '@symbiote-native/angular';
|
|
3
3
|
import type { IDescriptor } from '@symbiote-native/components';
|
|
4
4
|
import { createLivePhotoViewHandle, renderLivePhotoView } from '../../core';
|
|
5
|
-
import type {
|
|
6
|
-
ILivePhotoPlaybackStyle,
|
|
7
|
-
ILivePhotoViewProps,
|
|
8
|
-
} from '../../core';
|
|
5
|
+
import type { ILivePhotoPlaybackStyle, ILivePhotoViewProps } from '../../core';
|
|
9
6
|
|
|
10
7
|
/** Angular twin of `expo-live-photo`'s `LivePhotoView`, iOS only */
|
|
11
8
|
@Component({
|
|
@@ -16,7 +16,9 @@ export function LivePhotoView({
|
|
|
16
16
|
...props
|
|
17
17
|
}: ILivePhotoViewReactProps): ReactElement | null {
|
|
18
18
|
const [host] = useState(createHostNodeHolder);
|
|
19
|
-
useImperativeHandle(ref, () => createLivePhotoViewHandle(host.getNode), [
|
|
19
|
+
useImperativeHandle(ref, () => createLivePhotoViewHandle(host.getNode), [
|
|
20
|
+
host,
|
|
21
|
+
]);
|
|
20
22
|
const descriptor = renderLivePhotoView(props);
|
|
21
23
|
return descriptor ? descriptorToReact(host.capture(descriptor)) : null;
|
|
22
24
|
}
|