@symbiote-native/live-photo 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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -2
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +3 -0
  5. package/build/angular/live-photo-view/index.d.ts +21 -0
  6. package/build/angular/live-photo-view/index.js +147 -0
  7. package/build/core/constants.d.ts +2 -0
  8. package/build/core/constants.js +3 -0
  9. package/build/core/index.d.ts +3 -0
  10. package/build/core/index.js +2 -0
  11. package/build/core/live-photo-view.d.ts +8 -0
  12. package/build/core/live-photo-view.js +57 -0
  13. package/build/core/types.d.ts +50 -0
  14. package/build/core/types.js +1 -0
  15. package/build/react/index.d.ts +3 -0
  16. package/build/react/index.js +3 -0
  17. package/build/react/live-photo-view.d.ts +8 -0
  18. package/build/react/live-photo-view.js +13 -0
  19. package/build/solid/index.d.ts +3 -0
  20. package/build/solid/index.js +3 -0
  21. package/build/solid/live-photo-view.d.ts +8 -0
  22. package/build/solid/live-photo-view.js +18 -0
  23. package/build/svelte/index.d.ts +2 -0
  24. package/build/svelte/index.js +3 -0
  25. package/build/svelte/live-photo-view.svelte +31 -0
  26. package/build/svelte/live-photo-view.svelte.d.ts +7 -0
  27. package/build/vue/index.d.ts +2 -0
  28. package/build/vue/index.js +3 -0
  29. package/build/vue/live-photo-view.d.ts +3 -0
  30. package/build/vue/live-photo-view.js +18 -0
  31. package/build-ngc/angular/index.d.ts +2 -0
  32. package/build-ngc/angular/index.js +4 -0
  33. package/build-ngc/angular/index.js.map +1 -0
  34. package/build-ngc/angular/live-photo-view/index.d.ts +24 -0
  35. package/build-ngc/angular/live-photo-view/index.js +76 -0
  36. package/build-ngc/angular/live-photo-view/index.js.map +1 -0
  37. package/build-ngc/core/constants.d.ts +2 -0
  38. package/build-ngc/core/constants.js +4 -0
  39. package/build-ngc/core/constants.js.map +1 -0
  40. package/build-ngc/core/index.d.ts +3 -0
  41. package/build-ngc/core/index.js +3 -0
  42. package/build-ngc/core/index.js.map +1 -0
  43. package/build-ngc/core/live-photo-view.d.ts +8 -0
  44. package/build-ngc/core/live-photo-view.js +58 -0
  45. package/build-ngc/core/live-photo-view.js.map +1 -0
  46. package/build-ngc/core/types.d.ts +50 -0
  47. package/build-ngc/core/types.js +2 -0
  48. package/build-ngc/core/types.js.map +1 -0
  49. package/native-link.json +5 -0
  50. package/package.json +144 -3
  51. package/src/angular/index.ts +13 -0
  52. package/src/angular/live-photo-view/index.ts +56 -0
  53. package/src/core/constants.ts +4 -0
  54. package/src/core/index.ts +15 -0
  55. package/src/core/live-photo-view.ts +95 -0
  56. package/src/core/types.ts +66 -0
  57. package/src/react/index.ts +14 -0
  58. package/src/react/live-photo-view.ts +24 -0
  59. package/src/solid/index.ts +14 -0
  60. package/src/solid/live-photo-view.ts +27 -0
  61. package/src/svelte/index.ts +4 -0
  62. package/src/svelte/live-photo-view.svelte +31 -0
  63. package/src/svelte/svelte-ambient.d.ts +3 -0
  64. package/src/svelte/svelte-compile.test-helper.ts +6 -0
  65. package/src/vue/index.ts +13 -0
  66. package/src/vue/live-photo-view.ts +19 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,226 @@
