@capgo/capacitor-health 8.1.3 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/android/src/main/java/app/capgo/plugin/health/HealthManager.kt +142 -2
- package/android/src/main/java/app/capgo/plugin/health/HealthPlugin.kt +67 -9
- package/android/src/main/java/app/capgo/plugin/health/WorkoutType.kt +64 -0
- package/dist/docs.json +267 -0
- package/dist/esm/definitions.d.ts +45 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +3 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +3 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +3 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/HealthPlugin/Health.swift +239 -31
- package/ios/Sources/HealthPlugin/HealthPlugin.swift +28 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,7 @@ All write operations expect the default unit shown above. On Android the `metada
|
|
|
163
163
|
* [`getPluginVersion()`](#getpluginversion)
|
|
164
164
|
* [`openHealthConnectSettings()`](#openhealthconnectsettings)
|
|
165
165
|
* [`showPrivacyPolicy()`](#showprivacypolicy)
|
|
166
|
+
* [`queryWorkouts(...)`](#queryworkouts)
|
|
166
167
|
* [Interfaces](#interfaces)
|
|
167
168
|
* [Type Aliases](#type-aliases)
|
|
168
169
|
|
|
@@ -297,6 +298,24 @@ or by placing an HTML file at www/privacypolicy.html in your assets.
|
|
|
297
298
|
--------------------
|
|
298
299
|
|
|
299
300
|
|
|
301
|
+
### queryWorkouts(...)
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
queryWorkouts(options: QueryWorkoutsOptions) => Promise<QueryWorkoutsResult>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Queries workout sessions from the native health store.
|
|
308
|
+
Supported on iOS (HealthKit) and Android (Health Connect).
|
|
309
|
+
|
|
310
|
+
| Param | Type | Description |
|
|
311
|
+
| ------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
312
|
+
| **`options`** | <code><a href="#queryworkoutsoptions">QueryWorkoutsOptions</a></code> | Query options including optional workout type filter, date range, limit, and sort order |
|
|
313
|
+
|
|
314
|
+
**Returns:** <code>Promise<<a href="#queryworkoutsresult">QueryWorkoutsResult</a>></code>
|
|
315
|
+
|
|
316
|
+
--------------------
|
|
317
|
+
|
|
318
|
+
|
|
300
319
|
### Interfaces
|
|
301
320
|
|
|
302
321
|
|
|
@@ -370,6 +389,39 @@ or by placing an HTML file at www/privacypolicy.html in your assets.
|
|
|
370
389
|
| **`metadata`** | <code><a href="#record">Record</a><string, string></code> | Metadata key-value pairs forwarded to the native APIs where supported. |
|
|
371
390
|
|
|
372
391
|
|
|
392
|
+
#### QueryWorkoutsResult
|
|
393
|
+
|
|
394
|
+
| Prop | Type |
|
|
395
|
+
| -------------- | ---------------------- |
|
|
396
|
+
| **`workouts`** | <code>Workout[]</code> |
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
#### Workout
|
|
400
|
+
|
|
401
|
+
| Prop | Type | Description |
|
|
402
|
+
| ----------------------- | --------------------------------------------------------------- | --------------------------------------------------- |
|
|
403
|
+
| **`workoutType`** | <code><a href="#workouttype">WorkoutType</a></code> | The type of workout. |
|
|
404
|
+
| **`duration`** | <code>number</code> | Duration of the workout in seconds. |
|
|
405
|
+
| **`totalEnergyBurned`** | <code>number</code> | Total energy burned in kilocalories (if available). |
|
|
406
|
+
| **`totalDistance`** | <code>number</code> | Total distance in meters (if available). |
|
|
407
|
+
| **`startDate`** | <code>string</code> | ISO 8601 start date of the workout. |
|
|
408
|
+
| **`endDate`** | <code>string</code> | ISO 8601 end date of the workout. |
|
|
409
|
+
| **`sourceName`** | <code>string</code> | Source name that recorded the workout. |
|
|
410
|
+
| **`sourceId`** | <code>string</code> | Source bundle identifier. |
|
|
411
|
+
| **`metadata`** | <code><a href="#record">Record</a><string, string></code> | Additional metadata (if available). |
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
#### QueryWorkoutsOptions
|
|
415
|
+
|
|
416
|
+
| Prop | Type | Description |
|
|
417
|
+
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
418
|
+
| **`workoutType`** | <code><a href="#workouttype">WorkoutType</a></code> | Optional workout type filter. If omitted, all workout types are returned. |
|
|
419
|
+
| **`startDate`** | <code>string</code> | Inclusive ISO 8601 start date (defaults to now - 1 day). |
|
|
420
|
+
| **`endDate`** | <code>string</code> | Exclusive ISO 8601 end date (defaults to now). |
|
|
421
|
+
| **`limit`** | <code>number</code> | Maximum number of workouts to return (defaults to 100). |
|
|
422
|
+
| **`ascending`** | <code>boolean</code> | Return results sorted ascending by start date (defaults to false). |
|
|
423
|
+
|
|
424
|
+
|
|
373
425
|
### Type Aliases
|
|
374
426
|
|
|
375
427
|
|
|
@@ -389,6 +441,11 @@ Construct a type with a set of properties K of type T
|
|
|
389
441
|
|
|
390
442
|
<code>{
|
|
391
443
|
[P in K]: T;
|
|
392
444
|
}</code>
|
|
393
445
|
|
|
446
|
+
|
|
447
|
+
#### WorkoutType
|
|
448
|
+
|
|
449
|
+
<code>'running' | 'cycling' | 'walking' | 'swimming' | 'yoga' | 'strengthTraining' | 'hiking' | 'tennis' | 'basketball' | 'soccer' | 'americanFootball' | 'baseball' | 'crossTraining' | 'elliptical' | 'rowing' | 'stairClimbing' | 'traditionalStrengthTraining' | 'waterFitness' | 'waterPolo' | 'waterSports' | 'wrestling' | 'other'</code>
|
|
450
|
+
|
|
394
451
|
</docgen-api>
|
|
395
452
|
|
|
396
453
|
### Credits:
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
package app.capgo.plugin.health
|
|
2
2
|
|
|
3
3
|
import androidx.health.connect.client.HealthConnectClient
|
|
4
|
+
import androidx.health.connect.client.permission.HealthPermission
|
|
5
|
+
import androidx.health.connect.client.request.AggregateRequest
|
|
4
6
|
import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
|
|
5
7
|
import androidx.health.connect.client.records.DistanceRecord
|
|
8
|
+
import androidx.health.connect.client.records.ExerciseSessionRecord
|
|
6
9
|
import androidx.health.connect.client.records.HeartRateRecord
|
|
7
10
|
import androidx.health.connect.client.records.Record
|
|
8
11
|
import androidx.health.connect.client.records.StepsRecord
|
|
@@ -13,6 +16,7 @@ import androidx.health.connect.client.units.Energy
|
|
|
13
16
|
import androidx.health.connect.client.units.Length
|
|
14
17
|
import androidx.health.connect.client.units.Mass
|
|
15
18
|
import androidx.health.connect.client.records.metadata.Metadata
|
|
19
|
+
import java.time.Duration
|
|
16
20
|
import com.getcapacitor.JSArray
|
|
17
21
|
import com.getcapacitor.JSObject
|
|
18
22
|
import java.time.Instant
|
|
@@ -26,15 +30,20 @@ class HealthManager {
|
|
|
26
30
|
|
|
27
31
|
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_INSTANT
|
|
28
32
|
|
|
29
|
-
fun permissionsFor(readTypes: Collection<HealthDataType>, writeTypes: Collection<HealthDataType
|
|
33
|
+
fun permissionsFor(readTypes: Collection<HealthDataType>, writeTypes: Collection<HealthDataType>, includeWorkouts: Boolean = false): Set<String> = buildSet {
|
|
30
34
|
readTypes.forEach { add(it.readPermission) }
|
|
31
35
|
writeTypes.forEach { add(it.writePermission) }
|
|
36
|
+
// Include workout read permission if explicitly requested
|
|
37
|
+
if (includeWorkouts) {
|
|
38
|
+
add(HealthPermission.getReadPermission(ExerciseSessionRecord::class))
|
|
39
|
+
}
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
suspend fun authorizationStatus(
|
|
35
43
|
client: HealthConnectClient,
|
|
36
44
|
readTypes: Collection<HealthDataType>,
|
|
37
|
-
writeTypes: Collection<HealthDataType
|
|
45
|
+
writeTypes: Collection<HealthDataType>,
|
|
46
|
+
includeWorkouts: Boolean = false
|
|
38
47
|
): JSObject {
|
|
39
48
|
val granted = client.permissionController.getGrantedPermissions()
|
|
40
49
|
|
|
@@ -48,6 +57,16 @@ class HealthManager {
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
// Check workout permission if requested
|
|
61
|
+
if (includeWorkouts) {
|
|
62
|
+
val workoutPermission = HealthPermission.getReadPermission(ExerciseSessionRecord::class)
|
|
63
|
+
if (granted.contains(workoutPermission)) {
|
|
64
|
+
readAuthorized.put("workouts")
|
|
65
|
+
} else {
|
|
66
|
+
readDenied.put("workouts")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
val writeAuthorized = JSArray()
|
|
52
71
|
val writeDenied = JSArray()
|
|
53
72
|
writeTypes.forEach { type ->
|
|
@@ -273,6 +292,127 @@ class HealthManager {
|
|
|
273
292
|
return java.lang.Math.round(this.coerceAtLeast(0.0))
|
|
274
293
|
}
|
|
275
294
|
|
|
295
|
+
suspend fun queryWorkouts(
|
|
296
|
+
client: HealthConnectClient,
|
|
297
|
+
workoutType: String?,
|
|
298
|
+
startTime: Instant,
|
|
299
|
+
endTime: Instant,
|
|
300
|
+
limit: Int,
|
|
301
|
+
ascending: Boolean
|
|
302
|
+
): JSArray {
|
|
303
|
+
val workouts = mutableListOf<Pair<Instant, JSObject>>()
|
|
304
|
+
|
|
305
|
+
var pageToken: String? = null
|
|
306
|
+
val pageSize = if (limit > 0) min(limit, MAX_PAGE_SIZE) else DEFAULT_PAGE_SIZE
|
|
307
|
+
var fetched = 0
|
|
308
|
+
|
|
309
|
+
val exerciseTypeFilter = WorkoutType.fromString(workoutType)
|
|
310
|
+
|
|
311
|
+
do {
|
|
312
|
+
val request = ReadRecordsRequest(
|
|
313
|
+
recordType = ExerciseSessionRecord::class,
|
|
314
|
+
timeRangeFilter = TimeRangeFilter.between(startTime, endTime),
|
|
315
|
+
pageSize = pageSize,
|
|
316
|
+
pageToken = pageToken
|
|
317
|
+
)
|
|
318
|
+
val response = client.readRecords(request)
|
|
319
|
+
|
|
320
|
+
response.records.forEach { record ->
|
|
321
|
+
val session = record as ExerciseSessionRecord
|
|
322
|
+
|
|
323
|
+
// Filter by exercise type if specified
|
|
324
|
+
if (exerciseTypeFilter != null && session.exerciseType != exerciseTypeFilter) {
|
|
325
|
+
return@forEach
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Aggregate calories and distance for this workout session
|
|
329
|
+
val aggregatedData = aggregateWorkoutData(client, session)
|
|
330
|
+
val payload = createWorkoutPayload(session, aggregatedData)
|
|
331
|
+
workouts.add(session.startTime to payload)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fetched += response.records.size
|
|
335
|
+
pageToken = response.pageToken
|
|
336
|
+
} while (pageToken != null && (limit <= 0 || fetched < limit))
|
|
337
|
+
|
|
338
|
+
val sorted = workouts.sortedBy { it.first }
|
|
339
|
+
val ordered = if (ascending) sorted else sorted.asReversed()
|
|
340
|
+
val limited = if (limit > 0) ordered.take(limit) else ordered
|
|
341
|
+
|
|
342
|
+
val array = JSArray()
|
|
343
|
+
limited.forEach { array.put(it.second) }
|
|
344
|
+
return array
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private suspend fun aggregateWorkoutData(
|
|
348
|
+
client: HealthConnectClient,
|
|
349
|
+
session: ExerciseSessionRecord
|
|
350
|
+
): WorkoutAggregatedData {
|
|
351
|
+
val timeRange = TimeRangeFilter.between(session.startTime, session.endTime)
|
|
352
|
+
// Don't filter by dataOrigin - distance might come from different sources
|
|
353
|
+
// than the workout session itself (e.g., fitness tracker vs workout app)
|
|
354
|
+
|
|
355
|
+
// Aggregate distance
|
|
356
|
+
val distanceAggregate = try {
|
|
357
|
+
val aggregateRequest = AggregateRequest(
|
|
358
|
+
metrics = setOf(DistanceRecord.DISTANCE_TOTAL),
|
|
359
|
+
timeRangeFilter = timeRange
|
|
360
|
+
// Removed dataOriginFilter to get distance from all sources during workout time
|
|
361
|
+
)
|
|
362
|
+
val result = client.aggregate(aggregateRequest)
|
|
363
|
+
result[DistanceRecord.DISTANCE_TOTAL]?.inMeters
|
|
364
|
+
} catch (e: Exception) {
|
|
365
|
+
android.util.Log.d("HealthManager", "Distance aggregation failed for workout: ${e.message}", e)
|
|
366
|
+
null // Permission might not be granted or no data available
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return WorkoutAggregatedData(
|
|
370
|
+
totalDistance = distanceAggregate
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private data class WorkoutAggregatedData(
|
|
375
|
+
val totalDistance: Double?
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
private fun createWorkoutPayload(session: ExerciseSessionRecord, aggregatedData: WorkoutAggregatedData): JSObject {
|
|
379
|
+
val payload = JSObject()
|
|
380
|
+
|
|
381
|
+
// Workout type
|
|
382
|
+
payload.put("workoutType", WorkoutType.toWorkoutTypeString(session.exerciseType))
|
|
383
|
+
|
|
384
|
+
// Duration in seconds
|
|
385
|
+
val durationSeconds = Duration.between(session.startTime, session.endTime).seconds.toInt()
|
|
386
|
+
payload.put("duration", durationSeconds)
|
|
387
|
+
|
|
388
|
+
// Start and end dates
|
|
389
|
+
payload.put("startDate", formatter.format(session.startTime))
|
|
390
|
+
payload.put("endDate", formatter.format(session.endTime))
|
|
391
|
+
|
|
392
|
+
// Total distance (aggregated from DistanceRecord)
|
|
393
|
+
aggregatedData.totalDistance?.let { distance ->
|
|
394
|
+
payload.put("totalDistance", distance)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Source information
|
|
398
|
+
val dataOrigin = session.metadata.dataOrigin
|
|
399
|
+
payload.put("sourceId", dataOrigin.packageName)
|
|
400
|
+
payload.put("sourceName", dataOrigin.packageName)
|
|
401
|
+
session.metadata.device?.let { device ->
|
|
402
|
+
val manufacturer = device.manufacturer?.takeIf { it.isNotBlank() }
|
|
403
|
+
val model = device.model?.takeIf { it.isNotBlank() }
|
|
404
|
+
val label = listOfNotNull(manufacturer, model).joinToString(" ").trim()
|
|
405
|
+
if (label.isNotEmpty()) {
|
|
406
|
+
payload.put("sourceName", label)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Note: customMetadata is not available on Metadata in Health Connect
|
|
411
|
+
// Metadata only contains dataOrigin, device, and lastModifiedTime
|
|
412
|
+
|
|
413
|
+
return payload
|
|
414
|
+
}
|
|
415
|
+
|
|
276
416
|
companion object {
|
|
277
417
|
private const val DEFAULT_PAGE_SIZE = 100
|
|
278
418
|
private const val MAX_PAGE_SIZE = 500
|
|
@@ -32,6 +32,7 @@ class HealthPlugin : Plugin() {
|
|
|
32
32
|
// Store pending request data for callback
|
|
33
33
|
private var pendingReadTypes: List<HealthDataType> = emptyList()
|
|
34
34
|
private var pendingWriteTypes: List<HealthDataType> = emptyList()
|
|
35
|
+
private var pendingIncludeWorkouts: Boolean = false
|
|
35
36
|
|
|
36
37
|
override fun handleOnDestroy() {
|
|
37
38
|
super.handleOnDestroy()
|
|
@@ -46,8 +47,8 @@ class HealthPlugin : Plugin() {
|
|
|
46
47
|
|
|
47
48
|
@PluginMethod
|
|
48
49
|
fun requestAuthorization(call: PluginCall) {
|
|
49
|
-
val readTypes = try {
|
|
50
|
-
|
|
50
|
+
val (readTypes, includeWorkouts) = try {
|
|
51
|
+
parseTypeListWithWorkouts(call, "read")
|
|
51
52
|
} catch (e: IllegalArgumentException) {
|
|
52
53
|
call.reject(e.message, null, e)
|
|
53
54
|
return
|
|
@@ -62,17 +63,17 @@ class HealthPlugin : Plugin() {
|
|
|
62
63
|
|
|
63
64
|
pluginScope.launch {
|
|
64
65
|
val client = getClientOrReject(call) ?: return@launch
|
|
65
|
-
val permissions = manager.permissionsFor(readTypes, writeTypes)
|
|
66
|
+
val permissions = manager.permissionsFor(readTypes, writeTypes, includeWorkouts)
|
|
66
67
|
|
|
67
68
|
if (permissions.isEmpty()) {
|
|
68
|
-
val status = manager.authorizationStatus(client, readTypes, writeTypes)
|
|
69
|
+
val status = manager.authorizationStatus(client, readTypes, writeTypes, includeWorkouts)
|
|
69
70
|
call.resolve(status)
|
|
70
71
|
return@launch
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
val granted = client.permissionController.getGrantedPermissions()
|
|
74
75
|
if (granted.containsAll(permissions)) {
|
|
75
|
-
val status = manager.authorizationStatus(client, readTypes, writeTypes)
|
|
76
|
+
val status = manager.authorizationStatus(client, readTypes, writeTypes, includeWorkouts)
|
|
76
77
|
call.resolve(status)
|
|
77
78
|
return@launch
|
|
78
79
|
}
|
|
@@ -80,6 +81,7 @@ class HealthPlugin : Plugin() {
|
|
|
80
81
|
// Store types for callback
|
|
81
82
|
pendingReadTypes = readTypes
|
|
82
83
|
pendingWriteTypes = writeTypes
|
|
84
|
+
pendingIncludeWorkouts = includeWorkouts
|
|
83
85
|
|
|
84
86
|
// Create intent using the Health Connect permission contract
|
|
85
87
|
val intent = permissionContract.createIntent(context, permissions)
|
|
@@ -102,20 +104,22 @@ class HealthPlugin : Plugin() {
|
|
|
102
104
|
|
|
103
105
|
val readTypes = pendingReadTypes
|
|
104
106
|
val writeTypes = pendingWriteTypes
|
|
107
|
+
val includeWorkouts = pendingIncludeWorkouts
|
|
105
108
|
pendingReadTypes = emptyList()
|
|
106
109
|
pendingWriteTypes = emptyList()
|
|
110
|
+
pendingIncludeWorkouts = false
|
|
107
111
|
|
|
108
112
|
pluginScope.launch {
|
|
109
113
|
val client = getClientOrReject(call) ?: return@launch
|
|
110
|
-
val status = manager.authorizationStatus(client, readTypes, writeTypes)
|
|
114
|
+
val status = manager.authorizationStatus(client, readTypes, writeTypes, includeWorkouts)
|
|
111
115
|
call.resolve(status)
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
@PluginMethod
|
|
116
120
|
fun checkAuthorization(call: PluginCall) {
|
|
117
|
-
val readTypes = try {
|
|
118
|
-
|
|
121
|
+
val (readTypes, includeWorkouts) = try {
|
|
122
|
+
parseTypeListWithWorkouts(call, "read")
|
|
119
123
|
} catch (e: IllegalArgumentException) {
|
|
120
124
|
call.reject(e.message, null, e)
|
|
121
125
|
return
|
|
@@ -130,7 +134,7 @@ class HealthPlugin : Plugin() {
|
|
|
130
134
|
|
|
131
135
|
pluginScope.launch {
|
|
132
136
|
val client = getClientOrReject(call) ?: return@launch
|
|
133
|
-
val status = manager.authorizationStatus(client, readTypes, writeTypes)
|
|
137
|
+
val status = manager.authorizationStatus(client, readTypes, writeTypes, includeWorkouts)
|
|
134
138
|
call.resolve(status)
|
|
135
139
|
}
|
|
136
140
|
}
|
|
@@ -265,6 +269,23 @@ class HealthPlugin : Plugin() {
|
|
|
265
269
|
return result
|
|
266
270
|
}
|
|
267
271
|
|
|
272
|
+
private fun parseTypeListWithWorkouts(call: PluginCall, key: String): Pair<List<HealthDataType>, Boolean> {
|
|
273
|
+
val array = call.getArray(key) ?: JSArray()
|
|
274
|
+
val result = mutableListOf<HealthDataType>()
|
|
275
|
+
var includeWorkouts = false
|
|
276
|
+
for (i in 0 until array.length()) {
|
|
277
|
+
val identifier = array.optString(i, null) ?: continue
|
|
278
|
+
if (identifier == "workouts") {
|
|
279
|
+
includeWorkouts = true
|
|
280
|
+
} else {
|
|
281
|
+
val dataType = HealthDataType.from(identifier)
|
|
282
|
+
?: throw IllegalArgumentException("Unsupported data type: $identifier")
|
|
283
|
+
result.add(dataType)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return Pair(result, includeWorkouts)
|
|
287
|
+
}
|
|
288
|
+
|
|
268
289
|
private fun getClientOrReject(call: PluginCall): HealthConnectClient? {
|
|
269
290
|
val status = HealthConnectClient.getSdkStatus(context)
|
|
270
291
|
if (status != HealthConnectClient.SDK_AVAILABLE) {
|
|
@@ -327,6 +348,43 @@ class HealthPlugin : Plugin() {
|
|
|
327
348
|
}
|
|
328
349
|
}
|
|
329
350
|
|
|
351
|
+
@PluginMethod
|
|
352
|
+
fun queryWorkouts(call: PluginCall) {
|
|
353
|
+
val workoutType = call.getString("workoutType")
|
|
354
|
+
val limit = (call.getInt("limit") ?: DEFAULT_LIMIT).coerceAtLeast(0)
|
|
355
|
+
val ascending = call.getBoolean("ascending") ?: false
|
|
356
|
+
|
|
357
|
+
val startInstant = try {
|
|
358
|
+
manager.parseInstant(call.getString("startDate"), Instant.now().minus(DEFAULT_PAST_DURATION))
|
|
359
|
+
} catch (e: DateTimeParseException) {
|
|
360
|
+
call.reject(e.message, null, e)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
val endInstant = try {
|
|
365
|
+
manager.parseInstant(call.getString("endDate"), Instant.now())
|
|
366
|
+
} catch (e: DateTimeParseException) {
|
|
367
|
+
call.reject(e.message, null, e)
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (endInstant.isBefore(startInstant)) {
|
|
372
|
+
call.reject("endDate must be greater than or equal to startDate")
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
pluginScope.launch {
|
|
377
|
+
val client = getClientOrReject(call) ?: return@launch
|
|
378
|
+
try {
|
|
379
|
+
val workouts = manager.queryWorkouts(client, workoutType, startInstant, endInstant, limit, ascending)
|
|
380
|
+
val result = JSObject().apply { put("workouts", workouts) }
|
|
381
|
+
call.resolve(result)
|
|
382
|
+
} catch (e: Exception) {
|
|
383
|
+
call.reject(e.message ?: "Failed to query workouts.", null, e)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
330
388
|
companion object {
|
|
331
389
|
private const val DEFAULT_LIMIT = 100
|
|
332
390
|
private val DEFAULT_PAST_DURATION: Duration = Duration.ofDays(1)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package app.capgo.plugin.health
|
|
2
|
+
|
|
3
|
+
import androidx.health.connect.client.records.ExerciseSessionRecord
|
|
4
|
+
|
|
5
|
+
object WorkoutType {
|
|
6
|
+
fun fromString(type: String?): Int? {
|
|
7
|
+
if (type.isNullOrBlank()) return null
|
|
8
|
+
|
|
9
|
+
return when (type) {
|
|
10
|
+
"running" -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING
|
|
11
|
+
"cycling" -> ExerciseSessionRecord.EXERCISE_TYPE_BIKING
|
|
12
|
+
"walking" -> ExerciseSessionRecord.EXERCISE_TYPE_WALKING
|
|
13
|
+
"swimming" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL
|
|
14
|
+
"yoga" -> ExerciseSessionRecord.EXERCISE_TYPE_YOGA
|
|
15
|
+
"strengthTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING
|
|
16
|
+
"hiking" -> ExerciseSessionRecord.EXERCISE_TYPE_HIKING
|
|
17
|
+
"tennis" -> ExerciseSessionRecord.EXERCISE_TYPE_TENNIS
|
|
18
|
+
"basketball" -> ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL
|
|
19
|
+
"soccer" -> ExerciseSessionRecord.EXERCISE_TYPE_SOCCER
|
|
20
|
+
"americanFootball" -> ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_AMERICAN
|
|
21
|
+
"baseball" -> ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL
|
|
22
|
+
"crossTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING
|
|
23
|
+
"elliptical" -> ExerciseSessionRecord.EXERCISE_TYPE_ELLIPTICAL
|
|
24
|
+
"rowing" -> ExerciseSessionRecord.EXERCISE_TYPE_ROWING
|
|
25
|
+
"stairClimbing" -> ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING
|
|
26
|
+
"traditionalStrengthTraining" -> ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING
|
|
27
|
+
"waterFitness" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL
|
|
28
|
+
"waterPolo" -> ExerciseSessionRecord.EXERCISE_TYPE_WATER_POLO
|
|
29
|
+
"waterSports" -> ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_OPEN_WATER
|
|
30
|
+
"wrestling" -> ExerciseSessionRecord.EXERCISE_TYPE_MARTIAL_ARTS
|
|
31
|
+
"other" -> ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT
|
|
32
|
+
else -> null
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fun toWorkoutTypeString(exerciseType: Int): String {
|
|
37
|
+
return when (exerciseType) {
|
|
38
|
+
ExerciseSessionRecord.EXERCISE_TYPE_RUNNING -> "running"
|
|
39
|
+
ExerciseSessionRecord.EXERCISE_TYPE_BIKING -> "cycling"
|
|
40
|
+
ExerciseSessionRecord.EXERCISE_TYPE_BIKING_STATIONARY -> "cycling"
|
|
41
|
+
ExerciseSessionRecord.EXERCISE_TYPE_WALKING -> "walking"
|
|
42
|
+
ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_POOL -> "swimming"
|
|
43
|
+
ExerciseSessionRecord.EXERCISE_TYPE_SWIMMING_OPEN_WATER -> "swimming"
|
|
44
|
+
ExerciseSessionRecord.EXERCISE_TYPE_YOGA -> "yoga"
|
|
45
|
+
ExerciseSessionRecord.EXERCISE_TYPE_STRENGTH_TRAINING -> "strengthTraining"
|
|
46
|
+
ExerciseSessionRecord.EXERCISE_TYPE_HIKING -> "hiking"
|
|
47
|
+
ExerciseSessionRecord.EXERCISE_TYPE_TENNIS -> "tennis"
|
|
48
|
+
ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL -> "basketball"
|
|
49
|
+
ExerciseSessionRecord.EXERCISE_TYPE_SOCCER -> "soccer"
|
|
50
|
+
ExerciseSessionRecord.EXERCISE_TYPE_FOOTBALL_AMERICAN -> "americanFootball"
|
|
51
|
+
ExerciseSessionRecord.EXERCISE_TYPE_BASEBALL -> "baseball"
|
|
52
|
+
ExerciseSessionRecord.EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING -> "crossTraining"
|
|
53
|
+
ExerciseSessionRecord.EXERCISE_TYPE_ELLIPTICAL -> "elliptical"
|
|
54
|
+
ExerciseSessionRecord.EXERCISE_TYPE_ROWING -> "rowing"
|
|
55
|
+
ExerciseSessionRecord.EXERCISE_TYPE_ROWING_MACHINE -> "rowing"
|
|
56
|
+
ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING -> "stairClimbing"
|
|
57
|
+
ExerciseSessionRecord.EXERCISE_TYPE_STAIR_CLIMBING_MACHINE -> "stairClimbing"
|
|
58
|
+
ExerciseSessionRecord.EXERCISE_TYPE_WATER_POLO -> "waterPolo"
|
|
59
|
+
ExerciseSessionRecord.EXERCISE_TYPE_MARTIAL_ARTS -> "wrestling"
|
|
60
|
+
ExerciseSessionRecord.EXERCISE_TYPE_OTHER_WORKOUT -> "other"
|
|
61
|
+
else -> "other"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/docs.json
CHANGED
|
@@ -140,6 +140,38 @@
|
|
|
140
140
|
"docs": "Shows the app's privacy policy for Health Connect (Android only).\nOn iOS, this method does nothing.\n\nThis displays the same privacy policy screen that Health Connect shows\nwhen the user taps \"Privacy policy\" in the permissions dialog.\n\nThe privacy policy URL can be configured by adding a string resource\nnamed \"health_connect_privacy_policy_url\" in your app's strings.xml,\nor by placing an HTML file at www/privacypolicy.html in your assets.",
|
|
141
141
|
"complexTypes": [],
|
|
142
142
|
"slug": "showprivacypolicy"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "queryWorkouts",
|
|
146
|
+
"signature": "(options: QueryWorkoutsOptions) => Promise<QueryWorkoutsResult>",
|
|
147
|
+
"parameters": [
|
|
148
|
+
{
|
|
149
|
+
"name": "options",
|
|
150
|
+
"docs": "Query options including optional workout type filter, date range, limit, and sort order",
|
|
151
|
+
"type": "QueryWorkoutsOptions"
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"returns": "Promise<QueryWorkoutsResult>",
|
|
155
|
+
"tags": [
|
|
156
|
+
{
|
|
157
|
+
"name": "param",
|
|
158
|
+
"text": "options Query options including optional workout type filter, date range, limit, and sort order"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"name": "returns",
|
|
162
|
+
"text": "A promise that resolves with the workout sessions"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "throws",
|
|
166
|
+
"text": "An error if something went wrong"
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
"docs": "Queries workout sessions from the native health store.\nSupported on iOS (HealthKit) and Android (Health Connect).",
|
|
170
|
+
"complexTypes": [
|
|
171
|
+
"QueryWorkoutsResult",
|
|
172
|
+
"QueryWorkoutsOptions"
|
|
173
|
+
],
|
|
174
|
+
"slug": "queryworkouts"
|
|
143
175
|
}
|
|
144
176
|
],
|
|
145
177
|
"properties": []
|
|
@@ -429,6 +461,146 @@
|
|
|
429
461
|
"type": "Record<string, string>"
|
|
430
462
|
}
|
|
431
463
|
]
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"name": "QueryWorkoutsResult",
|
|
467
|
+
"slug": "queryworkoutsresult",
|
|
468
|
+
"docs": "",
|
|
469
|
+
"tags": [],
|
|
470
|
+
"methods": [],
|
|
471
|
+
"properties": [
|
|
472
|
+
{
|
|
473
|
+
"name": "workouts",
|
|
474
|
+
"tags": [],
|
|
475
|
+
"docs": "",
|
|
476
|
+
"complexTypes": [
|
|
477
|
+
"Workout"
|
|
478
|
+
],
|
|
479
|
+
"type": "Workout[]"
|
|
480
|
+
}
|
|
481
|
+
]
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
"name": "Workout",
|
|
485
|
+
"slug": "workout",
|
|
486
|
+
"docs": "",
|
|
487
|
+
"tags": [],
|
|
488
|
+
"methods": [],
|
|
489
|
+
"properties": [
|
|
490
|
+
{
|
|
491
|
+
"name": "workoutType",
|
|
492
|
+
"tags": [],
|
|
493
|
+
"docs": "The type of workout.",
|
|
494
|
+
"complexTypes": [
|
|
495
|
+
"WorkoutType"
|
|
496
|
+
],
|
|
497
|
+
"type": "WorkoutType"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"name": "duration",
|
|
501
|
+
"tags": [],
|
|
502
|
+
"docs": "Duration of the workout in seconds.",
|
|
503
|
+
"complexTypes": [],
|
|
504
|
+
"type": "number"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"name": "totalEnergyBurned",
|
|
508
|
+
"tags": [],
|
|
509
|
+
"docs": "Total energy burned in kilocalories (if available).",
|
|
510
|
+
"complexTypes": [],
|
|
511
|
+
"type": "number | undefined"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
"name": "totalDistance",
|
|
515
|
+
"tags": [],
|
|
516
|
+
"docs": "Total distance in meters (if available).",
|
|
517
|
+
"complexTypes": [],
|
|
518
|
+
"type": "number | undefined"
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"name": "startDate",
|
|
522
|
+
"tags": [],
|
|
523
|
+
"docs": "ISO 8601 start date of the workout.",
|
|
524
|
+
"complexTypes": [],
|
|
525
|
+
"type": "string"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"name": "endDate",
|
|
529
|
+
"tags": [],
|
|
530
|
+
"docs": "ISO 8601 end date of the workout.",
|
|
531
|
+
"complexTypes": [],
|
|
532
|
+
"type": "string"
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
"name": "sourceName",
|
|
536
|
+
"tags": [],
|
|
537
|
+
"docs": "Source name that recorded the workout.",
|
|
538
|
+
"complexTypes": [],
|
|
539
|
+
"type": "string | undefined"
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"name": "sourceId",
|
|
543
|
+
"tags": [],
|
|
544
|
+
"docs": "Source bundle identifier.",
|
|
545
|
+
"complexTypes": [],
|
|
546
|
+
"type": "string | undefined"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
"name": "metadata",
|
|
550
|
+
"tags": [],
|
|
551
|
+
"docs": "Additional metadata (if available).",
|
|
552
|
+
"complexTypes": [
|
|
553
|
+
"Record"
|
|
554
|
+
],
|
|
555
|
+
"type": "Record<string, string>"
|
|
556
|
+
}
|
|
557
|
+
]
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"name": "QueryWorkoutsOptions",
|
|
561
|
+
"slug": "queryworkoutsoptions",
|
|
562
|
+
"docs": "",
|
|
563
|
+
"tags": [],
|
|
564
|
+
"methods": [],
|
|
565
|
+
"properties": [
|
|
566
|
+
{
|
|
567
|
+
"name": "workoutType",
|
|
568
|
+
"tags": [],
|
|
569
|
+
"docs": "Optional workout type filter. If omitted, all workout types are returned.",
|
|
570
|
+
"complexTypes": [
|
|
571
|
+
"WorkoutType"
|
|
572
|
+
],
|
|
573
|
+
"type": "WorkoutType"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"name": "startDate",
|
|
577
|
+
"tags": [],
|
|
578
|
+
"docs": "Inclusive ISO 8601 start date (defaults to now - 1 day).",
|
|
579
|
+
"complexTypes": [],
|
|
580
|
+
"type": "string | undefined"
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
"name": "endDate",
|
|
584
|
+
"tags": [],
|
|
585
|
+
"docs": "Exclusive ISO 8601 end date (defaults to now).",
|
|
586
|
+
"complexTypes": [],
|
|
587
|
+
"type": "string | undefined"
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
"name": "limit",
|
|
591
|
+
"tags": [],
|
|
592
|
+
"docs": "Maximum number of workouts to return (defaults to 100).",
|
|
593
|
+
"complexTypes": [],
|
|
594
|
+
"type": "number | undefined"
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
"name": "ascending",
|
|
598
|
+
"tags": [],
|
|
599
|
+
"docs": "Return results sorted ascending by start date (defaults to false).",
|
|
600
|
+
"complexTypes": [],
|
|
601
|
+
"type": "boolean | undefined"
|
|
602
|
+
}
|
|
603
|
+
]
|
|
432
604
|
}
|
|
433
605
|
],
|
|
434
606
|
"enums": [],
|
|
@@ -500,6 +672,101 @@
|
|
|
500
672
|
]
|
|
501
673
|
}
|
|
502
674
|
]
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
"name": "WorkoutType",
|
|
678
|
+
"slug": "workouttype",
|
|
679
|
+
"docs": "",
|
|
680
|
+
"types": [
|
|
681
|
+
{
|
|
682
|
+
"text": "'running'",
|
|
683
|
+
"complexTypes": []
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
"text": "'cycling'",
|
|
687
|
+
"complexTypes": []
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
"text": "'walking'",
|
|
691
|
+
"complexTypes": []
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
"text": "'swimming'",
|
|
695
|
+
"complexTypes": []
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
"text": "'yoga'",
|
|
699
|
+
"complexTypes": []
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
"text": "'strengthTraining'",
|
|
703
|
+
"complexTypes": []
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
"text": "'hiking'",
|
|
707
|
+
"complexTypes": []
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
"text": "'tennis'",
|
|
711
|
+
"complexTypes": []
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
"text": "'basketball'",
|
|
715
|
+
"complexTypes": []
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
"text": "'soccer'",
|
|
719
|
+
"complexTypes": []
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
"text": "'americanFootball'",
|
|
723
|
+
"complexTypes": []
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
"text": "'baseball'",
|
|
727
|
+
"complexTypes": []
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
"text": "'crossTraining'",
|
|
731
|
+
"complexTypes": []
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
"text": "'elliptical'",
|
|
735
|
+
"complexTypes": []
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
"text": "'rowing'",
|
|
739
|
+
"complexTypes": []
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
"text": "'stairClimbing'",
|
|
743
|
+
"complexTypes": []
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
"text": "'traditionalStrengthTraining'",
|
|
747
|
+
"complexTypes": []
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
"text": "'waterFitness'",
|
|
751
|
+
"complexTypes": []
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
"text": "'waterPolo'",
|
|
755
|
+
"complexTypes": []
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
"text": "'waterSports'",
|
|
759
|
+
"complexTypes": []
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
"text": "'wrestling'",
|
|
763
|
+
"complexTypes": []
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
"text": "'other'",
|
|
767
|
+
"complexTypes": []
|
|
768
|
+
}
|
|
769
|
+
]
|
|
503
770
|
}
|
|
504
771
|
],
|
|
505
772
|
"pluginConfigs": []
|
|
@@ -42,6 +42,42 @@ export interface HealthSample {
|
|
|
42
42
|
export interface ReadSamplesResult {
|
|
43
43
|
samples: HealthSample[];
|
|
44
44
|
}
|
|
45
|
+
export type WorkoutType = 'running' | 'cycling' | 'walking' | 'swimming' | 'yoga' | 'strengthTraining' | 'hiking' | 'tennis' | 'basketball' | 'soccer' | 'americanFootball' | 'baseball' | 'crossTraining' | 'elliptical' | 'rowing' | 'stairClimbing' | 'traditionalStrengthTraining' | 'waterFitness' | 'waterPolo' | 'waterSports' | 'wrestling' | 'other';
|
|
46
|
+
export interface QueryWorkoutsOptions {
|
|
47
|
+
/** Optional workout type filter. If omitted, all workout types are returned. */
|
|
48
|
+
workoutType?: WorkoutType;
|
|
49
|
+
/** Inclusive ISO 8601 start date (defaults to now - 1 day). */
|
|
50
|
+
startDate?: string;
|
|
51
|
+
/** Exclusive ISO 8601 end date (defaults to now). */
|
|
52
|
+
endDate?: string;
|
|
53
|
+
/** Maximum number of workouts to return (defaults to 100). */
|
|
54
|
+
limit?: number;
|
|
55
|
+
/** Return results sorted ascending by start date (defaults to false). */
|
|
56
|
+
ascending?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface Workout {
|
|
59
|
+
/** The type of workout. */
|
|
60
|
+
workoutType: WorkoutType;
|
|
61
|
+
/** Duration of the workout in seconds. */
|
|
62
|
+
duration: number;
|
|
63
|
+
/** Total energy burned in kilocalories (if available). */
|
|
64
|
+
totalEnergyBurned?: number;
|
|
65
|
+
/** Total distance in meters (if available). */
|
|
66
|
+
totalDistance?: number;
|
|
67
|
+
/** ISO 8601 start date of the workout. */
|
|
68
|
+
startDate: string;
|
|
69
|
+
/** ISO 8601 end date of the workout. */
|
|
70
|
+
endDate: string;
|
|
71
|
+
/** Source name that recorded the workout. */
|
|
72
|
+
sourceName?: string;
|
|
73
|
+
/** Source bundle identifier. */
|
|
74
|
+
sourceId?: string;
|
|
75
|
+
/** Additional metadata (if available). */
|
|
76
|
+
metadata?: Record<string, string>;
|
|
77
|
+
}
|
|
78
|
+
export interface QueryWorkoutsResult {
|
|
79
|
+
workouts: Workout[];
|
|
80
|
+
}
|
|
45
81
|
export interface WriteSampleOptions {
|
|
46
82
|
dataType: HealthDataType;
|
|
47
83
|
value: number;
|
|
@@ -101,4 +137,13 @@ export interface HealthPlugin {
|
|
|
101
137
|
* @throws An error if the privacy policy cannot be displayed
|
|
102
138
|
*/
|
|
103
139
|
showPrivacyPolicy(): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Queries workout sessions from the native health store.
|
|
142
|
+
* Supported on iOS (HealthKit) and Android (Health Connect).
|
|
143
|
+
*
|
|
144
|
+
* @param options Query options including optional workout type filter, date range, limit, and sort order
|
|
145
|
+
* @returns A promise that resolves with the workout sessions
|
|
146
|
+
* @throws An error if something went wrong
|
|
147
|
+
*/
|
|
148
|
+
queryWorkouts(options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;
|
|
104
149
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type HealthDataType = 'steps' | 'distance' | 'calories' | 'heartRate' | 'weight';\n\nexport type HealthUnit = 'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram';\n\nexport interface AuthorizationOptions {\n /** Data types that should be readable after authorization. */\n read?: HealthDataType[];\n /** Data types that should be writable after authorization. */\n write?: HealthDataType[];\n}\n\nexport interface AuthorizationStatus {\n readAuthorized: HealthDataType[];\n readDenied: HealthDataType[];\n writeAuthorized: HealthDataType[];\n writeDenied: HealthDataType[];\n}\n\nexport interface AvailabilityResult {\n available: boolean;\n /** Platform specific details (for debugging/diagnostics). */\n platform?: 'ios' | 'android' | 'web';\n reason?: string;\n}\n\nexport interface QueryOptions {\n /** The type of data to retrieve from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of samples to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n}\n\nexport interface HealthSample {\n dataType: HealthDataType;\n value: number;\n unit: HealthUnit;\n startDate: string;\n endDate: string;\n sourceName?: string;\n sourceId?: string;\n}\n\nexport interface ReadSamplesResult {\n samples: HealthSample[];\n}\n\nexport interface WriteSampleOptions {\n dataType: HealthDataType;\n value: number;\n /**\n * Optional unit override. If omitted, the default unit for the data type is used\n * (count for `steps`, meter for `distance`, kilocalorie for `calories`, bpm for `heartRate`, kilogram for `weight`).\n */\n unit?: HealthUnit;\n /** ISO 8601 start date for the sample. Defaults to now. */\n startDate?: string;\n /** ISO 8601 end date for the sample. Defaults to startDate. */\n endDate?: string;\n /** Metadata key-value pairs forwarded to the native APIs where supported. */\n metadata?: Record<string, string>;\n}\n\nexport interface HealthPlugin {\n /** Returns whether the current platform supports the native health SDK. */\n isAvailable(): Promise<AvailabilityResult>;\n /** Requests read/write access to the provided data types. */\n requestAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Checks authorization status for the provided data types without prompting the user. */\n checkAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Reads samples for the given data type within the specified time frame. */\n readSamples(options: QueryOptions): Promise<ReadSamplesResult>;\n /** Writes a single sample to the native health store. */\n saveSample(options: WriteSampleOptions): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ version: string }>} a Promise with version for this device\n * @throws An error if something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n\n /**\n * Opens the Health Connect settings screen (Android only).\n * On iOS, this method does nothing.\n *\n * Use this to direct users to manage their Health Connect permissions\n * or to install Health Connect if not available.\n *\n * @throws An error if Health Connect settings cannot be opened\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Shows the app's privacy policy for Health Connect (Android only).\n * On iOS, this method does nothing.\n *\n * This displays the same privacy policy screen that Health Connect shows\n * when the user taps \"Privacy policy\" in the permissions dialog.\n *\n * The privacy policy URL can be configured by adding a string resource\n * named \"health_connect_privacy_policy_url\" in your app's strings.xml,\n * or by placing an HTML file at www/privacypolicy.html in your assets.\n *\n * @throws An error if the privacy policy cannot be displayed\n */\n showPrivacyPolicy(): Promise<void>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export type HealthDataType = 'steps' | 'distance' | 'calories' | 'heartRate' | 'weight';\n\nexport type HealthUnit = 'count' | 'meter' | 'kilocalorie' | 'bpm' | 'kilogram';\n\nexport interface AuthorizationOptions {\n /** Data types that should be readable after authorization. */\n read?: HealthDataType[];\n /** Data types that should be writable after authorization. */\n write?: HealthDataType[];\n}\n\nexport interface AuthorizationStatus {\n readAuthorized: HealthDataType[];\n readDenied: HealthDataType[];\n writeAuthorized: HealthDataType[];\n writeDenied: HealthDataType[];\n}\n\nexport interface AvailabilityResult {\n available: boolean;\n /** Platform specific details (for debugging/diagnostics). */\n platform?: 'ios' | 'android' | 'web';\n reason?: string;\n}\n\nexport interface QueryOptions {\n /** The type of data to retrieve from the health store. */\n dataType: HealthDataType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of samples to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n}\n\nexport interface HealthSample {\n dataType: HealthDataType;\n value: number;\n unit: HealthUnit;\n startDate: string;\n endDate: string;\n sourceName?: string;\n sourceId?: string;\n}\n\nexport interface ReadSamplesResult {\n samples: HealthSample[];\n}\n\nexport type WorkoutType =\n | 'running'\n | 'cycling'\n | 'walking'\n | 'swimming'\n | 'yoga'\n | 'strengthTraining'\n | 'hiking'\n | 'tennis'\n | 'basketball'\n | 'soccer'\n | 'americanFootball'\n | 'baseball'\n | 'crossTraining'\n | 'elliptical'\n | 'rowing'\n | 'stairClimbing'\n | 'traditionalStrengthTraining'\n | 'waterFitness'\n | 'waterPolo'\n | 'waterSports'\n | 'wrestling'\n | 'other';\n\nexport interface QueryWorkoutsOptions {\n /** Optional workout type filter. If omitted, all workout types are returned. */\n workoutType?: WorkoutType;\n /** Inclusive ISO 8601 start date (defaults to now - 1 day). */\n startDate?: string;\n /** Exclusive ISO 8601 end date (defaults to now). */\n endDate?: string;\n /** Maximum number of workouts to return (defaults to 100). */\n limit?: number;\n /** Return results sorted ascending by start date (defaults to false). */\n ascending?: boolean;\n}\n\nexport interface Workout {\n /** The type of workout. */\n workoutType: WorkoutType;\n /** Duration of the workout in seconds. */\n duration: number;\n /** Total energy burned in kilocalories (if available). */\n totalEnergyBurned?: number;\n /** Total distance in meters (if available). */\n totalDistance?: number;\n /** ISO 8601 start date of the workout. */\n startDate: string;\n /** ISO 8601 end date of the workout. */\n endDate: string;\n /** Source name that recorded the workout. */\n sourceName?: string;\n /** Source bundle identifier. */\n sourceId?: string;\n /** Additional metadata (if available). */\n metadata?: Record<string, string>;\n}\n\nexport interface QueryWorkoutsResult {\n workouts: Workout[];\n}\n\nexport interface WriteSampleOptions {\n dataType: HealthDataType;\n value: number;\n /**\n * Optional unit override. If omitted, the default unit for the data type is used\n * (count for `steps`, meter for `distance`, kilocalorie for `calories`, bpm for `heartRate`, kilogram for `weight`).\n */\n unit?: HealthUnit;\n /** ISO 8601 start date for the sample. Defaults to now. */\n startDate?: string;\n /** ISO 8601 end date for the sample. Defaults to startDate. */\n endDate?: string;\n /** Metadata key-value pairs forwarded to the native APIs where supported. */\n metadata?: Record<string, string>;\n}\n\nexport interface HealthPlugin {\n /** Returns whether the current platform supports the native health SDK. */\n isAvailable(): Promise<AvailabilityResult>;\n /** Requests read/write access to the provided data types. */\n requestAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Checks authorization status for the provided data types without prompting the user. */\n checkAuthorization(options: AuthorizationOptions): Promise<AuthorizationStatus>;\n /** Reads samples for the given data type within the specified time frame. */\n readSamples(options: QueryOptions): Promise<ReadSamplesResult>;\n /** Writes a single sample to the native health store. */\n saveSample(options: WriteSampleOptions): Promise<void>;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ version: string }>} a Promise with version for this device\n * @throws An error if something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n\n /**\n * Opens the Health Connect settings screen (Android only).\n * On iOS, this method does nothing.\n *\n * Use this to direct users to manage their Health Connect permissions\n * or to install Health Connect if not available.\n *\n * @throws An error if Health Connect settings cannot be opened\n */\n openHealthConnectSettings(): Promise<void>;\n\n /**\n * Shows the app's privacy policy for Health Connect (Android only).\n * On iOS, this method does nothing.\n *\n * This displays the same privacy policy screen that Health Connect shows\n * when the user taps \"Privacy policy\" in the permissions dialog.\n *\n * The privacy policy URL can be configured by adding a string resource\n * named \"health_connect_privacy_policy_url\" in your app's strings.xml,\n * or by placing an HTML file at www/privacypolicy.html in your assets.\n *\n * @throws An error if the privacy policy cannot be displayed\n */\n showPrivacyPolicy(): Promise<void>;\n\n /**\n * Queries workout sessions from the native health store.\n * Supported on iOS (HealthKit) and Android (Health Connect).\n *\n * @param options Query options including optional workout type filter, date range, limit, and sort order\n * @returns A promise that resolves with the workout sessions\n * @throws An error if something went wrong\n */\n queryWorkouts(options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;\n}\n"]}
|
package/dist/esm/web.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebPlugin } from '@capacitor/core';
|
|
2
|
-
import type { AuthorizationOptions, AuthorizationStatus, AvailabilityResult, HealthPlugin, QueryOptions, ReadSamplesResult, WriteSampleOptions } from './definitions';
|
|
2
|
+
import type { AuthorizationOptions, AuthorizationStatus, AvailabilityResult, HealthPlugin, QueryOptions, QueryWorkoutsOptions, QueryWorkoutsResult, ReadSamplesResult, WriteSampleOptions } from './definitions';
|
|
3
3
|
export declare class HealthWeb extends WebPlugin implements HealthPlugin {
|
|
4
4
|
isAvailable(): Promise<AvailabilityResult>;
|
|
5
5
|
requestAuthorization(_options: AuthorizationOptions): Promise<AuthorizationStatus>;
|
|
@@ -11,4 +11,5 @@ export declare class HealthWeb extends WebPlugin implements HealthPlugin {
|
|
|
11
11
|
}>;
|
|
12
12
|
openHealthConnectSettings(): Promise<void>;
|
|
13
13
|
showPrivacyPolicy(): Promise<void>;
|
|
14
|
+
queryWorkouts(_options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult>;
|
|
14
15
|
}
|
package/dist/esm/web.js
CHANGED
|
@@ -28,5 +28,8 @@ export class HealthWeb extends WebPlugin {
|
|
|
28
28
|
async showPrivacyPolicy() {
|
|
29
29
|
// No-op on web - Health Connect privacy policy is Android only
|
|
30
30
|
}
|
|
31
|
+
async queryWorkouts(_options) {
|
|
32
|
+
throw this.unimplemented('Querying workouts is only available on native platforms.');
|
|
33
|
+
}
|
|
31
34
|
}
|
|
32
35
|
//# sourceMappingURL=web.js.map
|
package/dist/esm/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAc5C,MAAM,OAAO,SAAU,SAAQ,SAAS;IACtC,KAAK,CAAC,WAAW;QACf,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,iEAAiE;SAC1E,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,QAA8B;QACvD,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,QAA8B;QACrD,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,QAAsB;QACtC,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAA4B;QAC3C,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC,CAAC;IACzF,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,yBAAyB;QAC7B,gDAAgD;IAClD,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,+DAA+D;IACjE,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,QAA8B;QAChD,MAAM,IAAI,CAAC,aAAa,CAAC,0DAA0D,CAAC,CAAC;IACvF,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type {\n AuthorizationOptions,\n AuthorizationStatus,\n AvailabilityResult,\n HealthPlugin,\n QueryOptions,\n QueryWorkoutsOptions,\n QueryWorkoutsResult,\n ReadSamplesResult,\n WriteSampleOptions,\n} from './definitions';\n\nexport class HealthWeb extends WebPlugin implements HealthPlugin {\n async isAvailable(): Promise<AvailabilityResult> {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n\n async requestAuthorization(_options: AuthorizationOptions): Promise<AuthorizationStatus> {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n\n async checkAuthorization(_options: AuthorizationOptions): Promise<AuthorizationStatus> {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n\n async readSamples(_options: QueryOptions): Promise<ReadSamplesResult> {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n\n async saveSample(_options: WriteSampleOptions): Promise<void> {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n\n async getPluginVersion(): Promise<{ version: string }> {\n return { version: 'web' };\n }\n\n async openHealthConnectSettings(): Promise<void> {\n // No-op on web - Health Connect is Android only\n }\n\n async showPrivacyPolicy(): Promise<void> {\n // No-op on web - Health Connect privacy policy is Android only\n }\n\n async queryWorkouts(_options: QueryWorkoutsOptions): Promise<QueryWorkoutsResult> {\n throw this.unimplemented('Querying workouts is only available on native platforms.');\n }\n}\n"]}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -35,6 +35,9 @@ class HealthWeb extends core.WebPlugin {
|
|
|
35
35
|
async showPrivacyPolicy() {
|
|
36
36
|
// No-op on web - Health Connect privacy policy is Android only
|
|
37
37
|
}
|
|
38
|
+
async queryWorkouts(_options) {
|
|
39
|
+
throw this.unimplemented('Querying workouts is only available on native platforms.');
|
|
40
|
+
}
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
var web = /*#__PURE__*/Object.freeze({
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst Health = registerPlugin('Health', {\n web: () => import('./web').then((m) => new m.HealthWeb()),\n});\nexport * from './definitions';\nexport { Health };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HealthWeb extends WebPlugin {\n async isAvailable() {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n async requestAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async checkAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async readSamples(_options) {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n async saveSample(_options) {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n async openHealthConnectSettings() {\n // No-op on web - Health Connect is Android only\n }\n async showPrivacyPolicy() {\n // No-op on web - Health Connect privacy policy is Android only\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,MAAM,GAAGA,mBAAc,CAAC,QAAQ,EAAE;AACxC,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;AAC7D,CAAC;;ACFM,MAAM,SAAS,SAASC,cAAS,CAAC;AACzC,IAAI,MAAM,WAAW,GAAG;AACxB,QAAQ,OAAO;AACf,YAAY,SAAS,EAAE,KAAK;AAC5B,YAAY,QAAQ,EAAE,KAAK;AAC3B,YAAY,MAAM,EAAE,iEAAiE;AACrF,SAAS;AACT,IAAI;AACJ,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;AACzC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,EAAE;AACvC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;AAChC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,gBAAgB,GAAG;AAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;AACjC,IAAI;AACJ,IAAI,MAAM,yBAAyB,GAAG;AACtC;AACA,IAAI;AACJ,IAAI,MAAM,iBAAiB,GAAG;AAC9B;AACA,IAAI;AACJ;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst Health = registerPlugin('Health', {\n web: () => import('./web').then((m) => new m.HealthWeb()),\n});\nexport * from './definitions';\nexport { Health };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HealthWeb extends WebPlugin {\n async isAvailable() {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n async requestAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async checkAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async readSamples(_options) {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n async saveSample(_options) {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n async openHealthConnectSettings() {\n // No-op on web - Health Connect is Android only\n }\n async showPrivacyPolicy() {\n // No-op on web - Health Connect privacy policy is Android only\n }\n async queryWorkouts(_options) {\n throw this.unimplemented('Querying workouts is only available on native platforms.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,MAAM,GAAGA,mBAAc,CAAC,QAAQ,EAAE;AACxC,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;AAC7D,CAAC;;ACFM,MAAM,SAAS,SAASC,cAAS,CAAC;AACzC,IAAI,MAAM,WAAW,GAAG;AACxB,QAAQ,OAAO;AACf,YAAY,SAAS,EAAE,KAAK;AAC5B,YAAY,QAAQ,EAAE,KAAK;AAC3B,YAAY,MAAM,EAAE,iEAAiE;AACrF,SAAS;AACT,IAAI;AACJ,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;AACzC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,EAAE;AACvC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;AAChC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,MAAM,gBAAgB,GAAG;AAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;AACjC,IAAI;AACJ,IAAI,MAAM,yBAAyB,GAAG;AACtC;AACA,IAAI;AACJ,IAAI,MAAM,iBAAiB,GAAG;AAC9B;AACA,IAAI;AACJ,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;AAClC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,0DAA0D,CAAC;AAC5F,IAAI;AACJ;;;;;;;;;"}
|
package/dist/plugin.js
CHANGED
|
@@ -34,6 +34,9 @@ var capacitorHealth = (function (exports, core) {
|
|
|
34
34
|
async showPrivacyPolicy() {
|
|
35
35
|
// No-op on web - Health Connect privacy policy is Android only
|
|
36
36
|
}
|
|
37
|
+
async queryWorkouts(_options) {
|
|
38
|
+
throw this.unimplemented('Querying workouts is only available on native platforms.');
|
|
39
|
+
}
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
var web = /*#__PURE__*/Object.freeze({
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst Health = registerPlugin('Health', {\n web: () => import('./web').then((m) => new m.HealthWeb()),\n});\nexport * from './definitions';\nexport { Health };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HealthWeb extends WebPlugin {\n async isAvailable() {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n async requestAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async checkAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async readSamples(_options) {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n async saveSample(_options) {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n async openHealthConnectSettings() {\n // No-op on web - Health Connect is Android only\n }\n async showPrivacyPolicy() {\n // No-op on web - Health Connect privacy policy is Android only\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,MAAM,GAAGA,mBAAc,CAAC,QAAQ,EAAE;IACxC,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;IAC7D,CAAC;;ICFM,MAAM,SAAS,SAASC,cAAS,CAAC;IACzC,IAAI,MAAM,WAAW,GAAG;IACxB,QAAQ,OAAO;IACf,YAAY,SAAS,EAAE,KAAK;IAC5B,YAAY,QAAQ,EAAE,KAAK;IAC3B,YAAY,MAAM,EAAE,iEAAiE;IACrF,SAAS;IACT,IAAI;IACJ,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;IACzC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,EAAE;IACvC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;IAChC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;IACjC,IAAI;IACJ,IAAI,MAAM,yBAAyB,GAAG;IACtC;IACA,IAAI;IACJ,IAAI,MAAM,iBAAiB,GAAG;IAC9B;IACA,IAAI;IACJ;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst Health = registerPlugin('Health', {\n web: () => import('./web').then((m) => new m.HealthWeb()),\n});\nexport * from './definitions';\nexport { Health };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HealthWeb extends WebPlugin {\n async isAvailable() {\n return {\n available: false,\n platform: 'web',\n reason: 'Native health APIs are not accessible in a browser environment.',\n };\n }\n async requestAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async checkAuthorization(_options) {\n throw this.unimplemented('Health permissions are only available on native platforms.');\n }\n async readSamples(_options) {\n throw this.unimplemented('Reading health data is only available on native platforms.');\n }\n async saveSample(_options) {\n throw this.unimplemented('Writing health data is only available on native platforms.');\n }\n async getPluginVersion() {\n return { version: 'web' };\n }\n async openHealthConnectSettings() {\n // No-op on web - Health Connect is Android only\n }\n async showPrivacyPolicy() {\n // No-op on web - Health Connect privacy policy is Android only\n }\n async queryWorkouts(_options) {\n throw this.unimplemented('Querying workouts is only available on native platforms.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,MAAM,GAAGA,mBAAc,CAAC,QAAQ,EAAE;IACxC,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;IAC7D,CAAC;;ICFM,MAAM,SAAS,SAASC,cAAS,CAAC;IACzC,IAAI,MAAM,WAAW,GAAG;IACxB,QAAQ,OAAO;IACf,YAAY,SAAS,EAAE,KAAK;IAC5B,YAAY,QAAQ,EAAE,KAAK;IAC3B,YAAY,MAAM,EAAE,iEAAiE;IACrF,SAAS;IACT,IAAI;IACJ,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;IACzC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,kBAAkB,CAAC,QAAQ,EAAE;IACvC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,WAAW,CAAC,QAAQ,EAAE;IAChC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;IACjC,IAAI;IACJ,IAAI,MAAM,yBAAyB,GAAG;IACtC;IACA,IAAI;IACJ,IAAI,MAAM,iBAAiB,GAAG;IAC9B;IACA,IAAI;IACJ,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;IAClC,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,0DAA0D,CAAC;IAC5F,IAAI;IACJ;;;;;;;;;;;;;;;"}
|
|
@@ -27,6 +27,128 @@ enum HealthManagerError: LocalizedError {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
enum WorkoutType: String, CaseIterable {
|
|
31
|
+
case running
|
|
32
|
+
case cycling
|
|
33
|
+
case walking
|
|
34
|
+
case swimming
|
|
35
|
+
case yoga
|
|
36
|
+
case strengthTraining
|
|
37
|
+
case hiking
|
|
38
|
+
case tennis
|
|
39
|
+
case basketball
|
|
40
|
+
case soccer
|
|
41
|
+
case americanFootball
|
|
42
|
+
case baseball
|
|
43
|
+
case crossTraining
|
|
44
|
+
case elliptical
|
|
45
|
+
case rowing
|
|
46
|
+
case stairClimbing
|
|
47
|
+
case traditionalStrengthTraining
|
|
48
|
+
case waterFitness
|
|
49
|
+
case waterPolo
|
|
50
|
+
case waterSports
|
|
51
|
+
case wrestling
|
|
52
|
+
case other
|
|
53
|
+
|
|
54
|
+
func hkWorkoutActivityType() -> HKWorkoutActivityType {
|
|
55
|
+
switch self {
|
|
56
|
+
case .running:
|
|
57
|
+
return .running
|
|
58
|
+
case .cycling:
|
|
59
|
+
return .cycling
|
|
60
|
+
case .walking:
|
|
61
|
+
return .walking
|
|
62
|
+
case .swimming:
|
|
63
|
+
return .swimming
|
|
64
|
+
case .yoga:
|
|
65
|
+
return .yoga
|
|
66
|
+
case .strengthTraining:
|
|
67
|
+
return .traditionalStrengthTraining
|
|
68
|
+
case .hiking:
|
|
69
|
+
return .hiking
|
|
70
|
+
case .tennis:
|
|
71
|
+
return .tennis
|
|
72
|
+
case .basketball:
|
|
73
|
+
return .basketball
|
|
74
|
+
case .soccer:
|
|
75
|
+
return .soccer
|
|
76
|
+
case .americanFootball:
|
|
77
|
+
return .americanFootball
|
|
78
|
+
case .baseball:
|
|
79
|
+
return .baseball
|
|
80
|
+
case .crossTraining:
|
|
81
|
+
return .crossTraining
|
|
82
|
+
case .elliptical:
|
|
83
|
+
return .elliptical
|
|
84
|
+
case .rowing:
|
|
85
|
+
return .rowing
|
|
86
|
+
case .stairClimbing:
|
|
87
|
+
return .stairClimbing
|
|
88
|
+
case .traditionalStrengthTraining:
|
|
89
|
+
return .traditionalStrengthTraining
|
|
90
|
+
case .waterFitness:
|
|
91
|
+
return .waterFitness
|
|
92
|
+
case .waterPolo:
|
|
93
|
+
return .waterPolo
|
|
94
|
+
case .waterSports:
|
|
95
|
+
return .waterSports
|
|
96
|
+
case .wrestling:
|
|
97
|
+
return .wrestling
|
|
98
|
+
case .other:
|
|
99
|
+
return .other
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static func fromHKWorkoutActivityType(_ hkType: HKWorkoutActivityType) -> WorkoutType {
|
|
104
|
+
switch hkType {
|
|
105
|
+
case .running:
|
|
106
|
+
return .running
|
|
107
|
+
case .cycling:
|
|
108
|
+
return .cycling
|
|
109
|
+
case .walking:
|
|
110
|
+
return .walking
|
|
111
|
+
case .swimming:
|
|
112
|
+
return .swimming
|
|
113
|
+
case .yoga:
|
|
114
|
+
return .yoga
|
|
115
|
+
case .traditionalStrengthTraining:
|
|
116
|
+
// Map back to strengthTraining for consistency (both map to the same HK type)
|
|
117
|
+
return .strengthTraining
|
|
118
|
+
case .hiking:
|
|
119
|
+
return .hiking
|
|
120
|
+
case .tennis:
|
|
121
|
+
return .tennis
|
|
122
|
+
case .basketball:
|
|
123
|
+
return .basketball
|
|
124
|
+
case .soccer:
|
|
125
|
+
return .soccer
|
|
126
|
+
case .americanFootball:
|
|
127
|
+
return .americanFootball
|
|
128
|
+
case .baseball:
|
|
129
|
+
return .baseball
|
|
130
|
+
case .crossTraining:
|
|
131
|
+
return .crossTraining
|
|
132
|
+
case .elliptical:
|
|
133
|
+
return .elliptical
|
|
134
|
+
case .rowing:
|
|
135
|
+
return .rowing
|
|
136
|
+
case .stairClimbing:
|
|
137
|
+
return .stairClimbing
|
|
138
|
+
case .waterFitness:
|
|
139
|
+
return .waterFitness
|
|
140
|
+
case .waterPolo:
|
|
141
|
+
return .waterPolo
|
|
142
|
+
case .waterSports:
|
|
143
|
+
return .waterSports
|
|
144
|
+
case .wrestling:
|
|
145
|
+
return .wrestling
|
|
146
|
+
default:
|
|
147
|
+
return .other
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
30
152
|
enum HealthDataType: String, CaseIterable {
|
|
31
153
|
case steps
|
|
32
154
|
case distance
|
|
@@ -319,44 +441,40 @@ final class Health {
|
|
|
319
441
|
return
|
|
320
442
|
}
|
|
321
443
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
var denied: [HealthDataType] = []
|
|
444
|
+
let group = DispatchGroup()
|
|
445
|
+
let lock = NSLock()
|
|
446
|
+
var authorized: [HealthDataType] = []
|
|
447
|
+
var denied: [HealthDataType] = []
|
|
327
448
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
449
|
+
for type in types {
|
|
450
|
+
guard let objectType = try? type.sampleType() else {
|
|
451
|
+
denied.append(type)
|
|
452
|
+
continue
|
|
453
|
+
}
|
|
333
454
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
455
|
+
group.enter()
|
|
456
|
+
let readSet = Set<HKObjectType>([objectType])
|
|
457
|
+
healthStore.getRequestStatusForAuthorization(toShare: Set<HKSampleType>(), read: readSet) { status, error in
|
|
458
|
+
defer { group.leave() }
|
|
338
459
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
460
|
+
if error != nil {
|
|
461
|
+
lock.lock(); denied.append(type); lock.unlock()
|
|
462
|
+
return
|
|
463
|
+
}
|
|
343
464
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
465
|
+
switch status {
|
|
466
|
+
case .unnecessary:
|
|
467
|
+
lock.lock(); authorized.append(type); lock.unlock()
|
|
468
|
+
case .shouldRequest, .unknown:
|
|
469
|
+
lock.lock(); denied.append(type); lock.unlock()
|
|
470
|
+
@unknown default:
|
|
471
|
+
lock.lock(); denied.append(type); lock.unlock()
|
|
352
472
|
}
|
|
353
473
|
}
|
|
474
|
+
}
|
|
354
475
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
} else {
|
|
359
|
-
completion(types, [])
|
|
476
|
+
group.notify(queue: .main) {
|
|
477
|
+
completion(authorized, denied)
|
|
360
478
|
}
|
|
361
479
|
}
|
|
362
480
|
|
|
@@ -406,6 +524,8 @@ final class Health {
|
|
|
406
524
|
let type = try dataType.sampleType()
|
|
407
525
|
set.insert(type)
|
|
408
526
|
}
|
|
527
|
+
// Always include workout type for read access to enable workout queries
|
|
528
|
+
set.insert(HKObjectType.workoutType())
|
|
409
529
|
return set
|
|
410
530
|
}
|
|
411
531
|
|
|
@@ -417,4 +537,92 @@ final class Health {
|
|
|
417
537
|
}
|
|
418
538
|
return set
|
|
419
539
|
}
|
|
540
|
+
|
|
541
|
+
func queryWorkouts(workoutTypeString: String?, startDateString: String?, endDateString: String?, limit: Int?, ascending: Bool, completion: @escaping (Result<[[String: Any]], Error>) -> Void) {
|
|
542
|
+
let startDate = (try? parseDate(startDateString, defaultValue: Date().addingTimeInterval(-86400))) ?? Date().addingTimeInterval(-86400)
|
|
543
|
+
let endDate = (try? parseDate(endDateString, defaultValue: Date())) ?? Date()
|
|
544
|
+
|
|
545
|
+
guard endDate >= startDate else {
|
|
546
|
+
completion(.failure(HealthManagerError.invalidDateRange))
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
var predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [])
|
|
551
|
+
|
|
552
|
+
// Filter by workout type if specified
|
|
553
|
+
if let workoutTypeString = workoutTypeString, let workoutType = WorkoutType(rawValue: workoutTypeString) {
|
|
554
|
+
let hkWorkoutType = workoutType.hkWorkoutActivityType()
|
|
555
|
+
let typePredicate = HKQuery.predicateForWorkouts(with: hkWorkoutType)
|
|
556
|
+
predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, typePredicate])
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: ascending)
|
|
560
|
+
let queryLimit = limit ?? 100
|
|
561
|
+
|
|
562
|
+
guard let workoutSampleType = HKObjectType.workoutType() as? HKSampleType else {
|
|
563
|
+
completion(.failure(HealthManagerError.operationFailed("Workout type is not available.")))
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let query = HKSampleQuery(sampleType: workoutSampleType, predicate: predicate, limit: queryLimit, sortDescriptors: [sortDescriptor]) { [weak self] _, samples, error in
|
|
568
|
+
guard let self = self else { return }
|
|
569
|
+
|
|
570
|
+
if let error = error {
|
|
571
|
+
completion(.failure(error))
|
|
572
|
+
return
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
guard let workouts = samples as? [HKWorkout] else {
|
|
576
|
+
completion(.success([]))
|
|
577
|
+
return
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let results = workouts.map { workout -> [String: Any] in
|
|
581
|
+
var payload: [String: Any] = [
|
|
582
|
+
"workoutType": WorkoutType.fromHKWorkoutActivityType(workout.workoutActivityType).rawValue,
|
|
583
|
+
"duration": Int(workout.duration),
|
|
584
|
+
"startDate": self.isoFormatter.string(from: workout.startDate),
|
|
585
|
+
"endDate": self.isoFormatter.string(from: workout.endDate)
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
// Add total energy burned if available
|
|
589
|
+
if let totalEnergyBurned = workout.totalEnergyBurned {
|
|
590
|
+
let energyInKilocalories = totalEnergyBurned.doubleValue(for: HKUnit.kilocalorie())
|
|
591
|
+
payload["totalEnergyBurned"] = energyInKilocalories
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Add total distance if available
|
|
595
|
+
if let totalDistance = workout.totalDistance {
|
|
596
|
+
let distanceInMeters = totalDistance.doubleValue(for: HKUnit.meter())
|
|
597
|
+
payload["totalDistance"] = distanceInMeters
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Add source information
|
|
601
|
+
let source = workout.sourceRevision.source
|
|
602
|
+
payload["sourceName"] = source.name
|
|
603
|
+
payload["sourceId"] = source.bundleIdentifier
|
|
604
|
+
|
|
605
|
+
// Add metadata if available
|
|
606
|
+
if let metadata = workout.metadata, !metadata.isEmpty {
|
|
607
|
+
var metadataDict: [String: String] = [:]
|
|
608
|
+
for (key, value) in metadata {
|
|
609
|
+
if let stringValue = value as? String {
|
|
610
|
+
metadataDict[key] = stringValue
|
|
611
|
+
} else if let numberValue = value as? NSNumber {
|
|
612
|
+
metadataDict[key] = numberValue.stringValue
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if !metadataDict.isEmpty {
|
|
616
|
+
payload["metadata"] = metadataDict
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return payload
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
completion(.success(results))
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
healthStore.execute(query)
|
|
627
|
+
}
|
|
420
628
|
}
|
|
@@ -3,7 +3,7 @@ import Capacitor
|
|
|
3
3
|
|
|
4
4
|
@objc(HealthPlugin)
|
|
5
5
|
public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
6
|
-
private let pluginVersion: String = "8.
|
|
6
|
+
private let pluginVersion: String = "8.2.0"
|
|
7
7
|
public let identifier = "HealthPlugin"
|
|
8
8
|
public let jsName = "Health"
|
|
9
9
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -14,7 +14,8 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
14
14
|
CAPPluginMethod(name: "saveSample", returnType: CAPPluginReturnPromise),
|
|
15
15
|
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
|
|
16
16
|
CAPPluginMethod(name: "openHealthConnectSettings", returnType: CAPPluginReturnPromise),
|
|
17
|
-
CAPPluginMethod(name: "showPrivacyPolicy", returnType: CAPPluginReturnPromise)
|
|
17
|
+
CAPPluginMethod(name: "showPrivacyPolicy", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "queryWorkouts", returnType: CAPPluginReturnPromise)
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
private let implementation = Health()
|
|
@@ -146,4 +147,29 @@ public class HealthPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
146
147
|
call.resolve()
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
@objc func queryWorkouts(_ call: CAPPluginCall) {
|
|
151
|
+
let workoutType = call.getString("workoutType")
|
|
152
|
+
let startDate = call.getString("startDate")
|
|
153
|
+
let endDate = call.getString("endDate")
|
|
154
|
+
let limit = call.getInt("limit")
|
|
155
|
+
let ascending = call.getBool("ascending") ?? false
|
|
156
|
+
|
|
157
|
+
implementation.queryWorkouts(
|
|
158
|
+
workoutTypeString: workoutType,
|
|
159
|
+
startDateString: startDate,
|
|
160
|
+
endDateString: endDate,
|
|
161
|
+
limit: limit,
|
|
162
|
+
ascending: ascending
|
|
163
|
+
) { result in
|
|
164
|
+
DispatchQueue.main.async {
|
|
165
|
+
switch result {
|
|
166
|
+
case let .success(workouts):
|
|
167
|
+
call.resolve(["workouts": workouts])
|
|
168
|
+
case let .failure(error):
|
|
169
|
+
call.reject(error.localizedDescription, nil, error)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
149
175
|
}
|
package/package.json
CHANGED