@ng-native/expo 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +413 -1
- package/dist/app-info.d.ts +58 -0
- package/dist/app-info.js +71 -0
- package/dist/app-info.js.map +1 -0
- package/dist/apple-sign-in-button.d.ts +16 -0
- package/dist/apple-sign-in-button.js +55 -0
- package/dist/apple-sign-in-button.js.map +1 -0
- package/dist/apple-sign-in.d.ts +68 -0
- package/dist/apple-sign-in.js +120 -0
- package/dist/apple-sign-in.js.map +1 -0
- package/dist/assets.d.ts +24 -0
- package/dist/assets.js +47 -0
- package/dist/assets.js.map +1 -0
- package/dist/background-task.d.ts +66 -0
- package/dist/background-task.js +82 -0
- package/dist/background-task.js.map +1 -0
- package/dist/battery.d.ts +46 -0
- package/dist/battery.js +76 -0
- package/dist/battery.js.map +1 -0
- package/dist/biometrics.d.ts +45 -0
- package/dist/biometrics.js +53 -0
- package/dist/biometrics.js.map +1 -0
- package/dist/brightness.d.ts +38 -0
- package/dist/brightness.js +81 -0
- package/dist/brightness.js.map +1 -0
- package/dist/browser.d.ts +42 -0
- package/dist/browser.js +42 -0
- package/dist/browser.js.map +1 -0
- package/dist/camera.d.ts +48 -0
- package/dist/camera.js +59 -0
- package/dist/camera.js.map +1 -0
- package/dist/clipboard.d.ts +43 -0
- package/dist/clipboard.js +60 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/community-views.d.ts +14 -0
- package/dist/community-views.js +76 -0
- package/dist/community-views.js.map +1 -0
- package/dist/crypto.d.ts +56 -0
- package/dist/crypto.js +84 -0
- package/dist/crypto.js.map +1 -0
- package/dist/database.d.ts +47 -0
- package/dist/database.js +108 -0
- package/dist/database.js.map +1 -0
- package/dist/document-picker.d.ts +34 -0
- package/dist/document-picker.js +40 -0
- package/dist/document-picker.js.map +1 -0
- package/dist/dom-component.d.ts +87 -0
- package/dist/dom-component.js +139 -0
- package/dist/dom-component.js.map +1 -0
- package/dist/expo-image.d.ts +33 -0
- package/dist/expo-image.js +57 -0
- package/dist/expo-image.js.map +1 -0
- package/dist/expo-ui-components.d.ts +327 -0
- package/dist/expo-ui-components.js +707 -0
- package/dist/expo-ui-components.js.map +1 -0
- package/dist/expo-ui.d.ts +46 -0
- package/dist/expo-ui.js +128 -0
- package/dist/expo-ui.js.map +1 -0
- package/dist/file-system.d.ts +63 -0
- package/dist/file-system.js +67 -0
- package/dist/file-system.js.map +1 -0
- package/dist/fonts.d.ts +90 -0
- package/dist/fonts.js +118 -0
- package/dist/fonts.js.map +1 -0
- package/dist/glass.d.ts +33 -0
- package/dist/glass.js +72 -0
- package/dist/glass.js.map +1 -0
- package/dist/haptics.d.ts +49 -0
- package/dist/haptics.js +64 -0
- package/dist/haptics.js.map +1 -0
- package/dist/image-editor.d.ts +62 -0
- package/dist/image-editor.js +101 -0
- package/dist/image-editor.js.map +1 -0
- package/dist/image-picker.d.ts +49 -0
- package/dist/image-picker.js +58 -0
- package/dist/image-picker.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/keep-awake.d.ts +39 -0
- package/dist/keep-awake.js +80 -0
- package/dist/keep-awake.js.map +1 -0
- package/dist/language-model.d.ts +129 -0
- package/dist/language-model.js +225 -0
- package/dist/language-model.js.map +1 -0
- package/dist/locale.d.ts +51 -0
- package/dist/locale.js +78 -0
- package/dist/locale.js.map +1 -0
- package/dist/location.d.ts +68 -0
- package/dist/location.js +83 -0
- package/dist/location.js.map +1 -0
- package/dist/map-view.d.ts +203 -0
- package/dist/map-view.js +184 -0
- package/dist/map-view.js.map +1 -0
- package/dist/media-library.d.ts +103 -0
- package/dist/media-library.js +160 -0
- package/dist/media-library.js.map +1 -0
- package/dist/native-state.d.ts +22 -0
- package/dist/native-state.js +43 -0
- package/dist/native-state.js.map +1 -0
- package/dist/native.d.ts +42 -0
- package/dist/native.js +73 -0
- package/dist/native.js.map +1 -0
- package/dist/network.d.ts +35 -0
- package/dist/network.js +60 -0
- package/dist/network.js.map +1 -0
- package/dist/notifications.d.ts +175 -0
- package/dist/notifications.js +296 -0
- package/dist/notifications.js.map +1 -0
- package/dist/observed.d.ts +37 -0
- package/dist/observed.js +55 -0
- package/dist/observed.js.map +1 -0
- package/dist/orientation.d.ts +35 -0
- package/dist/orientation.js +78 -0
- package/dist/orientation.js.map +1 -0
- package/dist/permissions.d.ts +54 -0
- package/dist/permissions.js +72 -0
- package/dist/permissions.js.map +1 -0
- package/dist/player.d.ts +92 -0
- package/dist/player.js +141 -0
- package/dist/player.js.map +1 -0
- package/dist/register-expo-view.d.ts +55 -0
- package/dist/register-expo-view.js +110 -0
- package/dist/register-expo-view.js.map +1 -0
- package/dist/screen-capture.d.ts +58 -0
- package/dist/screen-capture.js +78 -0
- package/dist/screen-capture.js.map +1 -0
- package/dist/segmented-control.d.ts +32 -0
- package/dist/segmented-control.js +55 -0
- package/dist/segmented-control.js.map +1 -0
- package/dist/sensors.d.ts +85 -0
- package/dist/sensors.js +115 -0
- package/dist/sensors.js.map +1 -0
- package/dist/splash-screen.d.ts +73 -0
- package/dist/splash-screen.js +115 -0
- package/dist/splash-screen.js.map +1 -0
- package/dist/store-review.d.ts +43 -0
- package/dist/store-review.js +52 -0
- package/dist/store-review.js.map +1 -0
- package/dist/store.d.ts +108 -0
- package/dist/store.js +247 -0
- package/dist/store.js.map +1 -0
- package/dist/symbol.d.ts +36 -0
- package/dist/symbol.js +69 -0
- package/dist/symbol.js.map +1 -0
- package/dist/tracking.d.ts +45 -0
- package/dist/tracking.js +54 -0
- package/dist/tracking.js.map +1 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +13 -0
- package/dist/transforms.js.map +1 -0
- package/dist/updates.d.ts +61 -0
- package/dist/updates.js +97 -0
- package/dist/updates.js.map +1 -0
- package/package.json +397 -3
package/dist/map-view.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<expo-map>`: `expo-maps`' Apple map on iOS and its Google map on Android, with markers,
|
|
3
|
+
* polylines, polygons and circles, a camera, and taps.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* registerExpoMap(Platform.OS); // once, at startup
|
|
7
|
+
*
|
|
8
|
+
* @Component({
|
|
9
|
+
* imports: [MapView],
|
|
10
|
+
* template: `
|
|
11
|
+
* <expo-map class="flex-1" [markers]="stations" [cameraPosition]="london"
|
|
12
|
+
* (markerClick)="open($event.nativeEvent.id)" />
|
|
13
|
+
* `,
|
|
14
|
+
* })
|
|
15
|
+
* export class Stations {
|
|
16
|
+
* private readonly map = viewChild.required(MapView);
|
|
17
|
+
* zoomIn() { this.map().setCameraPosition({ coordinates: KINGS_CROSS, zoom: 16 }); }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* One element rather than one per platform because the two views share everything an app reaches
|
|
22
|
+
* for first: a marker list, a camera position, the same three events and the same two view
|
|
23
|
+
* functions. What differs is inside the option objects, and each platform's native side reads only
|
|
24
|
+
* the fields it knows, so an app writes one template. The enum-valued options are the exception
|
|
25
|
+
* (a map type is `STANDARD` on iOS and `NORMAL` on Android) and are typed as either.
|
|
26
|
+
*
|
|
27
|
+
* Each input goes straight to the view as the prop `expo-maps` names it, except that a shape's
|
|
28
|
+
* colours are converted first, as `expo-maps`' own React components run them through
|
|
29
|
+
* `processColor`: native reads the converted value. The outputs are declared
|
|
30
|
+
* for their types only and are never emitted: the view's own event is what reaches `(markerClick)`,
|
|
31
|
+
* as with the components in `expo-ui-components.ts`, so `$event` is a native event and the payload
|
|
32
|
+
* is its `nativeEvent`.
|
|
33
|
+
*
|
|
34
|
+
* `setCameraPosition` and `selectMarker` are the functions `expo-maps` defines on the view. In
|
|
35
|
+
* React they are methods on a ref; underneath, native finds the view by the `nativeTag` it is
|
|
36
|
+
* called with, so they are called here with the tag the engine committed the view under.
|
|
37
|
+
*
|
|
38
|
+
* A committed view has a tag before native has mounted anything under it, and a function called
|
|
39
|
+
* in between finds no view to run on. So calls are held until the map says it is there, which both
|
|
40
|
+
* platforms do with a camera move once it first appears, and `ready` says whether it has.
|
|
41
|
+
*/
|
|
42
|
+
import { Component, DestroyRef, computed, ElementRef, InjectionToken, inject, input, output, signal, } from '@angular/core';
|
|
43
|
+
import { Engine } from '@ng-native/fabric';
|
|
44
|
+
import { expoModule, optional } from "./native.js";
|
|
45
|
+
import { registerExpoView } from "./register-expo-view.js";
|
|
46
|
+
import * as i0 from "@angular/core";
|
|
47
|
+
/**
|
|
48
|
+
* Make `<expo-map>` commit as the platform's map: `ExpoAppleMaps` on iOS, `ExpoGoogleMaps` on
|
|
49
|
+
* Android. Each is its module's only view, so its default. Call once, before the first map.
|
|
50
|
+
*/
|
|
51
|
+
export function registerExpoMap(platform) {
|
|
52
|
+
registerExpoView('expo-map', platform === 'ios' ? 'ExpoAppleMaps' : 'ExpoGoogleMaps');
|
|
53
|
+
}
|
|
54
|
+
export class MapView {
|
|
55
|
+
/** Overridden in a test to move a camera without a map. */
|
|
56
|
+
static SOURCE = new InjectionToken('angular-native.mapViewSource', {
|
|
57
|
+
factory: () => {
|
|
58
|
+
const core = optional(() => require('expo-modules-core'));
|
|
59
|
+
// Only one of the two modules exists on a platform. Each map is its module's default
|
|
60
|
+
// view, so its functions are under the module's own name.
|
|
61
|
+
const functions = (module) => core?.requireOptionalNativeModule(module)?.ViewPrototypes?.[module];
|
|
62
|
+
return expoModule('expo-maps', () => functions('ExpoAppleMaps') ?? functions('ExpoGoogleMaps'));
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
functions = inject(MapView.SOURCE);
|
|
66
|
+
engine = inject(Engine);
|
|
67
|
+
node = inject(ElementRef).nativeElement;
|
|
68
|
+
appeared = signal(false, /* @ts-ignore */
|
|
69
|
+
...(ngDevMode ? [{ debugName: "appeared" }] : /* istanbul ignore next */ []));
|
|
70
|
+
/** Calls made before the map appeared, in the order they were made. */
|
|
71
|
+
held = [];
|
|
72
|
+
markers = input(/* @ts-ignore */
|
|
73
|
+
...(ngDevMode ? [undefined, { debugName: "markers" }] : /* istanbul ignore next */ []));
|
|
74
|
+
/** Lines, such as a route. */
|
|
75
|
+
polylines = input(/* @ts-ignore */
|
|
76
|
+
...(ngDevMode ? [undefined, { debugName: "polylines" }] : /* istanbul ignore next */ []));
|
|
77
|
+
/** Filled shapes. */
|
|
78
|
+
polygons = input(/* @ts-ignore */
|
|
79
|
+
...(ngDevMode ? [undefined, { debugName: "polygons" }] : /* istanbul ignore next */ []));
|
|
80
|
+
circles = input(/* @ts-ignore */
|
|
81
|
+
...(ngDevMode ? [undefined, { debugName: "circles" }] : /* istanbul ignore next */ []));
|
|
82
|
+
/** Where the camera is. A changed value moves it; so does `setCameraPosition`. */
|
|
83
|
+
cameraPosition = input(/* @ts-ignore */
|
|
84
|
+
...(ngDevMode ? [undefined, { debugName: "cameraPosition" }] : /* istanbul ignore next */ []));
|
|
85
|
+
properties = input(/* @ts-ignore */
|
|
86
|
+
...(ngDevMode ? [undefined, { debugName: "properties" }] : /* istanbul ignore next */ []));
|
|
87
|
+
uiSettings = input(/* @ts-ignore */
|
|
88
|
+
...(ngDevMode ? [undefined, { debugName: "uiSettings" }] : /* istanbul ignore next */ []));
|
|
89
|
+
colorScheme = input(/* @ts-ignore */
|
|
90
|
+
...(ngDevMode ? [undefined, { debugName: "colorScheme" }] : /* istanbul ignore next */ []));
|
|
91
|
+
/** A tap on the map, not on a marker. */
|
|
92
|
+
mapClick = output();
|
|
93
|
+
/** A tap on a marker, with the marker as it was given. iOS 18 and later on iOS. */
|
|
94
|
+
markerClick = output();
|
|
95
|
+
/** The camera moved, by the user or by the app. Also sent once when the map first appears. */
|
|
96
|
+
cameraMove = output();
|
|
97
|
+
/** A tap on a polyline, a polygon or a circle. iOS 18 and later on iOS. */
|
|
98
|
+
polylineClick = output();
|
|
99
|
+
polygonClick = output();
|
|
100
|
+
circleClick = output();
|
|
101
|
+
nativePolylines = computed(() => this.polylines()?.map((line) => ({ ...line, color: this.engine.color(line.color) })), /* @ts-ignore */
|
|
102
|
+
...(ngDevMode ? [{ debugName: "nativePolylines" }] : /* istanbul ignore next */ []));
|
|
103
|
+
nativePolygons = computed(() => this.polygons()?.map((shape) => this.filled(shape)), /* @ts-ignore */
|
|
104
|
+
...(ngDevMode ? [{ debugName: "nativePolygons" }] : /* istanbul ignore next */ []));
|
|
105
|
+
nativeCircles = computed(() => this.circles()?.map((shape) => this.filled(shape)), /* @ts-ignore */
|
|
106
|
+
...(ngDevMode ? [{ debugName: "nativeCircles" }] : /* istanbul ignore next */ []));
|
|
107
|
+
/**
|
|
108
|
+
* Whether the native map is on screen, so its functions can reach it. Until then
|
|
109
|
+
* `setCameraPosition` and `selectMarker` are held, and sent in order once it is.
|
|
110
|
+
*/
|
|
111
|
+
ready = this.appeared.asReadonly();
|
|
112
|
+
constructor() {
|
|
113
|
+
const stop = this.engine.setEventListener(this.node, 'topCameraMove', () => this.appear());
|
|
114
|
+
inject(DestroyRef).onDestroy(() => {
|
|
115
|
+
stop();
|
|
116
|
+
this.release(false);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Move the camera. Held until the map is on screen, and false without the module or if the map
|
|
121
|
+
* goes before it gets there: there was never a map to move.
|
|
122
|
+
*/
|
|
123
|
+
setCameraPosition(position) {
|
|
124
|
+
return this.call((functions, nativeTag) => functions.setCameraPosition.call({ nativeTag }, position));
|
|
125
|
+
}
|
|
126
|
+
/** Select a marker by its `id`, as a tap would, or clear the selection with none. */
|
|
127
|
+
selectMarker(id, options) {
|
|
128
|
+
return this.call((functions, nativeTag) => functions.selectMarker.call({ nativeTag }, id, options));
|
|
129
|
+
}
|
|
130
|
+
async call(run) {
|
|
131
|
+
const functions = this.functions;
|
|
132
|
+
if (!functions)
|
|
133
|
+
return false;
|
|
134
|
+
if (!this.appeared() && !(await new Promise((resolve) => this.held.push(resolve)))) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const nativeTag = this.engine.tagOf(this.node);
|
|
138
|
+
if (nativeTag === null)
|
|
139
|
+
return false;
|
|
140
|
+
await run(functions, nativeTag);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
filled(shape) {
|
|
144
|
+
return {
|
|
145
|
+
...shape,
|
|
146
|
+
color: this.engine.color(shape.color),
|
|
147
|
+
lineColor: this.engine.color(shape.lineColor),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
appear() {
|
|
151
|
+
if (this.appeared())
|
|
152
|
+
return;
|
|
153
|
+
this.appeared.set(true);
|
|
154
|
+
this.release(true);
|
|
155
|
+
}
|
|
156
|
+
/** Let every held call go, to native if the map is there and back to its caller if not. */
|
|
157
|
+
release(ready) {
|
|
158
|
+
const held = this.held;
|
|
159
|
+
this.held = [];
|
|
160
|
+
for (const resolve of held)
|
|
161
|
+
resolve(ready);
|
|
162
|
+
}
|
|
163
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: MapView, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
164
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: MapView, isStandalone: true, selector: "expo-map", inputs: { markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, polylines: { classPropertyName: "polylines", publicName: "polylines", isSignal: true, isRequired: false, transformFunction: null }, polygons: { classPropertyName: "polygons", publicName: "polygons", isSignal: true, isRequired: false, transformFunction: null }, circles: { classPropertyName: "circles", publicName: "circles", isSignal: true, isRequired: false, transformFunction: null }, cameraPosition: { classPropertyName: "cameraPosition", publicName: "cameraPosition", isSignal: true, isRequired: false, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, uiSettings: { classPropertyName: "uiSettings", publicName: "uiSettings", isSignal: true, isRequired: false, transformFunction: null }, colorScheme: { classPropertyName: "colorScheme", publicName: "colorScheme", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { mapClick: "mapClick", markerClick: "markerClick", cameraMove: "cameraMove", polylineClick: "polylineClick", polygonClick: "polygonClick", circleClick: "circleClick" }, host: { properties: { "markers": "markers()", "polylines": "nativePolylines()", "polygons": "nativePolygons()", "circles": "nativeCircles()", "cameraPosition": "cameraPosition()", "properties": "properties()", "uiSettings": "uiSettings()", "colorScheme": "colorScheme()" } }, exportAs: ["map"], ngImport: i0, template: '', isInline: true });
|
|
165
|
+
}
|
|
166
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: MapView, decorators: [{
|
|
167
|
+
type: Component,
|
|
168
|
+
args: [{
|
|
169
|
+
selector: 'expo-map',
|
|
170
|
+
exportAs: 'map',
|
|
171
|
+
template: '',
|
|
172
|
+
host: {
|
|
173
|
+
'[markers]': 'markers()',
|
|
174
|
+
'[polylines]': 'nativePolylines()',
|
|
175
|
+
'[polygons]': 'nativePolygons()',
|
|
176
|
+
'[circles]': 'nativeCircles()',
|
|
177
|
+
'[cameraPosition]': 'cameraPosition()',
|
|
178
|
+
'[properties]': 'properties()',
|
|
179
|
+
'[uiSettings]': 'uiSettings()',
|
|
180
|
+
'[colorScheme]': 'colorScheme()',
|
|
181
|
+
},
|
|
182
|
+
}]
|
|
183
|
+
}], ctorParameters: () => [], propDecorators: { markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], polylines: [{ type: i0.Input, args: [{ isSignal: true, alias: "polylines", required: false }] }], polygons: [{ type: i0.Input, args: [{ isSignal: true, alias: "polygons", required: false }] }], circles: [{ type: i0.Input, args: [{ isSignal: true, alias: "circles", required: false }] }], cameraPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameraPosition", required: false }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: false }] }], uiSettings: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiSettings", required: false }] }], colorScheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "colorScheme", required: false }] }], mapClick: [{ type: i0.Output, args: ["mapClick"] }], markerClick: [{ type: i0.Output, args: ["markerClick"] }], cameraMove: [{ type: i0.Output, args: ["cameraMove"] }], polylineClick: [{ type: i0.Output, args: ["polylineClick"] }], polygonClick: [{ type: i0.Output, args: ["polygonClick"] }], circleClick: [{ type: i0.Output, args: ["circleClick"] }] } });
|
|
184
|
+
//# sourceMappingURL=map-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-view.js","sourceRoot":"","sources":["../src/map-view.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `<expo-map>`: `expo-maps`' Apple map on iOS and its Google map on Android, with markers,\n * polylines, polygons and circles, a camera, and taps.\n *\n * ```ts\n * registerExpoMap(Platform.OS); // once, at startup\n *\n * @Component({\n * imports: [MapView],\n * template: `\n * <expo-map class=\"flex-1\" [markers]=\"stations\" [cameraPosition]=\"london\"\n * (markerClick)=\"open($event.nativeEvent.id)\" />\n * `,\n * })\n * export class Stations {\n * private readonly map = viewChild.required(MapView);\n * zoomIn() { this.map().setCameraPosition({ coordinates: KINGS_CROSS, zoom: 16 }); }\n * }\n * ```\n *\n * One element rather than one per platform because the two views share everything an app reaches\n * for first: a marker list, a camera position, the same three events and the same two view\n * functions. What differs is inside the option objects, and each platform's native side reads only\n * the fields it knows, so an app writes one template. The enum-valued options are the exception\n * (a map type is `STANDARD` on iOS and `NORMAL` on Android) and are typed as either.\n *\n * Each input goes straight to the view as the prop `expo-maps` names it, except that a shape's\n * colours are converted first, as `expo-maps`' own React components run them through\n * `processColor`: native reads the converted value. The outputs are declared\n * for their types only and are never emitted: the view's own event is what reaches `(markerClick)`,\n * as with the components in `expo-ui-components.ts`, so `$event` is a native event and the payload\n * is its `nativeEvent`.\n *\n * `setCameraPosition` and `selectMarker` are the functions `expo-maps` defines on the view. In\n * React they are methods on a ref; underneath, native finds the view by the `nativeTag` it is\n * called with, so they are called here with the tag the engine committed the view under.\n *\n * A committed view has a tag before native has mounted anything under it, and a function called\n * in between finds no view to run on. So calls are held until the map says it is there, which both\n * platforms do with a camera move once it first appears, and `ready` says whether it has.\n */\nimport {\n Component,\n DestroyRef,\n computed,\n ElementRef,\n InjectionToken,\n inject,\n input,\n output,\n signal,\n} from '@angular/core';\nimport { Engine, type EngineNode, type NativeSyntheticEvent } from '@ng-native/fabric';\nimport type { AppleMaps, CameraMoveEvent, Coordinates, GoogleMaps } from 'expo-maps';\nimport { expoModule, optional } from './native.ts';\nimport { registerExpoView } from './register-expo-view.ts';\n\n/** `expo-maps`' types with its enums as their string values, so no import of the module is needed. */\ntype Plain<T> = T extends string\n ? `${T}`\n : T extends readonly (infer U)[]\n ? readonly Plain<U>[]\n : T extends object\n ? { readonly [K in keyof T]: Plain<T[K]> }\n : T;\n\nexport type MapCoordinates = Coordinates;\nexport type MapCameraPosition = import('expo-maps').CameraPosition;\n\n/**\n * A marker. `id`, `coordinates` and `title` work on both platforms; `systemImage`, `monogram` and\n * `tintColor` are Apple's, `snippet`, `draggable`, `showCallout`, `anchor` and `zIndex` Google's.\n */\nexport type MapMarker = Plain<AppleMaps.Marker> & Plain<Omit<GoogleMaps.Marker, 'icon'>>;\n\n/** The map's properties: Apple's on iOS, Google's on Android. */\nexport type MapProperties = Plain<AppleMaps.MapProperties> | Plain<GoogleMaps.MapProperties>;\n\n/** Which controls the map shows. Each platform reads the settings it has. */\nexport type MapUiSettings = AppleMaps.MapUISettings & GoogleMaps.MapUISettings;\n\n/** `LIGHT` and `DARK` on both; `AUTOMATIC` is iOS's and `FOLLOW_SYSTEM` Android's. */\nexport type MapColorScheme = `${AppleMaps.MapColorScheme}` | `${GoogleMaps.MapColorScheme}`;\n\n/** A colour, as a template writes one: `'#ff5a36'`, `'rgba(0, 0, 0, 0.5)'`, `'green'`. */\nexport type MapColor = string;\n\n/**\n * A line through `coordinates`, such as a route. `contourStyle` is Apple's, `geodesic` Google's:\n * both draw the line along the Earth's curve rather than straight across the map.\n */\nexport interface MapPolyline {\n readonly id?: string;\n readonly coordinates: readonly MapCoordinates[];\n readonly color?: MapColor;\n readonly width?: number;\n readonly contourStyle?: `${AppleMaps.ContourStyle}`;\n readonly geodesic?: boolean;\n}\n\n/** A filled shape with `coordinates` as its corners. */\nexport interface MapPolygon {\n readonly id?: string;\n readonly coordinates: readonly MapCoordinates[];\n readonly color?: MapColor;\n readonly lineColor?: MapColor;\n readonly lineWidth?: number;\n}\n\n/** A filled circle, `radius` metres across from `center`. */\nexport interface MapCircle {\n readonly id?: string;\n readonly center: MapCoordinates;\n readonly radius: number;\n readonly color?: MapColor;\n readonly lineColor?: MapColor;\n readonly lineWidth?: number;\n}\n\n/** Where to move the camera. `duration`, in milliseconds, is Android's; iOS does not animate it. */\nexport type MapCameraMove = MapCameraPosition & { readonly duration?: number };\n\nexport interface MapSelectOptions {\n /** The zoom to animate to. */\n readonly zoom?: number;\n /** Whether to move the camera to the marker at all. Defaults to true. */\n readonly moveCamera?: boolean;\n}\n\nexport type MapClickEvent = NativeSyntheticEvent<{ readonly coordinates: MapCoordinates }>;\nexport type MapMarkerClickEvent = NativeSyntheticEvent<MapMarker>;\nexport type MapCameraMoveEvent = NativeSyntheticEvent<CameraMoveEvent>;\n/** A tapped shape, as native holds it: its colours are native's own values, not strings. */\ntype Tapped<T> = Omit<T, 'color' | 'lineColor'> & {\n readonly color?: unknown;\n readonly lineColor?: unknown;\n};\nexport type MapPolylineClickEvent = NativeSyntheticEvent<Tapped<MapPolyline>>;\nexport type MapPolygonClickEvent = NativeSyntheticEvent<Tapped<MapPolygon>>;\n/** Android sends the circle's `center` and where it was tapped; iOS sends its centre as `coordinates`. */\nexport type MapCircleClickEvent = NativeSyntheticEvent<\n Omit<Tapped<MapCircle>, 'center'> & {\n readonly center?: MapCoordinates;\n readonly coordinates?: MapCoordinates;\n readonly clickCoordinates?: MapCoordinates;\n }\n>;\n\n/** The functions `expo-maps` defines on both views, called with the view's tag as `this`. */\nexport interface MapViewFunctions {\n setCameraPosition(this: { nativeTag: number }, position?: MapCameraMove): Promise<void>;\n selectMarker(this: { nativeTag: number }, id?: string, options?: MapSelectOptions): Promise<void>;\n}\n\n/**\n * Make `<expo-map>` commit as the platform's map: `ExpoAppleMaps` on iOS, `ExpoGoogleMaps` on\n * Android. Each is its module's only view, so its default. Call once, before the first map.\n */\nexport function registerExpoMap(platform: 'ios' | 'android'): void {\n registerExpoView('expo-map', platform === 'ios' ? 'ExpoAppleMaps' : 'ExpoGoogleMaps');\n}\n\n@Component({\n selector: 'expo-map',\n exportAs: 'map',\n template: '',\n host: {\n '[markers]': 'markers()',\n '[polylines]': 'nativePolylines()',\n '[polygons]': 'nativePolygons()',\n '[circles]': 'nativeCircles()',\n '[cameraPosition]': 'cameraPosition()',\n '[properties]': 'properties()',\n '[uiSettings]': 'uiSettings()',\n '[colorScheme]': 'colorScheme()',\n },\n})\nexport class MapView {\n /** Overridden in a test to move a camera without a map. */\n static readonly SOURCE = new InjectionToken<MapViewFunctions | null>(\n 'angular-native.mapViewSource',\n {\n factory: () => {\n const core = optional(\n () => require('expo-modules-core') as typeof import('expo-modules-core'),\n );\n // Only one of the two modules exists on a platform. Each map is its module's default\n // view, so its functions are under the module's own name.\n const functions = (module: string) =>\n core?.requireOptionalNativeModule<{\n ViewPrototypes?: Record<string, MapViewFunctions>;\n }>(module)?.ViewPrototypes?.[module];\n return expoModule(\n 'expo-maps',\n () => functions('ExpoAppleMaps') ?? functions('ExpoGoogleMaps'),\n );\n },\n },\n );\n\n private readonly functions = inject(MapView.SOURCE);\n private readonly engine = inject(Engine);\n private readonly node = inject<ElementRef<EngineNode>>(ElementRef).nativeElement;\n private readonly appeared = signal(false);\n /** Calls made before the map appeared, in the order they were made. */\n private held: ((ready: boolean) => void)[] = [];\n\n readonly markers = input<readonly MapMarker[]>();\n /** Lines, such as a route. */\n readonly polylines = input<readonly MapPolyline[]>();\n /** Filled shapes. */\n readonly polygons = input<readonly MapPolygon[]>();\n readonly circles = input<readonly MapCircle[]>();\n /** Where the camera is. A changed value moves it; so does `setCameraPosition`. */\n readonly cameraPosition = input<MapCameraPosition>();\n readonly properties = input<MapProperties>();\n readonly uiSettings = input<MapUiSettings>();\n readonly colorScheme = input<MapColorScheme>();\n\n /** A tap on the map, not on a marker. */\n readonly mapClick = output<MapClickEvent>();\n /** A tap on a marker, with the marker as it was given. iOS 18 and later on iOS. */\n readonly markerClick = output<MapMarkerClickEvent>();\n /** The camera moved, by the user or by the app. Also sent once when the map first appears. */\n readonly cameraMove = output<MapCameraMoveEvent>();\n /** A tap on a polyline, a polygon or a circle. iOS 18 and later on iOS. */\n readonly polylineClick = output<MapPolylineClickEvent>();\n readonly polygonClick = output<MapPolygonClickEvent>();\n readonly circleClick = output<MapCircleClickEvent>();\n\n protected readonly nativePolylines = computed(() =>\n this.polylines()?.map((line) => ({ ...line, color: this.engine.color(line.color) })),\n );\n protected readonly nativePolygons = computed(() =>\n this.polygons()?.map((shape) => this.filled(shape)),\n );\n protected readonly nativeCircles = computed(() =>\n this.circles()?.map((shape) => this.filled(shape)),\n );\n\n /**\n * Whether the native map is on screen, so its functions can reach it. Until then\n * `setCameraPosition` and `selectMarker` are held, and sent in order once it is.\n */\n readonly ready = this.appeared.asReadonly();\n\n constructor() {\n const stop = this.engine.setEventListener(this.node, 'topCameraMove', () => this.appear());\n inject(DestroyRef).onDestroy(() => {\n stop();\n this.release(false);\n });\n }\n\n /**\n * Move the camera. Held until the map is on screen, and false without the module or if the map\n * goes before it gets there: there was never a map to move.\n */\n setCameraPosition(position: MapCameraMove): Promise<boolean> {\n return this.call((functions, nativeTag) =>\n functions.setCameraPosition.call({ nativeTag }, position),\n );\n }\n\n /** Select a marker by its `id`, as a tap would, or clear the selection with none. */\n selectMarker(id?: string, options?: MapSelectOptions): Promise<boolean> {\n return this.call((functions, nativeTag) =>\n functions.selectMarker.call({ nativeTag }, id, options),\n );\n }\n\n private async call(\n run: (functions: MapViewFunctions, nativeTag: number) => Promise<void>,\n ): Promise<boolean> {\n const functions = this.functions;\n if (!functions) return false;\n if (!this.appeared() && !(await new Promise<boolean>((resolve) => this.held.push(resolve)))) {\n return false;\n }\n const nativeTag = this.engine.tagOf(this.node);\n if (nativeTag === null) return false;\n await run(functions, nativeTag);\n return true;\n }\n\n private filled<T extends MapPolygon | MapCircle>(shape: T) {\n return {\n ...shape,\n color: this.engine.color(shape.color),\n lineColor: this.engine.color(shape.lineColor),\n };\n }\n\n private appear(): void {\n if (this.appeared()) return;\n this.appeared.set(true);\n this.release(true);\n }\n\n /** Let every held call go, to native if the map is there and back to its caller if not. */\n private release(ready: boolean): void {\n const held = this.held;\n this.held = [];\n for (const resolve of held) resolve(ready);\n }\n}\n"]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MediaLibrary`, bound to `expo-media-library`: saving to and reading from the user's photos and
|
|
3
|
+
* videos.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { AssetField, MediaLibrary, MediaType } from '@ng-native/expo/media-library';
|
|
7
|
+
*
|
|
8
|
+
* private readonly library = inject(MediaLibrary);
|
|
9
|
+
*
|
|
10
|
+
* async keep(uri: string): Promise<void> {
|
|
11
|
+
* await this.library.save(uri);
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* async recentPhotos(): Promise<Asset[]> {
|
|
15
|
+
* if (!(await this.library.permission.ensure())) return [];
|
|
16
|
+
* return this.library.assets((query) =>
|
|
17
|
+
* query.eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).orderBy(AssetField.CREATION_TIME).limit(30),
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The module's API is its classes: an `Asset` and an `Album` are handles whose details are read
|
|
23
|
+
* with their own async getters, and a `Query` is built by chaining. This service is where they
|
|
24
|
+
* are reached from, so a test can provide all three, and where the two permissions are: full
|
|
25
|
+
* access to read the library, and the write-only access saving needs, which is the one to ask for
|
|
26
|
+
* when that is all the app does.
|
|
27
|
+
*
|
|
28
|
+
* Changes to the library are a signal, but only once `watch()` is called: starting the listener
|
|
29
|
+
* reads the library, which is not something to do before the user has granted access.
|
|
30
|
+
*
|
|
31
|
+
* Without the module installed, every query answers empty, saving answers null, and the
|
|
32
|
+
* permissions are refused.
|
|
33
|
+
*/
|
|
34
|
+
import { InjectionToken, type Signal } from '@angular/core';
|
|
35
|
+
import type { Album, Asset, AssetField as ExpoAssetField, AssetMetadata, MediaLibraryAssetsChangeEvent, MediaType as ExpoMediaType, Query } from 'expo-media-library';
|
|
36
|
+
import { Permission } from './permissions.ts';
|
|
37
|
+
import * as i0 from "@angular/core";
|
|
38
|
+
type Expo = typeof import('expo-media-library');
|
|
39
|
+
/** The module's functions and classes this service calls. A test provides fakes of each. */
|
|
40
|
+
export type NativeMediaLibrary = Pick<Expo, 'getPermissionsAsync' | 'requestPermissionsAsync' | 'presentPermissionsPicker' | 'addListener' | 'Asset' | 'Album' | 'Query'>;
|
|
41
|
+
type Args<K extends keyof NativeMediaLibrary> = NativeMediaLibrary[K] extends (...args: infer A) => unknown ? A : never;
|
|
42
|
+
/**
|
|
43
|
+
* The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module
|
|
44
|
+
* loads the module, which a test in Node cannot; these are the same strings, typed as the enum.
|
|
45
|
+
*/
|
|
46
|
+
export declare const MediaType: typeof ExpoMediaType;
|
|
47
|
+
/** The fields a query filters and sorts on, as the module's own enum. */
|
|
48
|
+
export declare const AssetField: typeof ExpoAssetField;
|
|
49
|
+
export declare class MediaLibrary {
|
|
50
|
+
/** Overridden in a test to save photos to a library that is not there. */
|
|
51
|
+
static readonly SOURCE: InjectionToken<NativeMediaLibrary | null>;
|
|
52
|
+
private readonly native;
|
|
53
|
+
private readonly destroyRef;
|
|
54
|
+
private readonly changed;
|
|
55
|
+
private readonly changes;
|
|
56
|
+
private watching;
|
|
57
|
+
/** Full access: reading the library, as well as adding to it. */
|
|
58
|
+
readonly permission: Permission;
|
|
59
|
+
/** Write-only access, which is all `save()` needs and what it asks for. */
|
|
60
|
+
readonly writePermission: Permission;
|
|
61
|
+
/**
|
|
62
|
+
* Asks with the module's own options: write-only, and on Android 13 and later which of
|
|
63
|
+
* `'photo'`, `'video'` and `'audio'` to ask for. The answer also updates {@link permission}.
|
|
64
|
+
*/
|
|
65
|
+
requestPermission(...options: Args<'requestPermissionsAsync'>): Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* On iOS and Android 14 and later, after the user has granted access to only some photos, shows
|
|
68
|
+
* the system picker for changing which. Does nothing otherwise.
|
|
69
|
+
*/
|
|
70
|
+
presentPermissionsPicker(...mediaTypes: Args<'presentPermissionsPicker'>): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Saves a local image or video file to the library, into an album if one is given. Asks for
|
|
73
|
+
* write-only access first; null if it is refused.
|
|
74
|
+
*/
|
|
75
|
+
save(uri: string, album?: Album): Promise<Asset | null>;
|
|
76
|
+
/**
|
|
77
|
+
* The assets a query finds, built on the module's own `Query`: `eq`, `within`, `gt`, `lt`,
|
|
78
|
+
* `orderBy`, `limit`, `offset` and `album`. Every asset without one.
|
|
79
|
+
*/
|
|
80
|
+
assets(build?: (query: Query) => Query): Promise<Asset[]>;
|
|
81
|
+
/** The same query, answered with each asset's details at once rather than as handles. */
|
|
82
|
+
metadata(build?: (query: Query) => Query): Promise<AssetMetadata[]>;
|
|
83
|
+
/** The handle for an asset whose id is already known, such as one kept from an earlier query. */
|
|
84
|
+
asset(id: string): Asset | null;
|
|
85
|
+
deleteAssets(assets: Asset[]): Promise<void>;
|
|
86
|
+
albums(): Promise<Album[]>;
|
|
87
|
+
/** The album with this title, or null. */
|
|
88
|
+
album(title: string): Promise<Album | null>;
|
|
89
|
+
/** A new album holding the assets given, copied into it unless `move` is true. */
|
|
90
|
+
createAlbum(name: string, assets: Asset[] | string[], move?: boolean): Promise<Album | null>;
|
|
91
|
+
/** Deletes the albums, and with `deleteAssets` the assets in them too. */
|
|
92
|
+
deleteAlbums(albums: Album[], deleteAssets?: boolean): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* The library's most recent change, from the first call on: on iOS the assets inserted, deleted
|
|
95
|
+
* and updated, on Android an empty event. Call it once access is granted, since listening reads
|
|
96
|
+
* the library. The listener is removed when the app is destroyed.
|
|
97
|
+
*/
|
|
98
|
+
watch(): Signal<MediaLibraryAssetsChangeEvent | null>;
|
|
99
|
+
private permissionFor;
|
|
100
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<MediaLibrary, never>;
|
|
101
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
102
|
+
}
|
|
103
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MediaLibrary`, bound to `expo-media-library`: saving to and reading from the user's photos and
|
|
3
|
+
* videos.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { AssetField, MediaLibrary, MediaType } from '@ng-native/expo/media-library';
|
|
7
|
+
*
|
|
8
|
+
* private readonly library = inject(MediaLibrary);
|
|
9
|
+
*
|
|
10
|
+
* async keep(uri: string): Promise<void> {
|
|
11
|
+
* await this.library.save(uri);
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* async recentPhotos(): Promise<Asset[]> {
|
|
15
|
+
* if (!(await this.library.permission.ensure())) return [];
|
|
16
|
+
* return this.library.assets((query) =>
|
|
17
|
+
* query.eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).orderBy(AssetField.CREATION_TIME).limit(30),
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The module's API is its classes: an `Asset` and an `Album` are handles whose details are read
|
|
23
|
+
* with their own async getters, and a `Query` is built by chaining. This service is where they
|
|
24
|
+
* are reached from, so a test can provide all three, and where the two permissions are: full
|
|
25
|
+
* access to read the library, and the write-only access saving needs, which is the one to ask for
|
|
26
|
+
* when that is all the app does.
|
|
27
|
+
*
|
|
28
|
+
* Changes to the library are a signal, but only once `watch()` is called: starting the listener
|
|
29
|
+
* reads the library, which is not something to do before the user has granted access.
|
|
30
|
+
*
|
|
31
|
+
* Without the module installed, every query answers empty, saving answers null, and the
|
|
32
|
+
* permissions are refused.
|
|
33
|
+
*/
|
|
34
|
+
import { DestroyRef, InjectionToken, Service, inject, signal } from '@angular/core';
|
|
35
|
+
import { expoModule } from "./native.js";
|
|
36
|
+
import { Permission } from "./permissions.js";
|
|
37
|
+
import * as i0 from "@angular/core";
|
|
38
|
+
/**
|
|
39
|
+
* The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module
|
|
40
|
+
* loads the module, which a test in Node cannot; these are the same strings, typed as the enum.
|
|
41
|
+
*/
|
|
42
|
+
export const MediaType = {
|
|
43
|
+
UNKNOWN: 'unknown',
|
|
44
|
+
IMAGE: 'image',
|
|
45
|
+
AUDIO: 'audio',
|
|
46
|
+
VIDEO: 'video',
|
|
47
|
+
};
|
|
48
|
+
/** The fields a query filters and sorts on, as the module's own enum. */
|
|
49
|
+
export const AssetField = {
|
|
50
|
+
CREATION_TIME: 'creationTime',
|
|
51
|
+
MODIFICATION_TIME: 'modificationTime',
|
|
52
|
+
MEDIA_TYPE: 'mediaType',
|
|
53
|
+
WIDTH: 'width',
|
|
54
|
+
HEIGHT: 'height',
|
|
55
|
+
DURATION: 'duration',
|
|
56
|
+
IS_FAVORITE: 'isFavorite',
|
|
57
|
+
};
|
|
58
|
+
/** The permission answer without the module: nothing granted, and nothing to ask. */
|
|
59
|
+
const UNAVAILABLE = { status: 'denied', granted: false, canAskAgain: false };
|
|
60
|
+
export class MediaLibrary {
|
|
61
|
+
/** Overridden in a test to save photos to a library that is not there. */
|
|
62
|
+
static SOURCE = new InjectionToken('angular-native.mediaLibrarySource', {
|
|
63
|
+
factory: () => expoModule('expo-media-library', () => require('expo-media-library')),
|
|
64
|
+
});
|
|
65
|
+
native = inject(MediaLibrary.SOURCE);
|
|
66
|
+
destroyRef = inject(DestroyRef);
|
|
67
|
+
changed = signal(null, /* @ts-ignore */
|
|
68
|
+
...(ngDevMode ? [{ debugName: "changed" }] : /* istanbul ignore next */ []));
|
|
69
|
+
changes = this.changed.asReadonly();
|
|
70
|
+
watching = false;
|
|
71
|
+
/** Full access: reading the library, as well as adding to it. */
|
|
72
|
+
permission = this.permissionFor(false);
|
|
73
|
+
/** Write-only access, which is all `save()` needs and what it asks for. */
|
|
74
|
+
writePermission = this.permissionFor(true);
|
|
75
|
+
/**
|
|
76
|
+
* Asks with the module's own options: write-only, and on Android 13 and later which of
|
|
77
|
+
* `'photo'`, `'video'` and `'audio'` to ask for. The answer also updates {@link permission}.
|
|
78
|
+
*/
|
|
79
|
+
async requestPermission(...options) {
|
|
80
|
+
if (!this.native)
|
|
81
|
+
return false;
|
|
82
|
+
await this.native.requestPermissionsAsync(...options);
|
|
83
|
+
return this.permission.check();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* On iOS and Android 14 and later, after the user has granted access to only some photos, shows
|
|
87
|
+
* the system picker for changing which. Does nothing otherwise.
|
|
88
|
+
*/
|
|
89
|
+
async presentPermissionsPicker(...mediaTypes) {
|
|
90
|
+
await this.native?.presentPermissionsPicker(...mediaTypes);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Saves a local image or video file to the library, into an album if one is given. Asks for
|
|
94
|
+
* write-only access first; null if it is refused.
|
|
95
|
+
*/
|
|
96
|
+
async save(uri, album) {
|
|
97
|
+
if (!this.native || !(await this.writePermission.ensure()))
|
|
98
|
+
return null;
|
|
99
|
+
return this.native.Asset.create(uri, album);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The assets a query finds, built on the module's own `Query`: `eq`, `within`, `gt`, `lt`,
|
|
103
|
+
* `orderBy`, `limit`, `offset` and `album`. Every asset without one.
|
|
104
|
+
*/
|
|
105
|
+
async assets(build = (query) => query) {
|
|
106
|
+
if (!this.native)
|
|
107
|
+
return [];
|
|
108
|
+
return build(new this.native.Query()).exe();
|
|
109
|
+
}
|
|
110
|
+
/** The same query, answered with each asset's details at once rather than as handles. */
|
|
111
|
+
async metadata(build = (query) => query) {
|
|
112
|
+
if (!this.native)
|
|
113
|
+
return [];
|
|
114
|
+
return build(new this.native.Query()).exeForMetadata();
|
|
115
|
+
}
|
|
116
|
+
/** The handle for an asset whose id is already known, such as one kept from an earlier query. */
|
|
117
|
+
asset(id) {
|
|
118
|
+
return this.native ? new this.native.Asset(id) : null;
|
|
119
|
+
}
|
|
120
|
+
async deleteAssets(assets) {
|
|
121
|
+
await this.native?.Asset.delete(assets);
|
|
122
|
+
}
|
|
123
|
+
async albums() {
|
|
124
|
+
return (await this.native?.Album.getAll()) ?? [];
|
|
125
|
+
}
|
|
126
|
+
/** The album with this title, or null. */
|
|
127
|
+
async album(title) {
|
|
128
|
+
return (await this.native?.Album.get(title)) ?? null;
|
|
129
|
+
}
|
|
130
|
+
/** A new album holding the assets given, copied into it unless `move` is true. */
|
|
131
|
+
async createAlbum(name, assets, move) {
|
|
132
|
+
return (await this.native?.Album.create(name, assets, move)) ?? null;
|
|
133
|
+
}
|
|
134
|
+
/** Deletes the albums, and with `deleteAssets` the assets in them too. */
|
|
135
|
+
async deleteAlbums(albums, deleteAssets) {
|
|
136
|
+
await this.native?.Album.delete(albums, deleteAssets);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The library's most recent change, from the first call on: on iOS the assets inserted, deleted
|
|
140
|
+
* and updated, on Android an empty event. Call it once access is granted, since listening reads
|
|
141
|
+
* the library. The listener is removed when the app is destroyed.
|
|
142
|
+
*/
|
|
143
|
+
watch() {
|
|
144
|
+
if (this.native && !this.watching) {
|
|
145
|
+
this.watching = true;
|
|
146
|
+
const subscription = this.native.addListener((event) => this.changed.set(event));
|
|
147
|
+
this.destroyRef.onDestroy(() => subscription.remove());
|
|
148
|
+
}
|
|
149
|
+
return this.changes;
|
|
150
|
+
}
|
|
151
|
+
permissionFor(writeOnly) {
|
|
152
|
+
return Permission.of(() => this.native?.getPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE), () => this.native?.requestPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE));
|
|
153
|
+
}
|
|
154
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: MediaLibrary, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
155
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.2.0", ngImport: i0, type: MediaLibrary });
|
|
156
|
+
}
|
|
157
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: MediaLibrary, decorators: [{
|
|
158
|
+
type: Service
|
|
159
|
+
}] });
|
|
160
|
+
//# sourceMappingURL=media-library.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-library.js","sourceRoot":"","sources":["../src/media-library.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `MediaLibrary`, bound to `expo-media-library`: saving to and reading from the user's photos and\n * videos.\n *\n * ```ts\n * import { AssetField, MediaLibrary, MediaType } from '@ng-native/expo/media-library';\n *\n * private readonly library = inject(MediaLibrary);\n *\n * async keep(uri: string): Promise<void> {\n * await this.library.save(uri);\n * }\n *\n * async recentPhotos(): Promise<Asset[]> {\n * if (!(await this.library.permission.ensure())) return [];\n * return this.library.assets((query) =>\n * query.eq(AssetField.MEDIA_TYPE, MediaType.IMAGE).orderBy(AssetField.CREATION_TIME).limit(30),\n * );\n * }\n * ```\n *\n * The module's API is its classes: an `Asset` and an `Album` are handles whose details are read\n * with their own async getters, and a `Query` is built by chaining. This service is where they\n * are reached from, so a test can provide all three, and where the two permissions are: full\n * access to read the library, and the write-only access saving needs, which is the one to ask for\n * when that is all the app does.\n *\n * Changes to the library are a signal, but only once `watch()` is called: starting the listener\n * reads the library, which is not something to do before the user has granted access.\n *\n * Without the module installed, every query answers empty, saving answers null, and the\n * permissions are refused.\n */\nimport { DestroyRef, InjectionToken, Service, inject, signal, type Signal } from '@angular/core';\nimport type {\n Album,\n Asset,\n AssetField as ExpoAssetField,\n AssetMetadata,\n MediaLibraryAssetsChangeEvent,\n MediaType as ExpoMediaType,\n Query,\n} from 'expo-media-library';\nimport { expoModule } from './native.ts';\nimport { Permission, type PermissionResponse } from './permissions.ts';\n\ntype Expo = typeof import('expo-media-library');\n\n/** The module's functions and classes this service calls. A test provides fakes of each. */\nexport type NativeMediaLibrary = Pick<\n Expo,\n | 'getPermissionsAsync'\n | 'requestPermissionsAsync'\n | 'presentPermissionsPicker'\n | 'addListener'\n | 'Asset'\n | 'Album'\n | 'Query'\n>;\n\ntype Args<K extends keyof NativeMediaLibrary> = NativeMediaLibrary[K] extends (\n ...args: infer A\n) => unknown\n ? A\n : never;\n\n/**\n * The kinds of asset, as `expo-media-library`'s own enum. Importing the enum from the module\n * loads the module, which a test in Node cannot; these are the same strings, typed as the enum.\n */\nexport const MediaType = {\n UNKNOWN: 'unknown',\n IMAGE: 'image',\n AUDIO: 'audio',\n VIDEO: 'video',\n} as unknown as typeof ExpoMediaType;\n\n/** The fields a query filters and sorts on, as the module's own enum. */\nexport const AssetField = {\n CREATION_TIME: 'creationTime',\n MODIFICATION_TIME: 'modificationTime',\n MEDIA_TYPE: 'mediaType',\n WIDTH: 'width',\n HEIGHT: 'height',\n DURATION: 'duration',\n IS_FAVORITE: 'isFavorite',\n} as unknown as typeof ExpoAssetField;\n\n/** The permission answer without the module: nothing granted, and nothing to ask. */\nconst UNAVAILABLE: PermissionResponse = { status: 'denied', granted: false, canAskAgain: false };\n\n@Service()\nexport class MediaLibrary {\n /** Overridden in a test to save photos to a library that is not there. */\n static readonly SOURCE = new InjectionToken<NativeMediaLibrary | null>(\n 'angular-native.mediaLibrarySource',\n {\n factory: () => expoModule('expo-media-library', () => require('expo-media-library') as Expo),\n },\n );\n\n private readonly native = inject(MediaLibrary.SOURCE);\n private readonly destroyRef = inject(DestroyRef);\n private readonly changed = signal<MediaLibraryAssetsChangeEvent | null>(null);\n private readonly changes = this.changed.asReadonly();\n private watching = false;\n\n /** Full access: reading the library, as well as adding to it. */\n readonly permission: Permission = this.permissionFor(false);\n\n /** Write-only access, which is all `save()` needs and what it asks for. */\n readonly writePermission: Permission = this.permissionFor(true);\n\n /**\n * Asks with the module's own options: write-only, and on Android 13 and later which of\n * `'photo'`, `'video'` and `'audio'` to ask for. The answer also updates {@link permission}.\n */\n async requestPermission(...options: Args<'requestPermissionsAsync'>): Promise<boolean> {\n if (!this.native) return false;\n await this.native.requestPermissionsAsync(...options);\n return this.permission.check();\n }\n\n /**\n * On iOS and Android 14 and later, after the user has granted access to only some photos, shows\n * the system picker for changing which. Does nothing otherwise.\n */\n async presentPermissionsPicker(...mediaTypes: Args<'presentPermissionsPicker'>): Promise<void> {\n await this.native?.presentPermissionsPicker(...mediaTypes);\n }\n\n /**\n * Saves a local image or video file to the library, into an album if one is given. Asks for\n * write-only access first; null if it is refused.\n */\n async save(uri: string, album?: Album): Promise<Asset | null> {\n if (!this.native || !(await this.writePermission.ensure())) return null;\n return this.native.Asset.create(uri, album);\n }\n\n /**\n * The assets a query finds, built on the module's own `Query`: `eq`, `within`, `gt`, `lt`,\n * `orderBy`, `limit`, `offset` and `album`. Every asset without one.\n */\n async assets(build: (query: Query) => Query = (query) => query): Promise<Asset[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exe();\n }\n\n /** The same query, answered with each asset's details at once rather than as handles. */\n async metadata(build: (query: Query) => Query = (query) => query): Promise<AssetMetadata[]> {\n if (!this.native) return [];\n return build(new this.native.Query()).exeForMetadata();\n }\n\n /** The handle for an asset whose id is already known, such as one kept from an earlier query. */\n asset(id: string): Asset | null {\n return this.native ? new this.native.Asset(id) : null;\n }\n\n async deleteAssets(assets: Asset[]): Promise<void> {\n await this.native?.Asset.delete(assets);\n }\n\n async albums(): Promise<Album[]> {\n return (await this.native?.Album.getAll()) ?? [];\n }\n\n /** The album with this title, or null. */\n async album(title: string): Promise<Album | null> {\n return (await this.native?.Album.get(title)) ?? null;\n }\n\n /** A new album holding the assets given, copied into it unless `move` is true. */\n async createAlbum(\n name: string,\n assets: Asset[] | string[],\n move?: boolean,\n ): Promise<Album | null> {\n return (await this.native?.Album.create(name, assets, move)) ?? null;\n }\n\n /** Deletes the albums, and with `deleteAssets` the assets in them too. */\n async deleteAlbums(albums: Album[], deleteAssets?: boolean): Promise<void> {\n await this.native?.Album.delete(albums, deleteAssets);\n }\n\n /**\n * The library's most recent change, from the first call on: on iOS the assets inserted, deleted\n * and updated, on Android an empty event. Call it once access is granted, since listening reads\n * the library. The listener is removed when the app is destroyed.\n */\n watch(): Signal<MediaLibraryAssetsChangeEvent | null> {\n if (this.native && !this.watching) {\n this.watching = true;\n const subscription = this.native.addListener((event) => this.changed.set(event));\n this.destroyRef.onDestroy(() => subscription.remove());\n }\n return this.changes;\n }\n\n private permissionFor(writeOnly: boolean): Permission {\n return Permission.of(\n () => this.native?.getPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n () => this.native?.requestPermissionsAsync(writeOnly) ?? Promise.resolve(UNAVAILABLE),\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A value held on the native side, addressed by id. */
|
|
2
|
+
export interface NativeState<T> {
|
|
3
|
+
/** What the view prop takes. `undefined` if the object has already been released. */
|
|
4
|
+
readonly id: number | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* The current value.
|
|
7
|
+
*
|
|
8
|
+
* A write from JavaScript is scheduled onto the UI thread, so it is not readable back until
|
|
9
|
+
* that has run. `@expo/ui` says the same of its own accessors.
|
|
10
|
+
*/
|
|
11
|
+
get(): T;
|
|
12
|
+
set(value: T): void;
|
|
13
|
+
/**
|
|
14
|
+
* Detach from the native object. Nothing else may use the state afterwards.
|
|
15
|
+
*
|
|
16
|
+
* Worth calling from `DestroyRef` for a field inside a list that comes and goes; a state that
|
|
17
|
+
* lives as long as the app does not need it.
|
|
18
|
+
*/
|
|
19
|
+
release(): void;
|
|
20
|
+
}
|
|
21
|
+
/** Somewhere to hold a text field's text. See the file comment. */
|
|
22
|
+
export declare function nativeState<T>(initial: T): NativeState<T> | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ObservableState`: the one `@expo/ui` prop that is not a value.
|
|
3
|
+
*
|
|
4
|
+
* `TextFieldView` and `SecureFieldView` declare `text` as an `ObservableState` rather than a
|
|
5
|
+
* `String`, and what travels over the prop is the shared object's id, a number. Binding the string
|
|
6
|
+
* itself is the obvious thing to write and it fails as
|
|
7
|
+
* `FieldInvalidTypeException`, logged rather than thrown, so the field renders and simply ignores
|
|
8
|
+
* everything the app sets. Both fields also manage their own text when the prop is absent, which
|
|
9
|
+
* is why the mistake looks like it works: typing behaves, and only setting the value from Angular
|
|
10
|
+
* does nothing.
|
|
11
|
+
*
|
|
12
|
+
* The state lives on the native side and both sides hold a reference, so it survives the commit
|
|
13
|
+
* that a signal would not: writing to it moves the caret in a field that is already on screen,
|
|
14
|
+
* where a re-render would not. That is the whole reason the prop is shaped this way.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* protected readonly name = nativeState('');
|
|
18
|
+
* ```
|
|
19
|
+
* ```html
|
|
20
|
+
* <ui-text-field [text]="name?.id" (textChange)="typed.set($event.nativeEvent.value)" />
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* `null` off a device, where there is no native module to hold the state. Bind `name?.id` and the
|
|
24
|
+
* prop is simply absent, which is the field's own unmanaged behaviour rather than a crash.
|
|
25
|
+
*/
|
|
26
|
+
import { optional } from "./native.js";
|
|
27
|
+
/** Somewhere to hold a text field's text. See the file comment. */
|
|
28
|
+
export function nativeState(initial) {
|
|
29
|
+
const raw = optional(() => {
|
|
30
|
+
const { requireNativeModule } = require('expo');
|
|
31
|
+
const module = requireNativeModule('ExpoUI');
|
|
32
|
+
return new module.ObservableState({ value: initial });
|
|
33
|
+
});
|
|
34
|
+
if (!raw)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
id: raw.__expo_shared_object_id__,
|
|
38
|
+
get: () => raw.getValue(),
|
|
39
|
+
set: (value) => raw.setValue({ value }),
|
|
40
|
+
release: () => raw.release(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=native-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-state.js","sourceRoot":"","sources":["../src/native-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AA+BvC,mEAAmE;AACnE,MAAM,UAAU,WAAW,CAAI,OAAU;IACvC,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE;QACxB,MAAM,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAC,MAAM,CAA0B,CAAC;QACzE,MAAM,MAAM,GAAG,mBAAmB,CAAC,QAAQ,CAE1C,CAAC;QACF,OAAO,IAAI,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAEtB,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,yBAAyB;QACjC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAO;QAC9B,GAAG,EAAE,CAAC,KAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;QAC1C,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE;KAC7B,CAAC;AACJ,CAAC","sourcesContent":["/**\n * `ObservableState`: the one `@expo/ui` prop that is not a value.\n *\n * `TextFieldView` and `SecureFieldView` declare `text` as an `ObservableState` rather than a\n * `String`, and what travels over the prop is the shared object's id, a number. Binding the string\n * itself is the obvious thing to write and it fails as\n * `FieldInvalidTypeException`, logged rather than thrown, so the field renders and simply ignores\n * everything the app sets. Both fields also manage their own text when the prop is absent, which\n * is why the mistake looks like it works: typing behaves, and only setting the value from Angular\n * does nothing.\n *\n * The state lives on the native side and both sides hold a reference, so it survives the commit\n * that a signal would not: writing to it moves the caret in a field that is already on screen,\n * where a re-render would not. That is the whole reason the prop is shaped this way.\n *\n * ```ts\n * protected readonly name = nativeState('');\n * ```\n * ```html\n * <ui-text-field [text]=\"name?.id\" (textChange)=\"typed.set($event.nativeEvent.value)\" />\n * ```\n *\n * `null` off a device, where there is no native module to hold the state. Bind `name?.id` and the\n * prop is simply absent, which is the field's own unmanaged behaviour rather than a crash.\n */\nimport { optional } from './native.ts';\n\n/** What `useNativeState` builds by hand, since native exposes getters rather than a property. */\ninterface RawState {\n readonly __expo_shared_object_id__?: number;\n getValue(): unknown;\n setValue(value: { value: unknown }): void;\n release(): void;\n}\n\n/** A value held on the native side, addressed by id. */\nexport interface NativeState<T> {\n /** What the view prop takes. `undefined` if the object has already been released. */\n readonly id: number | undefined;\n /**\n * The current value.\n *\n * A write from JavaScript is scheduled onto the UI thread, so it is not readable back until\n * that has run. `@expo/ui` says the same of its own accessors.\n */\n get(): T;\n set(value: T): void;\n /**\n * Detach from the native object. Nothing else may use the state afterwards.\n *\n * Worth calling from `DestroyRef` for a field inside a list that comes and goes; a state that\n * lives as long as the app does not need it.\n */\n release(): void;\n}\n\n/** Somewhere to hold a text field's text. See the file comment. */\nexport function nativeState<T>(initial: T): NativeState<T> | null {\n const raw = optional(() => {\n const { requireNativeModule } = require('expo') as typeof import('expo');\n const module = requireNativeModule('ExpoUI') as {\n ObservableState: new (init: { value: unknown }) => RawState;\n };\n return new module.ObservableState({ value: initial });\n });\n if (!raw) return null;\n\n return {\n id: raw.__expo_shared_object_id__,\n get: () => raw.getValue() as T,\n set: (value: T) => raw.setValue({ value }),\n release: () => raw.release(),\n };\n}\n"]}
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reaching an Expo module without importing one.
|
|
3
|
+
*
|
|
4
|
+
* A static `import ... from 'expo-battery'` makes the file unloadable by Node, which is what used
|
|
5
|
+
* to force every service into two files: the behaviour somewhere the test suite could reach, and
|
|
6
|
+
* the binding somewhere it could not. A `require` inside a function has neither problem. Metro
|
|
7
|
+
* still resolves the string literal at build time, so the module is bundled exactly as before and
|
|
8
|
+
* an app that has not installed it still gets a build error rather than a surprise at runtime;
|
|
9
|
+
* Node never evaluates the call at all, because nothing in a test asks for the real platform.
|
|
10
|
+
*
|
|
11
|
+
* The `catch` is doing two jobs, and only one of them is Node. In an ESM test `require` is not
|
|
12
|
+
* defined, so calling it throws `ReferenceError` and the service goes inert - the same state as
|
|
13
|
+
* a device that has no such sensor. On a device the throw is a module that is present but not
|
|
14
|
+
* linked, which is the same answer for the same reason.
|
|
15
|
+
*/
|
|
16
|
+
export declare function optional<T>(load: () => T): T | null;
|
|
17
|
+
/** The platforms an Expo module is written for. */
|
|
18
|
+
export type ModulePlatform = 'ios' | 'android' | 'web';
|
|
19
|
+
/**
|
|
20
|
+
* A module the app needs and does not have: the package is not installed, or it is and the app was
|
|
21
|
+
* not rebuilt since, so its native half is not in the binary. Development builds and Expo Go carry
|
|
22
|
+
* only the native modules they were built with.
|
|
23
|
+
*/
|
|
24
|
+
export declare class MissingModuleError extends Error {
|
|
25
|
+
readonly name = "MissingModuleError";
|
|
26
|
+
/** The package, as it is installed: `expo-haptics`. */
|
|
27
|
+
readonly module: string;
|
|
28
|
+
constructor(module: string, platform: ModulePlatform, cause?: unknown);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The Expo module a service needs.
|
|
32
|
+
*
|
|
33
|
+
* Where the module exists for the platform the app is on, a module that is missing is a mistake
|
|
34
|
+
* the developer can fix, so this throws a `MissingModuleError` that says how - rather than leave a
|
|
35
|
+
* feature that silently does nothing. Anywhere it cannot exist - an iOS-only module on Android, the
|
|
36
|
+
* web, and Node, where a test runs with no platform at all - this answers null, and the service
|
|
37
|
+
* goes inert, as code shared across platforms needs it to.
|
|
38
|
+
*
|
|
39
|
+
* `load` answering null or undefined counts as missing too: `requireOptionalNativeModule` answers
|
|
40
|
+
* null rather than throwing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function expoModule<T>(module: string, load: () => T | null | undefined, platforms?: readonly ModulePlatform[]): T | null;
|