1
- # Temporary Holding Version
1
+ # @symbiote-native/live-photo
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [`expo-live-photo`](https://docs.expo.dev/versions/latest/sdk/live-photo/) for **every**
4
+ [SymbioteNative](../../README.md) adapter: React, Vue, Svelte, Solid and Angular. Shows a Live
5
+ Photo, iOS only.
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
+
14
+ ## Install
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
+
34
+ ```bash
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
+
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.
68
+
69
+ ## Use it
70
+
71
+ ### React
72
+
73
+ ```tsx
74
+ import { useRef } from 'react';
75
+ import { LivePhotoView, type ILivePhotoViewHandle } from '@symbiote-native/live-photo/react';
76
+
77
+ const handle = useRef<ILivePhotoViewHandle>(null);
78
+
79
+ <LivePhotoView
80
+ ref={handle}
81
+ style={{ width: 300, height: 400 }}
82
+ source={{ photoUri, pairedVideoUri }}
83
+ onLoadError={error => console.warn(error.message)}
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';
115
+
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
+ }
134
+ ```
135
+
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.
211
+
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).
216
+
217
+ ## Test it
218
+
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).
@@ -0,0 +1,2 @@
1
+ export { LivePhotoView } from './live-photo-view';
2
+ export { LIVE_PHOTO_MODULE_NAME, livePhotoViewName, type ILivePhotoAsset, type ILivePhotoContentFit, type ILivePhotoLoadError, type ILivePhotoPlaybackStyle, type ILivePhotoViewHandle, type ILivePhotoViewProps, } from '../core';
@@ -0,0 +1,3 @@
1
+ // @symbiote-native/live-photo/angular: показ живых фото на общем ядре
2
+ export { LivePhotoView } from './live-photo-view/index.js';
3
+ export { LIVE_PHOTO_MODULE_NAME, livePhotoViewName, } from '../core/index.js';
@@ -0,0 +1,21 @@
1
+ import { NativeViewBase } from '@symbiote-native/angular';
2
+ import type { IDescriptor } from '@symbiote-native/components';
3
+ import type { ILivePhotoPlaybackStyle, ILivePhotoViewProps } from '../../core';
4
+ /** Angular twin of `expo-live-photo`'s `LivePhotoView`, iOS only */
5
+ export declare class LivePhotoView extends NativeViewBase {
6
+ source?: ILivePhotoViewProps['source'];
7
+ isMuted?: ILivePhotoViewProps['isMuted'];
8
+ contentFit?: ILivePhotoViewProps['contentFit'];
9
+ useDefaultGestureRecognizer?: ILivePhotoViewProps['useDefaultGestureRecognizer'];
10
+ onPlaybackStart?: ILivePhotoViewProps['onPlaybackStart'];
11
+ onPlaybackStop?: ILivePhotoViewProps['onPlaybackStop'];
12
+ onLoadStart?: ILivePhotoViewProps['onLoadStart'];
13
+ onPreviewPhotoLoad?: ILivePhotoViewProps['onPreviewPhotoLoad'];
14
+ onLoadComplete?: ILivePhotoViewProps['onLoadComplete'];
15
+ onLoadError?: ILivePhotoViewProps['onLoadError'];
16
+ private readonly handle;
17
+ protected readonly propNames: readonly ["source", "isMuted", "contentFit", "useDefaultGestureRecognizer", "onPlaybackStart", "onPlaybackStop", "onLoadStart", "onPreviewPhotoLoad", "onLoadComplete", "onLoadError"];
18
+ protected renderView(props: object): IDescriptor | null;
19
+ startPlayback(playbackStyle?: ILivePhotoPlaybackStyle): void;
20
+ stopPlayback(): void;
21
+ }
@@ -0,0 +1,147 @@
1
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
2
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
3
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
4
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
5
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
6
+ var _, done = false;
7
+ for (var i = decorators.length - 1; i >= 0; i--) {
8
+ var context = {};
9
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
10
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
11
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
12
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
13
+ if (kind === "accessor") {
14
+ if (result === void 0) continue;
15
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
16
+ if (_ = accept(result.get)) descriptor.get = _;
17
+ if (_ = accept(result.set)) descriptor.set = _;
18
+ if (_ = accept(result.init)) initializers.unshift(_);
19
+ }
20
+ else if (_ = accept(result)) {
21
+ if (kind === "field") initializers.unshift(_);
22
+ else descriptor[key] = _;
23
+ }
24
+ }
25
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
26
+ done = true;
27
+ };
28
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
29
+ var useValue = arguments.length > 2;
30
+ for (var i = 0; i < initializers.length; i++) {
31
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
32
+ }
33
+ return useValue ? value : void 0;
34
+ };
35
+ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
36
+ import { DescriptorOutlet, NativeViewBase } from '@symbiote-native/angular';
37
+ import { createLivePhotoViewHandle, renderLivePhotoView } from '../../core/index.js';
38
+ /** Angular twin of `expo-live-photo`'s `LivePhotoView`, iOS only */
39
+ let LivePhotoView = (() => {
40
+ let _classDecorators = [Component({
41
+ selector: 'LivePhotoView',
42
+ standalone: true,
43
+ imports: [DescriptorOutlet],
44
+ changeDetection: ChangeDetectionStrategy.OnPush,
45
+ template: `@if (descriptor; as node) {
46
+ <symbiote-descriptor-outlet [node]="node" />
47
+ }`,
48
+ })];
49
+ let _classDescriptor;
50
+ let _classExtraInitializers = [];
51
+ let _classThis;
52
+ let _classSuper = NativeViewBase;
53
+ let _source_decorators;
54
+ let _source_initializers = [];
55
+ let _source_extraInitializers = [];
56
+ let _isMuted_decorators;
57
+ let _isMuted_initializers = [];
58
+ let _isMuted_extraInitializers = [];
59
+ let _contentFit_decorators;
60
+ let _contentFit_initializers = [];
61
+ let _contentFit_extraInitializers = [];
62
+ let _useDefaultGestureRecognizer_decorators;
63
+ let _useDefaultGestureRecognizer_initializers = [];
64
+ let _useDefaultGestureRecognizer_extraInitializers = [];
65
+ let _onPlaybackStart_decorators;
66
+ let _onPlaybackStart_initializers = [];
67
+ let _onPlaybackStart_extraInitializers = [];
68
+ let _onPlaybackStop_decorators;
69
+ let _onPlaybackStop_initializers = [];
70
+ let _onPlaybackStop_extraInitializers = [];
71
+ let _onLoadStart_decorators;
72
+ let _onLoadStart_initializers = [];
73
+ let _onLoadStart_extraInitializers = [];
74
+ let _onPreviewPhotoLoad_decorators;
75
+ let _onPreviewPhotoLoad_initializers = [];
76
+ let _onPreviewPhotoLoad_extraInitializers = [];
77
+ let _onLoadComplete_decorators;
78
+ let _onLoadComplete_initializers = [];
79
+ let _onLoadComplete_extraInitializers = [];
80
+ let _onLoadError_decorators;
81
+ let _onLoadError_initializers = [];
82
+ let _onLoadError_extraInitializers = [];
83
+ var LivePhotoView = class extends _classSuper {
84
+ static { _classThis = this; }
85
+ static {
86
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
87
+ _source_decorators = [Input()];
88
+ _isMuted_decorators = [Input()];
89
+ _contentFit_decorators = [Input()];
90
+ _useDefaultGestureRecognizer_decorators = [Input()];
91
+ _onPlaybackStart_decorators = [Input()];
92
+ _onPlaybackStop_decorators = [Input()];
93
+ _onLoadStart_decorators = [Input()];
94
+ _onPreviewPhotoLoad_decorators = [Input()];
95
+ _onLoadComplete_decorators = [Input()];
96
+ _onLoadError_decorators = [Input()];
97
+ __esDecorate(null, null, _source_decorators, { kind: "field", name: "source", static: false, private: false, access: { has: obj => "source" in obj, get: obj => obj.source, set: (obj, value) => { obj.source = value; } }, metadata: _metadata }, _source_initializers, _source_extraInitializers);
98
+ __esDecorate(null, null, _isMuted_decorators, { kind: "field", name: "isMuted", static: false, private: false, access: { has: obj => "isMuted" in obj, get: obj => obj.isMuted, set: (obj, value) => { obj.isMuted = value; } }, metadata: _metadata }, _isMuted_initializers, _isMuted_extraInitializers);
99
+ __esDecorate(null, null, _contentFit_decorators, { kind: "field", name: "contentFit", static: false, private: false, access: { has: obj => "contentFit" in obj, get: obj => obj.contentFit, set: (obj, value) => { obj.contentFit = value; } }, metadata: _metadata }, _contentFit_initializers, _contentFit_extraInitializers);
100
+ __esDecorate(null, null, _useDefaultGestureRecognizer_decorators, { kind: "field", name: "useDefaultGestureRecognizer", static: false, private: false, access: { has: obj => "useDefaultGestureRecognizer" in obj, get: obj => obj.useDefaultGestureRecognizer, set: (obj, value) => { obj.useDefaultGestureRecognizer = value; } }, metadata: _metadata }, _useDefaultGestureRecognizer_initializers, _useDefaultGestureRecognizer_extraInitializers);
101
+ __esDecorate(null, null, _onPlaybackStart_decorators, { kind: "field", name: "onPlaybackStart", static: false, private: false, access: { has: obj => "onPlaybackStart" in obj, get: obj => obj.onPlaybackStart, set: (obj, value) => { obj.onPlaybackStart = value; } }, metadata: _metadata }, _onPlaybackStart_initializers, _onPlaybackStart_extraInitializers);
102
+ __esDecorate(null, null, _onPlaybackStop_decorators, { kind: "field", name: "onPlaybackStop", static: false, private: false, access: { has: obj => "onPlaybackStop" in obj, get: obj => obj.onPlaybackStop, set: (obj, value) => { obj.onPlaybackStop = value; } }, metadata: _metadata }, _onPlaybackStop_initializers, _onPlaybackStop_extraInitializers);
103
+ __esDecorate(null, null, _onLoadStart_decorators, { kind: "field", name: "onLoadStart", static: false, private: false, access: { has: obj => "onLoadStart" in obj, get: obj => obj.onLoadStart, set: (obj, value) => { obj.onLoadStart = value; } }, metadata: _metadata }, _onLoadStart_initializers, _onLoadStart_extraInitializers);
104
+ __esDecorate(null, null, _onPreviewPhotoLoad_decorators, { kind: "field", name: "onPreviewPhotoLoad", static: false, private: false, access: { has: obj => "onPreviewPhotoLoad" in obj, get: obj => obj.onPreviewPhotoLoad, set: (obj, value) => { obj.onPreviewPhotoLoad = value; } }, metadata: _metadata }, _onPreviewPhotoLoad_initializers, _onPreviewPhotoLoad_extraInitializers);
105
+ __esDecorate(null, null, _onLoadComplete_decorators, { kind: "field", name: "onLoadComplete", static: false, private: false, access: { has: obj => "onLoadComplete" in obj, get: obj => obj.onLoadComplete, set: (obj, value) => { obj.onLoadComplete = value; } }, metadata: _metadata }, _onLoadComplete_initializers, _onLoadComplete_extraInitializers);
106
+ __esDecorate(null, null, _onLoadError_decorators, { kind: "field", name: "onLoadError", static: false, private: false, access: { has: obj => "onLoadError" in obj, get: obj => obj.onLoadError, set: (obj, value) => { obj.onLoadError = value; } }, metadata: _metadata }, _onLoadError_initializers, _onLoadError_extraInitializers);
107
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
108
+ LivePhotoView = _classThis = _classDescriptor.value;
109
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
110
+ __runInitializers(_classThis, _classExtraInitializers);
111
+ }
112
+ source = __runInitializers(this, _source_initializers, void 0);
113
+ isMuted = (__runInitializers(this, _source_extraInitializers), __runInitializers(this, _isMuted_initializers, void 0));
114
+ contentFit = (__runInitializers(this, _isMuted_extraInitializers), __runInitializers(this, _contentFit_initializers, void 0));
115
+ useDefaultGestureRecognizer = (__runInitializers(this, _contentFit_extraInitializers), __runInitializers(this, _useDefaultGestureRecognizer_initializers, void 0));
116
+ onPlaybackStart = (__runInitializers(this, _useDefaultGestureRecognizer_extraInitializers), __runInitializers(this, _onPlaybackStart_initializers, void 0));
117
+ onPlaybackStop = (__runInitializers(this, _onPlaybackStart_extraInitializers), __runInitializers(this, _onPlaybackStop_initializers, void 0));
118
+ onLoadStart = (__runInitializers(this, _onPlaybackStop_extraInitializers), __runInitializers(this, _onLoadStart_initializers, void 0));
119
+ onPreviewPhotoLoad = (__runInitializers(this, _onLoadStart_extraInitializers), __runInitializers(this, _onPreviewPhotoLoad_initializers, void 0));
120
+ onLoadComplete = (__runInitializers(this, _onPreviewPhotoLoad_extraInitializers), __runInitializers(this, _onLoadComplete_initializers, void 0));
121
+ onLoadError = (__runInitializers(this, _onLoadComplete_extraInitializers), __runInitializers(this, _onLoadError_initializers, void 0));
122
+ handle = (__runInitializers(this, _onLoadError_extraInitializers), createLivePhotoViewHandle(() => this.hostNode()));
123
+ propNames = [
124
+ 'source',
125
+ 'isMuted',
126
+ 'contentFit',
127
+ 'useDefaultGestureRecognizer',
128
+ 'onPlaybackStart',
129
+ 'onPlaybackStop',
130
+ 'onLoadStart',
131
+ 'onPreviewPhotoLoad',
132
+ 'onLoadComplete',
133
+ 'onLoadError',
134
+ ];
135
+ renderView(props) {
136
+ return renderLivePhotoView(props);
137
+ }
138
+ startPlayback(playbackStyle) {
139
+ this.handle.startPlayback(playbackStyle);
140
+ }
141
+ stopPlayback() {
142
+ this.handle.stopPlayback();
143
+ }
144
+ };
145
+ return LivePhotoView = _classThis;
146
+ })();
147
+ export { LivePhotoView };
@@ -0,0 +1,2 @@
1
+ export declare const LIVE_PHOTO_MODULE_NAME = "ExpoLivePhoto";
2
+ export declare const LIVE_PHOTO_PACKAGE_NAME = "expo-live-photo";
@@ -0,0 +1,3 @@
1
+ export const LIVE_PHOTO_MODULE_NAME = 'ExpoLivePhoto';
2
+ // The label upstream puts into its `UnavailabilityError`
3
+ export const LIVE_PHOTO_PACKAGE_NAME = 'expo-live-photo';
@@ -0,0 +1,3 @@
1
+ export { LIVE_PHOTO_MODULE_NAME } from './constants';
2
+ export { createLivePhotoViewHandle, ensureLivePhotoViewRegistered, livePhotoViewName, renderLivePhotoView, } from './live-photo-view';
3
+ export type { ILivePhotoAsset, ILivePhotoContentFit, ILivePhotoLoadError, ILivePhotoPlaybackStyle, ILivePhotoViewHandle, ILivePhotoViewProps, } from './types';
@@ -0,0 +1,2 @@
1
+ export { LIVE_PHOTO_MODULE_NAME } from './constants.js';
2
+ export { createLivePhotoViewHandle, ensureLivePhotoViewRegistered, livePhotoViewName, renderLivePhotoView, } from './live-photo-view.js';
@@ -0,0 +1,8 @@
1
+ import type { IDescriptor } from '@symbiote-native/components';
2
+ import type { ISymbioteNode } from '@symbiote-native/engine';
3
+ import type { ILivePhotoViewHandle } from './types';
4
+ export declare const livePhotoViewName: () => string;
5
+ export declare function ensureLivePhotoViewRegistered(): boolean;
6
+ /** `null` means the caller renders nothing, live photos exist only on iOS */
7
+ export declare function renderLivePhotoView(props: object): IDescriptor | null;
8
+ export declare function createLivePhotoViewHandle(getNode: () => ISymbioteNode | null | undefined): ILivePhotoViewHandle;
@@ -0,0 +1,57 @@
1
+ // Ядро `LivePhotoView`: нативный view с живыми фото, только iOS
2
+ import { Platform, UnavailabilityError, requireNativeModule, requireNativeViewManager, } from 'expo-modules-core';
3
+ import { el } from '@symbiote-native/components';
4
+ import { defineExpoNativeView, defineExpoViewMethods, getNativeTag, isDevBuild, } from '@symbiote-native/engine';
5
+ import { LIVE_PHOTO_MODULE_NAME, LIVE_PHOTO_PACKAGE_NAME } from './constants.js';
6
+ const LOAD_ERROR_PROP = 'onLoadError';
7
+ // Регистрация идёт при рендере: побочный эффект барреля теряется в release
8
+ const livePhotoView = defineExpoNativeView(requireNativeViewManager, LIVE_PHOTO_MODULE_NAME);
9
+ const callViewFunction = defineExpoViewMethods(requireNativeModule, LIVE_PHOTO_MODULE_NAME);
10
+ export const livePhotoViewName = livePhotoView.name;
11
+ function isAvailable() {
12
+ return Platform.select({ ios: true, default: false });
13
+ }
14
+ export function ensureLivePhotoViewRegistered() {
15
+ return isAvailable() && livePhotoView.ensureRegistered();
16
+ }
17
+ function isLoadError(value) {
18
+ return typeof Reflect.get(Object(value), 'message') === 'string';
19
+ }
20
+ // Нативное событие несёт полезную нагрузку в `nativeEvent`, наружу уходит она сама
21
+ function toLoadErrorHandler(onLoadError) {
22
+ return (event) => {
23
+ if (typeof onLoadError !== 'function')
24
+ return;
25
+ if (isLoadError(event.nativeEvent))
26
+ onLoadError(event.nativeEvent);
27
+ };
28
+ }
29
+ /** `null` means the caller renders nothing, live photos exist only on iOS */
30
+ export function renderLivePhotoView(props) {
31
+ if (!ensureLivePhotoViewRegistered()) {
32
+ if (isDevBuild())
33
+ console.warn("'LivePhotoView' is not available.");
34
+ return null;
35
+ }
36
+ return el(livePhotoViewName(), {
37
+ ...Object.fromEntries(Object.entries(props)),
38
+ [LOAD_ERROR_PROP]: toLoadErrorHandler(Reflect.get(props, LOAD_ERROR_PROP)),
39
+ });
40
+ }
41
+ // A view that has not committed yet has no native tag, the call is dropped like upstream's
42
+ // `nativeRef.current?.` does
43
+ function callIfMounted(getNode, method, args) {
44
+ if (!isAvailable()) {
45
+ throw new UnavailabilityError(LIVE_PHOTO_PACKAGE_NAME, method);
46
+ }
47
+ const node = getNode();
48
+ if (node && getNativeTag(node) !== undefined) {
49
+ callViewFunction(node, method, args);
50
+ }
51
+ }
52
+ export function createLivePhotoViewHandle(getNode) {
53
+ return {
54
+ startPlayback: (playbackStyle) => callIfMounted(getNode, 'startPlayback', [playbackStyle ?? 'full']),
55
+ stopPlayback: () => callIfMounted(getNode, 'stopPlayback', []),
56
+ };
57
+ }
@@ -0,0 +1,50 @@
1
+ import type { IAccessibilityProps, IAriaProps, IResponderProps } from '@symbiote-native/components';
2
+ import type { IStyleProp, ISymbioteEvent, IViewStyle } from '@symbiote-native/engine';
3
+ /**
4
+ * A live photo asset
5
+ *
6
+ * The photo and the video must come from one unaltered live photo file, the pairing lives in
7
+ * their metadata and native cannot rebuild it
8
+ */
9
+ export type ILivePhotoAsset = {
10
+ /** The URI of the photo part of the live photo */
11
+ photoUri: string;
12
+ /** The URI of the video part of the live photo */
13
+ pairedVideoUri: string;
14
+ };
15
+ export type ILivePhotoLoadError = {
16
+ /** Reason for the load failure */
17
+ message: string;
18
+ };
19
+ /** `contain` fits the larger dimension to the target, `cover` fills the whole target */
20
+ export type ILivePhotoContentFit = 'contain' | 'cover';
21
+ /** `hint` plays a short part of the video, `full` plays all of it */
22
+ export type ILivePhotoPlaybackStyle = 'hint' | 'full';
23
+ export type ILivePhotoViewProps = IAccessibilityProps & IAriaProps & IResponderProps & {
24
+ /** The live photo asset to display */
25
+ source?: ILivePhotoAsset | null;
26
+ /** Whether the live photo plays without audio, `true` by default */
27
+ isMuted?: boolean;
28
+ /** `contain` by default */
29
+ contentFit?: ILivePhotoContentFit;
30
+ /** Playback starts on press and hold when `true`, which is the default */
31
+ useDefaultGestureRecognizer?: boolean;
32
+ onPlaybackStart?: () => void;
33
+ onPlaybackStop?: () => void;
34
+ onLoadStart?: () => void;
35
+ /** The preview photo is loaded */
36
+ onPreviewPhotoLoad?: () => void;
37
+ /** The live photo is loaded and ready to play */
38
+ onLoadComplete?: () => void;
39
+ onLoadError?: (error: ILivePhotoLoadError) => void;
40
+ style?: IStyleProp<IViewStyle>;
41
+ testID?: string;
42
+ nativeID?: string;
43
+ onLayout?: (event: ISymbioteEvent) => void;
44
+ };
45
+ /** What a ref to the view hands out */
46
+ export type ILivePhotoViewHandle = {
47
+ /** Plays the video part, the full video unless `playbackStyle` says `hint` */
48
+ startPlayback: (playbackStyle?: ILivePhotoPlaybackStyle) => void;
49
+ stopPlayback: () => void;
50
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export { LivePhotoView } from './live-photo-view';
2
+ export type { ILivePhotoViewReactProps } from './live-photo-view';
3
+ export { LIVE_PHOTO_MODULE_NAME, livePhotoViewName, type ILivePhotoAsset, type ILivePhotoContentFit, type ILivePhotoLoadError, type ILivePhotoPlaybackStyle, type ILivePhotoViewHandle, type ILivePhotoViewProps, } from '../core';
@@ -0,0 +1,3 @@
1
+ // @symbiote-native/live-photo/react: показ живых фото на общем ядре
2
+ export { LivePhotoView } from './live-photo-view.js';
3
+ export { LIVE_PHOTO_MODULE_NAME, livePhotoViewName, } from '../core/index.js';
@@ -0,0 +1,8 @@
1
+ import type { ReactElement, Ref } from 'react';
2
+ import type { ILivePhotoViewHandle, ILivePhotoViewProps } from '../core';
3
+ export type ILivePhotoViewReactProps = ILivePhotoViewProps & {
4
+ ref?: Ref<ILivePhotoViewHandle>;
5
+ className?: string;
6
+ };
7
+ /** React twin of `expo-live-photo`'s `LivePhotoView`, iOS only */
8
+ export declare function LivePhotoView({ ref, ...props }: ILivePhotoViewReactProps): ReactElement | null;
@@ -0,0 +1,13 @@
1
+ import { useImperativeHandle, useState } from 'react';
2
+ import { descriptorToReact } from '@symbiote-native/react';
3
+ import { createHostNodeHolder } from '@symbiote-native/components';
4
+ import { createLivePhotoViewHandle, renderLivePhotoView } from '../core/index.js';
5
+ /** React twin of `expo-live-photo`'s `LivePhotoView`, iOS only */
6
+ export function LivePhotoView({ ref, ...props }) {
7
+ const [host] = useState(createHostNodeHolder);
8
+ useImperativeHandle(ref, () => createLivePhotoViewHandle(host.getNode), [
9
+ host,
10
+ ]);
11
+ const descriptor = renderLivePhotoView(props);
12
+ return descriptor ? descriptorToReact(host.capture(descriptor)) : null;
13
+ }
@@ -0,0 +1,3 @@
1
+ export { LivePhotoView } from './live-photo-view';
2
+ export type { ILivePhotoViewSolidProps } from './live-photo-view';
3
+ export { LIVE_PHOTO_MODULE_NAME, livePhotoViewName, type ILivePhotoAsset, type ILivePhotoContentFit, type ILivePhotoLoadError, type ILivePhotoPlaybackStyle, type ILivePhotoViewHandle, type ILivePhotoViewProps, } from '../core';
@@ -0,0 +1,3 @@
1
+ // @symbiote-native/live-photo/solid: показ живых фото на общем ядре
2
+ export { LivePhotoView } from './live-photo-view.js';
3
+ export { LIVE_PHOTO_MODULE_NAME, livePhotoViewName, } from '../core/index.js';
@@ -0,0 +1,8 @@
1
+ import type { ISymbioteNode } from '@symbiote-native/engine';
2
+ import type { ILivePhotoViewHandle, ILivePhotoViewProps } from '../core';
3
+ export type ILivePhotoViewSolidProps = ILivePhotoViewProps & {
4
+ /** Gets the handle that starts and stops the playback */
5
+ ref?: (handle: ILivePhotoViewHandle) => void;
6
+ };
7
+ /** Solid twin of `expo-live-photo`'s `LivePhotoView`, iOS only */
8
+ export declare function LivePhotoView(props: ILivePhotoViewSolidProps): ISymbioteNode | null;
@@ -0,0 +1,18 @@
1
+ import { splitProps } from 'solid-js';
2
+ import { descriptorToSolid } from '@symbiote-native/solid';
3
+ import { createHostNodeHolder } from '@symbiote-native/components';
4
+ import { createLivePhotoViewHandle, renderLivePhotoView } from '../core/index.js';
5
+ /** Solid twin of `expo-live-photo`'s `LivePhotoView`, iOS only */
6
+ export function LivePhotoView(props) {
7
+ const [own, viewProps] = splitProps(props, ['ref']);
8
+ const host = createHostNodeHolder();
9
+ const render = () => {
10
+ const descriptor = renderLivePhotoView({ ...viewProps });
11
+ return descriptor && host.capture(descriptor);
12
+ };
13
+ const initial = render();
14
+ if (!initial)
15
+ return null;
16
+ own.ref?.(createLivePhotoViewHandle(host.getNode));
17
+ return descriptorToSolid(() => render() ?? initial);
18
+ }
@@ -0,0 +1,2 @@
1
+ export { default as LivePhotoView } from './live-photo-view.svelte';
2
+ export * from '../core';