@stacksjs/mobile 0.75.25 → 0.75.26
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 +27 -1
- package/dist/index.d.ts +62 -1
- package/dist/index.js +1 -1
- package/dist/route.d.ts +60 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -76,6 +76,33 @@ const hrv = await health.getDailyStatistics('heartRateVariability', { startDate:
|
|
|
76
76
|
|
|
77
77
|
Both are iOS only for now; on Android they reject as unavailable.
|
|
78
78
|
|
|
79
|
+
## Recording a route
|
|
80
|
+
|
|
81
|
+
Craft records the route natively: fixes go to disk, keep coming with the screen
|
|
82
|
+
locked (`capabilities.backgroundLocation`), and survive the app being closed.
|
|
83
|
+
`createRouteRecorder` drives that recording and reads it back every few seconds
|
|
84
|
+
while the page is in front, turning it into the numbers a runner watches:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createRouteRecorder, location, paceLabel } from '@stacksjs/mobile'
|
|
88
|
+
|
|
89
|
+
const recorder = createRouteRecorder({
|
|
90
|
+
location,
|
|
91
|
+
onUpdate: stats => console.log(stats.distanceM, paceLabel(stats.paceSPerKm)),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
await recorder.attach() ?? await recorder.start() // pick up a recording that outlived the app
|
|
95
|
+
await recorder.pause()
|
|
96
|
+
await recorder.resume()
|
|
97
|
+
const fixes = await recorder.stop() // every fix, for the server to keep
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`routeStats(fixes)` is the arithmetic on its own: distance counted from an
|
|
101
|
+
anchor so GPS drift while standing still adds nothing, fixes less accurate than
|
|
102
|
+
25 m left out, a gap of more than `ROUTE_GAP_S` (a pause) not counted as ground
|
|
103
|
+
covered, moving time, and pace over the last 30 seconds and the whole route.
|
|
104
|
+
`paceLabel` and `speedLabel` format min/km and km/h.
|
|
105
|
+
|
|
79
106
|
## Before the bridge arrives
|
|
80
107
|
|
|
81
108
|
Craft installs `window.craft` once the page has finished loading, so
|
|
@@ -105,4 +132,3 @@ The phone-native behaviour a screen expects, kept out of STX templates:
|
|
|
105
132
|
|
|
106
133
|
The framework's `NativeNavBar`, `NativePullToRefresh`, `NativeSegmentedControl`
|
|
107
134
|
and `NativeSheet` components are built on these.
|
|
108
|
-
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bundled TypeScript declarations
|
|
3
|
-
* Generated from
|
|
3
|
+
* Generated from 5 source files
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
|
|
@@ -122,6 +122,67 @@ export declare function deepLinkPath(link: string, origin?: string | undefined):
|
|
|
122
122
|
*/
|
|
123
123
|
export declare function followDeepLinks(go: (path: string) => void, links: DeepLinksApi): () => void;
|
|
124
124
|
|
|
125
|
+
// From: src/route.ts
|
|
126
|
+
/** A fix as Craft records it. */
|
|
127
|
+
export declare interface RouteFix extends Location {
|
|
128
|
+
altitudeAccuracy?: number
|
|
129
|
+
}
|
|
130
|
+
export declare interface RouteStats {
|
|
131
|
+
distanceM: number
|
|
132
|
+
movingS: number
|
|
133
|
+
paceSPerKm: number | null
|
|
134
|
+
averagePaceSPerKm: number | null
|
|
135
|
+
speedMps: number | null
|
|
136
|
+
accuracyM: number | null
|
|
137
|
+
fixes: number
|
|
138
|
+
}
|
|
139
|
+
export declare interface RouteStatsOptions {
|
|
140
|
+
maxAccuracyM?: number
|
|
141
|
+
paceWindowS?: number
|
|
142
|
+
}
|
|
143
|
+
/** Fixes further apart than this, seconds, are a break in the route (a pause). */
|
|
144
|
+
export declare const ROUTE_GAP_S: unknown;
|
|
145
|
+
/** Great-circle distance between two fixes, metres. */
|
|
146
|
+
export declare function fixDistance(a: { latitude: number, longitude: number }, b: { latitude: number, longitude: number }): number;
|
|
147
|
+
/** The fixes worth counting, oldest first: a known position, accurate enough. */
|
|
148
|
+
export declare function usableFixes<T extends RouteFix>(fixes: readonly T[], maxAccuracyM?: number): T[];
|
|
149
|
+
/**
|
|
150
|
+
* The live numbers for a recording.
|
|
151
|
+
*
|
|
152
|
+
* Standing still, GPS wanders a few metres about the true spot, and summing
|
|
153
|
+
* every hop would add distance nobody covered. So distance is counted from an
|
|
154
|
+
* anchor: a fix only counts once it is further from the last counted one than
|
|
155
|
+
* the signal could explain (the larger of 3 m and the fixes' accuracy, capped
|
|
156
|
+
* at 10 m), and then becomes the anchor. Fixes more than ROUTE_GAP_S apart
|
|
157
|
+
* (a pause) are a break: the ground between them is not counted.
|
|
158
|
+
*/
|
|
159
|
+
export declare function routeStats(fixes: readonly RouteFix[], options?: RouteStatsOptions): RouteStats;
|
|
160
|
+
/** "5:12" per kilometre, or "–" without a pace. */
|
|
161
|
+
export declare function paceLabel(secondsPerKm: number | null | undefined): string;
|
|
162
|
+
/** "24.3" kilometres per hour. */
|
|
163
|
+
export declare function speedLabel(metresPerSecond: number | null | undefined): string;
|
|
164
|
+
export declare interface RouteRecorderOptions extends RouteStatsOptions {
|
|
165
|
+
intervalMs?: number
|
|
166
|
+
onUpdate?: (stats: RouteStats) => void
|
|
167
|
+
location: LocationApi
|
|
168
|
+
locationOptions?: LocationOptions
|
|
169
|
+
}
|
|
170
|
+
export declare interface RouteRecorder {
|
|
171
|
+
start: () => Promise<RouteStats>
|
|
172
|
+
attach: () => Promise<LocationRecordingState | null>
|
|
173
|
+
pause: () => Promise<void>
|
|
174
|
+
resume: () => Promise<void>
|
|
175
|
+
stop: () => Promise<RouteFix[]>
|
|
176
|
+
refresh: () => Promise<RouteStats>
|
|
177
|
+
readonly stats: RouteStats
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A recording to follow: start, pause, resume and stop the native recording,
|
|
181
|
+
* and the live numbers read back from it every few seconds while the page is
|
|
182
|
+
* in front. Reading pauses with the page hidden; the recording does not.
|
|
183
|
+
*/
|
|
184
|
+
export declare function createRouteRecorder(options: RouteRecorderOptions): RouteRecorder;
|
|
185
|
+
|
|
125
186
|
// From: src/types.ts
|
|
126
187
|
/** Enabled native features, not the hardware report from device.getCapabilities(). */
|
|
127
188
|
export declare interface CraftMobileCapabilities {
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{appReview as
|
|
1
|
+
import{appReview as O,biometrics as U,camera as _,deepLinks as M,device as B,haptics as I,health as Y,keepAwake as K,lifecycle as j,liveActivities as q,location as G,network as W,notifications as z,permissions as J,pushNotifications as Q,secureStorage as V,share as X,watchConnectivity as Z}from"craft-native/mobile";import{haptics as E}from"craft-native/mobile";function w(){if(typeof window>"u")return;return window}function v(e=w()){if(!e)return 0;if(typeof e.scrollY==="number")return e.scrollY;return e.document?.scrollingElement?.scrollTop??0}function Ae(e,t=w()){if(!t)return()=>{};let r=!1,n=typeof requestAnimationFrame==="function"?requestAnimationFrame:(o)=>setTimeout(o,16),i=()=>{if(r)return;r=!0,n(()=>{r=!1,e(v(t))})};return e(v(t)),t.addEventListener("scroll",i,{passive:!0}),()=>t.removeEventListener("scroll",i)}function H(e,t){let r=Math.max(0,-e),n=e<=0?Math.max(0,t)*0.5:0;return Math.round(Math.max(r,n))}function Re(e){let t=e.host??w();if(!t)return()=>{};let r=e.threshold??72,n=null,i=!1,o=!1,s=(a)=>e.onPull?.(a,i),d=(a)=>{let f=a.touches?.[0];return f?f.clientY:null},u=(a)=>{if(o||v(t)>0)return;n=d(a),i=!1},l=(a)=>{if(n===null)return;let f=d(a);if(f===null)return;let p=H(v(t),f-n);if(!i&&p>=r)i=!0,Promise.resolve(E.impact("medium")).catch(()=>{});else if(i&&p<r*0.8)i=!1;s(p)},m=async()=>{if(n===null)return;if(n=null,!i){s(0);return}i=!1,o=!0,e.onPull?.(r,!1);try{await e.onRefresh()}finally{o=!1,s(0)}},c=()=>{if(n===null)return;n=null,i=!1,s(0)};return t.addEventListener("touchstart",u,{passive:!0}),t.addEventListener("touchmove",l,{passive:!0}),t.addEventListener("touchend",m),t.addEventListener("touchcancel",c),()=>{t.removeEventListener("touchstart",u),t.removeEventListener("touchmove",l),t.removeEventListener("touchend",m),t.removeEventListener("touchcancel",c)}}function R(){if(typeof window>"u")return;return window}function T(e=R()){let t=e?.history?.state;return t!==null&&t!==void 0}function Le(e=R()){if(!T(e))return!1;return e.history.back(),!0}function C(e,t=N()){let r;try{r=new URL(e)}catch{return null}let n=`${r.search}${r.hash}`;if(r.protocol==="http:"||r.protocol==="https:"){if(!t||r.origin!==t)return null;return`${r.pathname||"/"}${n}`}return`${`/${[r.host,r.pathname.replace(/^\/+/,"")].filter(Boolean).join("/")}`}${n}`}function N(){if(typeof window>"u")return;return window.location?.origin}var A="stacks-mobile-deep-link";function Me(e,t){let r=(n)=>{let i=C(n);if(i)e(i)};return t.getInitialURL().then((n)=>{if(!n)return;try{if(sessionStorage.getItem(A)===n)return;sessionStorage.setItem(A,n)}catch{}r(n)}).catch(()=>{}),t.onLink((n,i)=>{if(i?.initial)return;r(n)})}var Pe=30;function F(e,t){let r=Math.PI/180,n=(t.latitude-e.latitude)*r,i=(t.longitude-e.longitude)*r,o=Math.sin(n/2)**2+Math.cos(e.latitude*r)*Math.cos(t.latitude*r)*Math.sin(i/2)**2;return 12742017.6*Math.asin(Math.min(1,Math.sqrt(o)))}function D(e,t=25){let r=new Set;return e.filter((n)=>Number.isFinite(n.latitude)&&Number.isFinite(n.longitude)&&Number.isFinite(n.timestamp)&&n.accuracy>=0&&n.accuracy<=t).sort((n,i)=>n.timestamp-i.timestamp).filter((n)=>{if(r.has(n.timestamp))return!1;return r.add(n.timestamp),!0})}function S(e,t={}){let r=D(e,t.maxAccuracyM??25),n=t.paceWindowS??30,i=[],o=0,s=0,d=null;for(let c of r){let a=i[i.length-1],f=d?(c.timestamp-d.timestamp)/1000:0;if(d=c,!a){i.push({fix:c,cumM:0});continue}if(f>30){i.push({fix:c,cumM:o});continue}let p=(c.timestamp-a.fix.timestamp)/1000,h=F(a.fix,c),P=Math.min(10,Math.max(3,a.fix.accuracy,c.accuracy));if(h<P)continue;if(o+=h,p>0&&h/p>=0.5)s+=p;i.push({fix:c,cumM:o})}let u=i[i.length-1],l=null,m=null;if(u){let c=u.fix.timestamp-n*1000,a=i.find((h)=>h.fix.timestamp>=c)??u,f=u.cumM-a.cumM,p=(u.fix.timestamp-a.fix.timestamp)/1000;if(f>=20&&p>0)m=f/p,l=p/(f/1000)}return{distanceM:o,movingS:s,paceSPerKm:l,averagePaceSPerKm:o>=50&&s>0?s/(o/1000):null,speedMps:m,accuracyM:r.length?r[r.length-1].accuracy:e.length?e[e.length-1].accuracy??null:null,fixes:r.length}}function Ee(e){if(!e||!Number.isFinite(e)||e>3600)return"–";let t=Math.round(e);return`${Math.floor(t/60)}:${String(t%60).padStart(2,"0")}`}function He(e){return e&&Number.isFinite(e)?(e*3.6).toFixed(1):"–"}var L={distanceM:0,movingS:0,paceSPerKm:null,averagePaceSPerKm:null,speedMps:null,accuracyM:null,fixes:0};function Te(e){let t=e.location,r=e.intervalMs??3000,n=L,i=null,o=()=>typeof document<"u"&&document.visibilityState==="hidden";async function s(){let l=await t.readRecording();return n=S(l,e),e.onUpdate?.(n),n}function d(){u(),i=setInterval(()=>{if(!o())s().catch(()=>{})},r)}function u(){if(i)clearInterval(i);i=null}return{get stats(){return n},async start(){return await t.startRecording({enableHighAccuracy:!0,...e.locationOptions}),n=L,e.onUpdate?.(n),d(),n},async attach(){let l=await t.getRecordingState().catch(()=>null);if(!l?.active)return null;if(await s().catch(()=>{}),!l.paused)d();return l},async pause(){u(),await t.pauseRecording(),await s().catch(()=>{})},async resume(){await t.resumeRecording(),d()},async stop(){u();let m=(await t.stopRecording())?.locations??[];return n=S(m,e),e.onUpdate?.(n),m},refresh:s}}var ee=U,te=_,ne=B,y=I,re=j,ie=G,oe=z,ae=J,se=V,ce=X,ue=O;function k(e){if(typeof e==="string")return e.trim()||null;if(!e||typeof e!=="object")return null;let t=e.url;return typeof t==="string"&&t.trim()?t:null}var le={async getInitialURL(){return k(await M.getInitialURL?.())},onLink(e){return M.onLink?.((t,r)=>{let n=k(t);if(n)e(n,{initial:r?.initial===!0})})??(()=>{})}},fe=K,pe=W,de=Q,me=Y,he=q,ve=Z;function x(){if(typeof window>"u")return;return window}function b(){let e=x()?.craft;if(!e||typeof e!=="object")return null;if(!("platform"in e)||e.platform!=="ios"&&e.platform!=="android")return null;let t="capabilities"in e?e.capabilities:void 0,r=t&&typeof t==="object"&&!Array.isArray(t)?Object.fromEntries(Object.entries(t).filter(([,n])=>typeof n==="boolean")):{};return{platform:e.platform,capabilities:r}}function g(){return b()!==null}function ye(){if(typeof window>"u")return!1;let e=window;if(e.CraftAndroid)return!0;let t=typeof navigator>"u"?"":navigator.userAgent;return Boolean(e.webkit?.messageHandlers?.craft)&&/iPhone|iPad|iPod/.test(t)}function be(e=2000){if(g()||ye())return Promise.resolve(!0);let t=x(),r=t;if(!t||!(r?.craft||r?.webkit?.messageHandlers?.craft))return Promise.resolve(!1);return new Promise((n)=>{let i=()=>{clearTimeout(o),t.removeEventListener("craftReady",i),n(g())},o=setTimeout(i,e);t.addEventListener("craftReady",i,{once:!0})})}function we(e){let t=x();if(!t)return()=>{};if(b()){let n=!1;return queueMicrotask(()=>{if(!n)e(new Event("craftReady"))}),()=>{n=!0}}let r=(n)=>{if(!b())return;t.removeEventListener("craftReady",r),e(n)};return t.addEventListener("craftReady",r),()=>t.removeEventListener("craftReady",r)}async function ge(e){await y.impact("light");try{let t=await e();return await y.notification("success"),t}catch(t){throw await y.notification("error"),t}}var Fe={get nativeBridge(){return b()},biometrics:ee,camera:te,device:ne,haptics:y,lifecycle:re,location:ie,notifications:oe,permissions:ae,secureStorage:se,share:ce,appReview:ue,deepLinks:le,keepAwake:fe,network:pe,pushNotifications:de,health:me,liveActivities:he,watchConnectivity:ve,isNativeMobile:g,whenNative:be,onReady:we,withFeedback:ge};export{Pe as ROUTE_GAP_S,ue as appReview,ee as biometrics,te as camera,T as canGoBack,Te as createRouteRecorder,C as deepLinkPath,le as deepLinks,ne as device,F as fixDistance,Me as followDeepLinks,b as getNativeMobileBridge,Le as goBack,y as haptics,ye as hasNativeMobileHost,me as health,g as isNativeMobile,fe as keepAwake,re as lifecycle,he as liveActivities,ie as location,Fe as mobile,pe as network,k as normalizeDeepLinkURL,oe as notifications,Ae as observePageScroll,Re as observePullToRefresh,we as onMobileReady,Ee as paceLabel,v as pageScrollTop,ae as permissions,H as pullDistance,de as pushNotifications,S as routeStats,se as secureStorage,ce as share,He as speedLabel,D as usableFixes,ve as watchConnectivity,be as whenNativeMobile,ge as withNativeFeedback};
|
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Location, LocationApi, LocationOptions, LocationRecordingState } from './types';
|
|
2
|
+
/** Great-circle distance between two fixes, metres. */
|
|
3
|
+
export declare function fixDistance(a: { latitude: number, longitude: number }, b: { latitude: number, longitude: number }): number;
|
|
4
|
+
/** The fixes worth counting, oldest first: a known position, accurate enough. */
|
|
5
|
+
export declare function usableFixes<T extends RouteFix>(fixes: readonly T[], maxAccuracyM?: number): T[];
|
|
6
|
+
/**
|
|
7
|
+
* The live numbers for a recording.
|
|
8
|
+
*
|
|
9
|
+
* Standing still, GPS wanders a few metres about the true spot, and summing
|
|
10
|
+
* every hop would add distance nobody covered. So distance is counted from an
|
|
11
|
+
* anchor: a fix only counts once it is further from the last counted one than
|
|
12
|
+
* the signal could explain (the larger of 3 m and the fixes' accuracy, capped
|
|
13
|
+
* at 10 m), and then becomes the anchor. Fixes more than ROUTE_GAP_S apart
|
|
14
|
+
* (a pause) are a break: the ground between them is not counted.
|
|
15
|
+
*/
|
|
16
|
+
export declare function routeStats(fixes: readonly RouteFix[], options?: RouteStatsOptions): RouteStats;
|
|
17
|
+
/** "5:12" per kilometre, or "–" without a pace. */
|
|
18
|
+
export declare function paceLabel(secondsPerKm: number | null | undefined): string;
|
|
19
|
+
/** "24.3" kilometres per hour. */
|
|
20
|
+
export declare function speedLabel(metresPerSecond: number | null | undefined): string;
|
|
21
|
+
/**
|
|
22
|
+
* A recording to follow: start, pause, resume and stop the native recording,
|
|
23
|
+
* and the live numbers read back from it every few seconds while the page is
|
|
24
|
+
* in front. Reading pauses with the page hidden; the recording does not.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createRouteRecorder(options: RouteRecorderOptions): RouteRecorder;
|
|
27
|
+
/** Fixes further apart than this, seconds, are a break in the route (a pause). */
|
|
28
|
+
export declare const ROUTE_GAP_S: 30;
|
|
29
|
+
/** A fix as Craft records it. */
|
|
30
|
+
export declare interface RouteFix extends Location {
|
|
31
|
+
altitudeAccuracy?: number
|
|
32
|
+
}
|
|
33
|
+
export declare interface RouteStats {
|
|
34
|
+
distanceM: number
|
|
35
|
+
movingS: number
|
|
36
|
+
paceSPerKm: number | null
|
|
37
|
+
averagePaceSPerKm: number | null
|
|
38
|
+
speedMps: number | null
|
|
39
|
+
accuracyM: number | null
|
|
40
|
+
fixes: number
|
|
41
|
+
}
|
|
42
|
+
export declare interface RouteStatsOptions {
|
|
43
|
+
maxAccuracyM?: number
|
|
44
|
+
paceWindowS?: number
|
|
45
|
+
}
|
|
46
|
+
export declare interface RouteRecorderOptions extends RouteStatsOptions {
|
|
47
|
+
intervalMs?: number
|
|
48
|
+
onUpdate?: (stats: RouteStats) => void
|
|
49
|
+
location: LocationApi
|
|
50
|
+
locationOptions?: LocationOptions
|
|
51
|
+
}
|
|
52
|
+
export declare interface RouteRecorder {
|
|
53
|
+
start: () => Promise<RouteStats>
|
|
54
|
+
attach: () => Promise<LocationRecordingState | null>
|
|
55
|
+
pause: () => Promise<void>
|
|
56
|
+
resume: () => Promise<void>
|
|
57
|
+
stop: () => Promise<RouteFix[]>
|
|
58
|
+
refresh: () => Promise<RouteStats>
|
|
59
|
+
readonly stats: RouteStats
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/mobile",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.26",
|
|
6
6
|
"description": "Craft-powered native mobile runtime for Stacks and STX applications",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"craft-native": ">=0.0.103"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@stacksjs/build": "0.75.
|
|
38
|
+
"@stacksjs/build": "0.75.26",
|
|
39
39
|
"better-dx": "^0.2.24"
|
|
40
40
|
}
|
|
41
41
|
}
|