@ng-native/expo 0.9.0 → 0.10.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.
@@ -0,0 +1,20 @@
1
+ plugins {
2
+ id 'com.android.library'
3
+ id 'expo-module-gradle-plugin'
4
+ }
5
+
6
+ group = 'dev.ngnative.expo'
7
+ version = '1.0.0'
8
+
9
+ android {
10
+ namespace "dev.ngnative.expo"
11
+ defaultConfig {
12
+ versionCode 1
13
+ versionName '1.0.0'
14
+ }
15
+ }
16
+
17
+ dependencies {
18
+ // The first version with ProgressStyle and promoted ongoing notifications in NotificationCompat.
19
+ implementation 'androidx.core:core:1.17.0'
20
+ }
@@ -0,0 +1,10 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
3
+ <uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />
4
+
5
+ <application>
6
+ <receiver
7
+ android:name=".OngoingNotificationReceiver"
8
+ android:exported="false" />
9
+ </application>
10
+ </manifest>
@@ -0,0 +1,207 @@
1
+ package dev.ngnative.expo
2
+
3
+ import android.Manifest
4
+ import android.app.Notification
5
+ import android.app.NotificationChannel
6
+ import android.app.NotificationManager
7
+ import android.app.PendingIntent
8
+ import android.content.Context
9
+ import android.content.Intent
10
+ import android.content.pm.PackageManager
11
+ import android.net.Uri
12
+ import android.os.Build
13
+ import android.provider.Settings
14
+ import androidx.core.app.NotificationCompat
15
+ import androidx.core.app.NotificationManagerCompat
16
+ import expo.modules.interfaces.permissions.PermissionsStatus
17
+ import expo.modules.kotlin.Promise
18
+ import expo.modules.kotlin.exception.CodedException
19
+ import expo.modules.kotlin.modules.Module
20
+ import expo.modules.kotlin.modules.ModuleDefinition
21
+
22
+ /**
23
+ * An ongoing notification drawn from data: a title and text, time Android counts by itself, a
24
+ * progress bar, and actions. It asks to be promoted to a Live Update, which Android 16 grants to a
25
+ * notification of a standard style with a title, and the user can turn off.
26
+ */
27
+ class OngoingNotificationModule : Module() {
28
+ private val context: Context
29
+ get() = appContext.reactContext ?: throw CodedException("There is no Android context yet.")
30
+
31
+ private val manager: NotificationManager
32
+ get() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
33
+
34
+ override fun definition() = ModuleDefinition {
35
+ Name("NgNativeOngoingNotification")
36
+
37
+ Events("onChange")
38
+
39
+ OnStartObserving {
40
+ OngoingNotificationReceiver.onChange = { sendEvent("onChange", emptyMap()) }
41
+ }
42
+
43
+ OnStopObserving {
44
+ OngoingNotificationReceiver.onChange = null
45
+ }
46
+
47
+ AsyncFunction("show") { id: String, content: Map<String, Any?>, options: Map<String, Any?>, promise: Promise ->
48
+ whenAllowed(promise) {
49
+ createChannel(options["channel"] as Map<*, *>)
50
+ manager.notify(id, NOTIFICATION, build(id, content, options))
51
+ promise.resolve(mapOf("promoted" to isPromoted(id)))
52
+ }
53
+ }
54
+
55
+ Function("cancel") { id: String ->
56
+ manager.cancel(id, NOTIFICATION)
57
+ }
58
+
59
+ Function("isActive") { id: String ->
60
+ active(id) != null
61
+ }
62
+
63
+ Function("takeTaps") { id: String ->
64
+ OngoingNotificationReceiver.take(context, id)
65
+ }
66
+
67
+ Function("openPromotionSettings") {
68
+ val action =
69
+ // Settings.ACTION_MANAGE_APP_PROMOTED_NOTIFICATIONS, which the SDK this builds with does not name.
70
+ if (Build.VERSION.SDK_INT >= 36) "android.settings.MANAGE_APP_PROMOTED_NOTIFICATIONS"
71
+ else Settings.ACTION_APP_NOTIFICATION_SETTINGS
72
+ context.startActivity(
73
+ Intent(action)
74
+ .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
75
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
76
+ )
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Runs `show` once notifications are allowed, asking first where Android 13 and later need it.
82
+ * Only when it is not granted yet: asking goes through the activity, and answers denied when
83
+ * there is none, as when an action is tapped with the app closed to the background.
84
+ */
85
+ private fun whenAllowed(promise: Promise, show: () -> Unit) {
86
+ val guarded = {
87
+ try {
88
+ if (NotificationManagerCompat.from(context).areNotificationsEnabled()) show()
89
+ else promise.reject(CodedException("Notifications are turned off for this app."))
90
+ } catch (failure: Throwable) {
91
+ promise.reject(CodedException(failure.message ?: "The notification could not be shown.", failure))
92
+ }
93
+ }
94
+ val permissions = appContext.permissions
95
+ if (
96
+ Build.VERSION.SDK_INT < 33 ||
97
+ permissions == null ||
98
+ permissions.hasGrantedPermissions(Manifest.permission.POST_NOTIFICATIONS)
99
+ ) {
100
+ return guarded()
101
+ }
102
+ permissions.askForPermissions(
103
+ { result ->
104
+ if (result[Manifest.permission.POST_NOTIFICATIONS]?.status == PermissionsStatus.GRANTED) guarded()
105
+ else promise.reject(CodedException("The notification permission was not granted."))
106
+ },
107
+ Manifest.permission.POST_NOTIFICATIONS
108
+ )
109
+ }
110
+
111
+ private fun createChannel(channel: Map<*, *>) {
112
+ if (Build.VERSION.SDK_INT < 26) return
113
+ val id = channel["id"] as String
114
+ if (manager.getNotificationChannel(id) != null) return
115
+ // Low: it is shown without a sound, and a Live Update needs more than the minimum.
116
+ manager.createNotificationChannel(
117
+ NotificationChannel(id, channel["name"] as String, NotificationManager.IMPORTANCE_LOW)
118
+ )
119
+ }
120
+
121
+ private fun build(id: String, content: Map<String, Any?>, options: Map<String, Any?>): Notification {
122
+ val channel = (options["channel"] as Map<*, *>)["id"] as String
123
+ val builder = NotificationCompat.Builder(context, channel)
124
+ .setSmallIcon(smallIcon())
125
+ .setContentTitle(content["title"] as String)
126
+ .setContentText(content["text"] as String?)
127
+ .setOngoing(true)
128
+ .setOnlyAlertOnce(true)
129
+ .setRequestPromotedOngoing(true)
130
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
131
+ .setShowWhen(false)
132
+ .setContentIntent(open(id, options["url"] as String?))
133
+ .setDeleteIntent(broadcast(id, null))
134
+
135
+ (content["chip"] as String?)?.let { builder.setShortCriticalText(it) }
136
+
137
+ (content["timer"] as Map<*, *>?)?.let { timer ->
138
+ val until = timer["until"] as Number?
139
+ builder
140
+ .setWhen(((until ?: timer["since"]) as Number).toLong())
141
+ .setShowWhen(true)
142
+ .setUsesChronometer(true)
143
+ .setChronometerCountDown(until != null)
144
+ }
145
+
146
+ // ponytail: the plain bar every version draws; ProgressStyle's segments and points when an app asks.
147
+ (content["progress"] as Map<*, *>?)?.let { progress ->
148
+ val indeterminate = progress["indeterminate"] == true
149
+ builder.setProgress(
150
+ (progress["max"] as Number?)?.toInt() ?: 100,
151
+ (progress["value"] as Number?)?.toInt() ?: 0,
152
+ indeterminate
153
+ )
154
+ }
155
+
156
+ (content["actions"] as List<*>?)?.forEach { action ->
157
+ action as Map<*, *>
158
+ val target = action["target"] as String
159
+ val url = action["url"] as String?
160
+ builder.addAction(
161
+ 0,
162
+ action["title"] as String,
163
+ if (url != null) open("$id $target", url) else broadcast(id, target)
164
+ )
165
+ }
166
+ return builder.build()
167
+ }
168
+
169
+ /** The app's notification icon, as `expo-notifications` names it, or the app's own icon. */
170
+ private fun smallIcon(): Int {
171
+ val info = context.packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
172
+ return info.metaData?.getInt("expo.modules.notifications.default_notification_icon", 0)
173
+ ?.takeIf { it != 0 } ?: info.icon
174
+ }
175
+
176
+ /** Opens the app, at `url` when there is one. */
177
+ private fun open(key: String, url: String?): PendingIntent? {
178
+ val intent =
179
+ if (url != null) Intent(Intent.ACTION_VIEW, Uri.parse(url)).setPackage(context.packageName)
180
+ else context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return null
181
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
182
+ return PendingIntent.getActivity(context, key.hashCode(), intent, FLAGS)
183
+ }
184
+
185
+ /** Tells the receiver of a tap on `target`, or with none, that the notification was swiped away. */
186
+ private fun broadcast(id: String, target: String?): PendingIntent {
187
+ val intent = Intent(context, OngoingNotificationReceiver::class.java)
188
+ .setData(Uri.fromParts("ongoing", id, target))
189
+ .putExtra(OngoingNotificationReceiver.EXTRA_ID, id)
190
+ .putExtra(OngoingNotificationReceiver.EXTRA_TARGET, target)
191
+ return PendingIntent.getBroadcast(context, 0, intent, FLAGS)
192
+ }
193
+
194
+ private fun active(id: String) =
195
+ manager.activeNotifications.firstOrNull { it.tag == id && it.id == NOTIFICATION }
196
+
197
+ private fun isPromoted(id: String): Boolean =
198
+ Build.VERSION.SDK_INT >= 36 &&
199
+ manager.canPostPromotedNotifications() &&
200
+ active(id)?.notification?.hasPromotableCharacteristics() == true
201
+
202
+ companion object {
203
+ /** Every notification shares this number and is told apart by its tag, the id JavaScript gives. */
204
+ private const val NOTIFICATION = 0x6e67
205
+ private const val FLAGS = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
206
+ }
207
+ }
@@ -0,0 +1,46 @@
1
+ package dev.ngnative.expo
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import org.json.JSONArray
7
+
8
+ /**
9
+ * Receives a tap on one of an ongoing notification's actions, and the notification being swiped
10
+ * away. Android starts the app's process for it when there is none, so the tap is stored, and
11
+ * handed to JavaScript when it asks: nothing here needs the app's JavaScript to be running.
12
+ */
13
+ class OngoingNotificationReceiver : BroadcastReceiver() {
14
+ override fun onReceive(context: Context, intent: Intent) {
15
+ val id = intent.getStringExtra(EXTRA_ID) ?: return
16
+ intent.getStringExtra(EXTRA_TARGET)?.let { store(context, id, it) }
17
+ onChange?.invoke()
18
+ }
19
+
20
+ companion object {
21
+ const val EXTRA_ID = "dev.ngnative.expo.ongoing.id"
22
+ const val EXTRA_TARGET = "dev.ngnative.expo.ongoing.target"
23
+ private const val PREFERENCES = "dev.ngnative.expo.ongoing.taps"
24
+
25
+ /** Set while the module is listening, to tell JavaScript there is something to take. */
26
+ @Volatile
27
+ var onChange: (() -> Unit)? = null
28
+
29
+ private fun preferences(context: Context) =
30
+ context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
31
+
32
+ @Synchronized
33
+ private fun store(context: Context, id: String, target: String) {
34
+ val taps = JSONArray(preferences(context).getString(id, "[]")).put(target)
35
+ preferences(context).edit().putString(id, taps.toString()).commit()
36
+ }
37
+
38
+ /** The targets tapped on `id` and not yet taken, oldest first, which are then taken. */
39
+ @Synchronized
40
+ fun take(context: Context, id: String): List<String> {
41
+ val taps = JSONArray(preferences(context).getString(id, "[]"))
42
+ preferences(context).edit().remove(id).commit()
43
+ return List(taps.length()) { taps.getString(it) }
44
+ }
45
+ }
46
+ }
@@ -114,8 +114,8 @@ export declare class UiButton {
114
114
  readonly systemImage: import("@angular/core").InputSignal<string | undefined>;
115
115
  readonly role: import("@angular/core").InputSignal<"default" | "cancel" | "destructive" | "close" | undefined>;
116
116
  /**
117
- * In a home-screen widget's layout, what a tap on it records for the app: see
118
- * `@ng-native/expo/widget`.
117
+ * In a home-screen widget's or a Live Activity's layout, what a tap on it hands the app: see
118
+ * `@ng-native/expo/widget` and `@ng-native/expo/live-activity`.
119
119
  */
120
120
  readonly target: import("@angular/core").InputSignal<string | undefined>;
121
121
  readonly modifiers: import("@angular/core").InputSignal<readonly UiModifier[] | undefined>;
@@ -207,27 +207,51 @@ export declare class UiVStack {
207
207
  static ɵfac: i0.ɵɵFactoryDeclaration<UiVStack, never>;
208
208
  static ɵcmp: i0.ɵɵComponentDeclaration<UiVStack, "ui-vstack", never, { "alignment": { "alias": "alignment"; "required": false; "isSignal": true; }; "spacing": { "alias": "spacing"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
209
209
  }
210
- /** A SwiftUI `Image`: an SF Symbol by `systemName`, or a picture by `uiImage` URL. */
210
+ /**
211
+ * A SwiftUI `Image`: an SF Symbol by `systemName`, a symbol or picture in the asset catalog by
212
+ * `assetName`, or a picture by `uiImage`, the URL of a file on the device.
213
+ */
211
214
  export declare class UiImage {
212
215
  protected readonly nativeView: true;
213
216
  readonly systemName: import("@angular/core").InputSignal<string | undefined>;
217
+ readonly assetName: import("@angular/core").InputSignal<string | undefined>;
214
218
  readonly uiImage: import("@angular/core").InputSignal<string | undefined>;
215
219
  readonly size: import("@angular/core").InputSignalWithTransform<number | undefined, unknown>;
216
220
  readonly color: import("@angular/core").InputSignal<string | undefined>;
217
221
  readonly modifiers: import("@angular/core").InputSignal<readonly UiModifier[] | undefined>;
218
222
  static ɵfac: i0.ɵɵFactoryDeclaration<UiImage, never>;
219
- static ɵcmp: i0.ɵɵComponentDeclaration<UiImage, "ui-image", never, { "systemName": { "alias": "systemName"; "required": false; "isSignal": true; }; "uiImage": { "alias": "uiImage"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
223
+ static ɵcmp: i0.ɵɵComponentDeclaration<UiImage, "ui-image", never, { "systemName": { "alias": "systemName"; "required": false; "isSignal": true; }; "assetName": { "alias": "assetName"; "required": false; "isSignal": true; }; "uiImage": { "alias": "uiImage"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
224
+ }
225
+ /** A moment: a `Date`, milliseconds since the epoch, or a string `Date` reads. */
226
+ export type UiDate = Date | number | string;
227
+ /** The start and end of a timer. */
228
+ export interface UiTimerInterval {
229
+ readonly lower: UiDate;
230
+ readonly upper: UiDate;
220
231
  }
221
232
  /**
222
233
  * A SwiftUI `Text`, or Compose's on Android. Its text is what is written inside it,
223
234
  * `<ui-text>Us {{ score }}</ui-text>`, as `<text>` takes its own, or `text`, which wins over it.
235
+ *
236
+ * On iOS it can keep time by itself instead, with no update from the app: `date` is drawn by
237
+ * `dateStyle`, as the time since or until it for `'relative'`, `'offset'` and `'timer'`, and
238
+ * `timerInterval` is a timer running from its `lower` to its `upper`, down unless `countsDown` is
239
+ * false.
224
240
  */
225
241
  export declare class UiText {
226
242
  protected readonly nativeView: true;
243
+ protected readonly android: boolean;
227
244
  readonly text: import("@angular/core").InputSignal<string | undefined>;
245
+ readonly date: import("@angular/core").InputSignalWithTransform<number | undefined, UiDate | null | undefined>;
246
+ readonly dateStyle: import("@angular/core").InputSignal<"date" | "time" | "timer" | "relative" | "offset" | undefined>;
247
+ readonly timerInterval: import("@angular/core").InputSignalWithTransform<{
248
+ lower: number;
249
+ upper: number;
250
+ } | undefined, UiTimerInterval | null | undefined>;
251
+ readonly countsDown: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
228
252
  readonly modifiers: import("@angular/core").InputSignal<readonly UiModifier[] | undefined>;
229
253
  static ɵfac: i0.ɵɵFactoryDeclaration<UiText, never>;
230
- static ɵcmp: i0.ɵɵComponentDeclaration<UiText, "ui-text", never, { "text": { "alias": "text"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
254
+ static ɵcmp: i0.ɵɵComponentDeclaration<UiText, "ui-text", never, { "text": { "alias": "text"; "required": false; "isSignal": true; }; "date": { "alias": "date"; "required": false; "isSignal": true; }; "dateStyle": { "alias": "dateStyle"; "required": false; "isSignal": true; }; "timerInterval": { "alias": "timerInterval"; "required": false; "isSignal": true; }; "countsDown": { "alias": "countsDown"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
231
255
  }
232
256
  /**
233
257
  * What `ui-date-picker` sends when the user picks: the chosen moment, which SwiftUI reports in
@@ -478,14 +502,21 @@ export declare class UiChart {
478
502
  /**
479
503
  * A SwiftUI `ProgressView`: a bar filled to `value`, from 0 to 1, or a spinner without one. On
480
504
  * Android, Compose's `LinearProgressIndicator`, a moving bar rather than a spinner without a value.
505
+ * On iOS, `timerInterval` has the bar keep time by itself from its `lower` to its `upper`, emptying
506
+ * unless `countsDown` is false.
481
507
  */
482
508
  export declare class UiProgress {
483
509
  protected readonly nativeView: true;
484
510
  protected readonly android: boolean;
485
511
  readonly value: import("@angular/core").InputSignalWithTransform<number | undefined, unknown>;
512
+ readonly timerInterval: import("@angular/core").InputSignalWithTransform<{
513
+ lower: number;
514
+ upper: number;
515
+ } | undefined, UiTimerInterval | null | undefined>;
516
+ readonly countsDown: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
486
517
  readonly modifiers: import("@angular/core").InputSignal<readonly UiModifier[] | undefined>;
487
518
  static ɵfac: i0.ɵɵFactoryDeclaration<UiProgress, never>;
488
- static ɵcmp: i0.ɵɵComponentDeclaration<UiProgress, "ui-progress", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
519
+ static ɵcmp: i0.ɵɵComponentDeclaration<UiProgress, "ui-progress", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "timerInterval": { "alias": "timerInterval"; "required": false; "isSignal": true; }; "countsDown": { "alias": "countsDown"; "required": false; "isSignal": true; }; "modifiers": { "alias": "modifiers"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
489
520
  }
490
521
  /**
491
522
  * A SwiftUI `HStack`, or Compose's `Row` on Android, centred, as SwiftUI's is, unless `alignment`
@@ -182,8 +182,8 @@ export class UiButton {
182
182
  role = input(/* @ts-ignore */
183
183
  ...(ngDevMode ? [undefined, { debugName: "role" }] : /* istanbul ignore next */ []));
184
184
  /**
185
- * In a home-screen widget's layout, what a tap on it records for the app: see
186
- * `@ng-native/expo/widget`.
185
+ * In a home-screen widget's or a Live Activity's layout, what a tap on it hands the app: see
186
+ * `@ng-native/expo/widget` and `@ng-native/expo/live-activity`.
187
187
  */
188
188
  target = input(/* @ts-ignore */
189
189
  ...(ngDevMode ? [undefined, { debugName: "target" }] : /* istanbul ignore next */ []));
@@ -194,7 +194,7 @@ export class UiButton {
194
194
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.2.0", type: UiButton, isStandalone: true, selector: "ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, systemImage: { classPropertyName: "systemImage", publicName: "systemImage", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, modifiers: { classPropertyName: "modifiers", publicName: "modifiers", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { buttonPress: "buttonPress" }, host: { listeners: { "buttonPressed": "android && buttonPress.emit($event)" }, properties: { "label": "label()", "systemImage": "systemImage()", "role": "role()", "target": "target()", "modifiers": "modifiers()" } }, ngImport: i0, template: `@if (android && label()) {
195
195
  <ui-text [text]="label()" />
196
196
  }
197
- <ng-content />`, isInline: true, dependencies: [{ kind: "component", type: i0.forwardRef(() => UiText), selector: "ui-text", inputs: ["text", "modifiers"] }] });
197
+ <ng-content />`, isInline: true, dependencies: [{ kind: "component", type: i0.forwardRef(() => UiText), selector: "ui-text", inputs: ["text", "date", "dateStyle", "timerInterval", "countsDown", "modifiers"] }] });
198
198
  }
199
199
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiButton, decorators: [{
200
200
  type: Component,
@@ -385,11 +385,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
385
385
  },
386
386
  }]
387
387
  }], propDecorators: { alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], spacing: [{ type: i0.Input, args: [{ isSignal: true, alias: "spacing", required: false }] }], modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
388
- /** A SwiftUI `Image`: an SF Symbol by `systemName`, or a picture by `uiImage` URL. */
388
+ /**
389
+ * A SwiftUI `Image`: an SF Symbol by `systemName`, a symbol or picture in the asset catalog by
390
+ * `assetName`, or a picture by `uiImage`, the URL of a file on the device.
391
+ */
389
392
  export class UiImage {
390
393
  nativeView = nativeView();
391
394
  systemName = input(/* @ts-ignore */
392
395
  ...(ngDevMode ? [undefined, { debugName: "systemName" }] : /* istanbul ignore next */ []));
396
+ assetName = input(/* @ts-ignore */
397
+ ...(ngDevMode ? [undefined, { debugName: "assetName" }] : /* istanbul ignore next */ []));
393
398
  uiImage = input(/* @ts-ignore */
394
399
  ...(ngDevMode ? [undefined, { debugName: "uiImage" }] : /* istanbul ignore next */ []));
395
400
  size = input(undefined, { ...(ngDevMode ? { debugName: "size" } : /* istanbul ignore next */ {}), transform: optionalNumber });
@@ -398,7 +403,7 @@ export class UiImage {
398
403
  modifiers = input(/* @ts-ignore */
399
404
  ...(ngDevMode ? [undefined, { debugName: "modifiers" }] : /* istanbul ignore next */ []));
400
405
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiImage, deps: [], target: i0.ɵɵFactoryTarget.Component });
401
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: UiImage, isStandalone: true, selector: "ui-image", inputs: { systemName: { classPropertyName: "systemName", publicName: "systemName", isSignal: true, isRequired: false, transformFunction: null }, uiImage: { classPropertyName: "uiImage", publicName: "uiImage", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, modifiers: { classPropertyName: "modifiers", publicName: "modifiers", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "systemName": "systemName()", "uiImage": "uiImage()", "size": "size()", "color": "color()", "modifiers": "modifiers()" } }, ngImport: i0, template: '', isInline: true });
406
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: UiImage, isStandalone: true, selector: "ui-image", inputs: { systemName: { classPropertyName: "systemName", publicName: "systemName", isSignal: true, isRequired: false, transformFunction: null }, assetName: { classPropertyName: "assetName", publicName: "assetName", isSignal: true, isRequired: false, transformFunction: null }, uiImage: { classPropertyName: "uiImage", publicName: "uiImage", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, modifiers: { classPropertyName: "modifiers", publicName: "modifiers", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "systemName": "systemName()", "assetName": "assetName()", "uiImage": "uiImage()", "size": "size()", "color": "color()", "modifiers": "modifiers()" } }, ngImport: i0, template: '', isInline: true });
402
407
  }
403
408
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiImage, decorators: [{
404
409
  type: Component,
@@ -407,34 +412,64 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
407
412
  template: '',
408
413
  host: {
409
414
  '[systemName]': 'systemName()',
415
+ '[assetName]': 'assetName()',
410
416
  '[uiImage]': 'uiImage()',
411
417
  '[size]': 'size()',
412
418
  '[color]': 'color()',
413
419
  '[modifiers]': 'modifiers()',
414
420
  },
415
421
  }]
416
- }], propDecorators: { systemName: [{ type: i0.Input, args: [{ isSignal: true, alias: "systemName", required: false }] }], uiImage: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiImage", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
422
+ }], propDecorators: { systemName: [{ type: i0.Input, args: [{ isSignal: true, alias: "systemName", required: false }] }], assetName: [{ type: i0.Input, args: [{ isSignal: true, alias: "assetName", required: false }] }], uiImage: [{ type: i0.Input, args: [{ isSignal: true, alias: "uiImage", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
423
+ /** A moment as SwiftUI takes it, in milliseconds since the epoch, or none for what is not one. */
424
+ function milliseconds(value) {
425
+ const time = value == null ? NaN : new Date(value).getTime();
426
+ return Number.isNaN(time) ? undefined : time;
427
+ }
428
+ /** No timer unless both ends are moments, as the extension has none. */
429
+ function timerMilliseconds(value) {
430
+ const lower = milliseconds(value?.lower);
431
+ const upper = milliseconds(value?.upper);
432
+ return lower === undefined || upper === undefined ? undefined : { lower, upper };
433
+ }
417
434
  /**
418
435
  * A SwiftUI `Text`, or Compose's on Android. Its text is what is written inside it,
419
436
  * `<ui-text>Us {{ score }}</ui-text>`, as `<text>` takes its own, or `text`, which wins over it.
437
+ *
438
+ * On iOS it can keep time by itself instead, with no update from the app: `date` is drawn by
439
+ * `dateStyle`, as the time since or until it for `'relative'`, `'offset'` and `'timer'`, and
440
+ * `timerInterval` is a timer running from its `lower` to its `upper`, down unless `countsDown` is
441
+ * false.
420
442
  */
421
443
  export class UiText {
422
444
  nativeView = nativeView();
445
+ android = nativePlatform() === 'android';
423
446
  text = input(/* @ts-ignore */
424
447
  ...(ngDevMode ? [undefined, { debugName: "text" }] : /* istanbul ignore next */ []));
448
+ date = input(undefined, { ...(ngDevMode ? { debugName: "date" } : /* istanbul ignore next */ {}), transform: milliseconds });
449
+ dateStyle = input(/* @ts-ignore */
450
+ ...(ngDevMode ? [undefined, { debugName: "dateStyle" }] : /* istanbul ignore next */ []));
451
+ timerInterval = input(undefined, { ...(ngDevMode ? { debugName: "timerInterval" } : /* istanbul ignore next */ {}), transform: timerMilliseconds });
452
+ countsDown = input(undefined, { ...(ngDevMode ? { debugName: "countsDown" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
425
453
  modifiers = input(/* @ts-ignore */
426
454
  ...(ngDevMode ? [undefined, { debugName: "modifiers" }] : /* istanbul ignore next */ []));
427
455
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiText, deps: [], target: i0.ɵɵFactoryTarget.Component });
428
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: UiText, isStandalone: true, selector: "ui-text", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, modifiers: { classPropertyName: "modifiers", publicName: "modifiers", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "text": "text()", "modifiers": "modifiers()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
456
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: UiText, isStandalone: true, selector: "ui-text", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, date: { classPropertyName: "date", publicName: "date", isSignal: true, isRequired: false, transformFunction: null }, dateStyle: { classPropertyName: "dateStyle", publicName: "dateStyle", isSignal: true, isRequired: false, transformFunction: null }, timerInterval: { classPropertyName: "timerInterval", publicName: "timerInterval", isSignal: true, isRequired: false, transformFunction: null }, countsDown: { classPropertyName: "countsDown", publicName: "countsDown", isSignal: true, isRequired: false, transformFunction: null }, modifiers: { classPropertyName: "modifiers", publicName: "modifiers", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "text": "text()", "date": "android ? undefined : date()", "dateStyle": "android ? undefined : dateStyle()", "timerInterval": "android ? undefined : timerInterval()", "countsDown": "android ? undefined : countsDown()", "modifiers": "modifiers()" } }, ngImport: i0, template: '<ng-content />', isInline: true });
429
457
  }
430
458
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiText, decorators: [{
431
459
  type: Component,
432
460
  args: [{
433
461
  selector: 'ui-text',
434
462
  template: '<ng-content />',
435
- host: { '[text]': 'text()', '[modifiers]': 'modifiers()' },
463
+ host: {
464
+ '[text]': 'text()',
465
+ '[date]': 'android ? undefined : date()',
466
+ '[dateStyle]': 'android ? undefined : dateStyle()',
467
+ '[timerInterval]': 'android ? undefined : timerInterval()',
468
+ '[countsDown]': 'android ? undefined : countsDown()',
469
+ '[modifiers]': 'modifiers()',
470
+ },
436
471
  }]
437
- }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
472
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], date: [{ type: i0.Input, args: [{ isSignal: true, alias: "date", required: false }] }], dateStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateStyle", required: false }] }], timerInterval: [{ type: i0.Input, args: [{ isSignal: true, alias: "timerInterval", required: false }] }], countsDown: [{ type: i0.Input, args: [{ isSignal: true, alias: "countsDown", required: false }] }], modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
438
473
  /**
439
474
  * Listen for one of a `ui-*` view's own events from inside its component. Not a host listener:
440
475
  * one named as an output listens to the output itself, and see the note at the top of this file
@@ -603,7 +638,7 @@ export class UiPicker {
603
638
  </ui-slot>
604
639
  }
605
640
  <ng-content />
606
- `, isInline: true, dependencies: [{ kind: "component", type: UiSlot, selector: "ui-slot", inputs: ["name", "extraProps"] }, { kind: "component", type: UiText, selector: "ui-text", inputs: ["text", "modifiers"] }] });
641
+ `, isInline: true, dependencies: [{ kind: "component", type: UiSlot, selector: "ui-slot", inputs: ["name", "extraProps"] }, { kind: "component", type: UiText, selector: "ui-text", inputs: ["text", "date", "dateStyle", "timerInterval", "countsDown", "modifiers"] }] });
607
642
  }
608
643
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiPicker, decorators: [{
609
644
  type: Component,
@@ -847,15 +882,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
847
882
  /**
848
883
  * A SwiftUI `ProgressView`: a bar filled to `value`, from 0 to 1, or a spinner without one. On
849
884
  * Android, Compose's `LinearProgressIndicator`, a moving bar rather than a spinner without a value.
885
+ * On iOS, `timerInterval` has the bar keep time by itself from its `lower` to its `upper`, emptying
886
+ * unless `countsDown` is false.
850
887
  */
851
888
  export class UiProgress {
852
889
  nativeView = nativeView();
853
890
  android = nativePlatform() === 'android';
854
891
  value = input(undefined, { ...(ngDevMode ? { debugName: "value" } : /* istanbul ignore next */ {}), transform: optionalNumber });
892
+ timerInterval = input(undefined, { ...(ngDevMode ? { debugName: "timerInterval" } : /* istanbul ignore next */ {}), transform: timerMilliseconds });
893
+ countsDown = input(undefined, { ...(ngDevMode ? { debugName: "countsDown" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
855
894
  modifiers = input(/* @ts-ignore */
856
895
  ...(ngDevMode ? [undefined, { debugName: "modifiers" }] : /* istanbul ignore next */ []));
857
896
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiProgress, deps: [], target: i0.ɵɵFactoryTarget.Component });
858
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: UiProgress, isStandalone: true, selector: "ui-progress", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, modifiers: { classPropertyName: "modifiers", publicName: "modifiers", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "value": "android ? undefined : value()", "progress": "android ? value() : undefined", "modifiers": "modifiers()" } }, ngImport: i0, template: '', isInline: true });
897
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: UiProgress, isStandalone: true, selector: "ui-progress", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, timerInterval: { classPropertyName: "timerInterval", publicName: "timerInterval", isSignal: true, isRequired: false, transformFunction: null }, countsDown: { classPropertyName: "countsDown", publicName: "countsDown", isSignal: true, isRequired: false, transformFunction: null }, modifiers: { classPropertyName: "modifiers", publicName: "modifiers", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "value": "android ? undefined : value()", "progress": "android ? value() : undefined", "timerInterval": "android ? undefined : timerInterval()", "countsDown": "android ? undefined : countsDown()", "modifiers": "modifiers()" } }, ngImport: i0, template: '', isInline: true });
859
898
  }
860
899
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: UiProgress, decorators: [{
861
900
  type: Component,
@@ -865,10 +904,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
865
904
  host: {
866
905
  '[value]': 'android ? undefined : value()',
867
906
  '[progress]': 'android ? value() : undefined',
907
+ '[timerInterval]': 'android ? undefined : timerInterval()',
908
+ '[countsDown]': 'android ? undefined : countsDown()',
868
909
  '[modifiers]': 'modifiers()',
869
910
  },
870
911
  }]
871
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
912
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], timerInterval: [{ type: i0.Input, args: [{ isSignal: true, alias: "timerInterval", required: false }] }], countsDown: [{ type: i0.Input, args: [{ isSignal: true, alias: "countsDown", required: false }] }], modifiers: [{ type: i0.Input, args: [{ isSignal: true, alias: "modifiers", required: false }] }] } });
872
913
  /**
873
914
  * A SwiftUI `HStack`, or Compose's `Row` on Android, centred, as SwiftUI's is, unless `alignment`
874
915
  * says otherwise. Compose has no `firstTextBaseline`, and centres instead. Without `spacing`, SwiftUI