@octane-xplat/platform 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +37 -0
  2. package/package.json +21 -8
  3. package/src/a11y.native.ts +9 -5
  4. package/src/a11y.web.ts +13 -9
  5. package/src/app-info.native.ts +35 -0
  6. package/src/app-info.web.ts +10 -0
  7. package/src/biometrics.native.ts +10 -10
  8. package/src/biometrics.web.ts +9 -17
  9. package/src/clipboard.native.ts +28 -15
  10. package/src/clipboard.web.ts +29 -17
  11. package/src/connectivity.native.ts +86 -0
  12. package/src/connectivity.web.ts +62 -0
  13. package/src/deep-links.native.ts +47 -20
  14. package/src/deep-links.web.ts +16 -11
  15. package/src/device.native.ts +3 -3
  16. package/src/device.web.ts +5 -4
  17. package/src/files.native.ts +122 -16
  18. package/src/files.web.ts +20 -19
  19. package/src/geolocation.native.ts +44 -0
  20. package/src/geolocation.web.ts +50 -0
  21. package/src/haptics.native.ts +14 -10
  22. package/src/haptics.web.ts +5 -4
  23. package/src/index.ts +49 -22
  24. package/src/lifecycle.native.ts +1 -1
  25. package/src/lifecycle.native.tsrx +2 -0
  26. package/src/lifecycle.web.ts +1 -1
  27. package/src/lifecycle.web.tsrx +1 -0
  28. package/src/locale.native.ts +3 -3
  29. package/src/locale.web.ts +5 -9
  30. package/src/media.native.ts +74 -14
  31. package/src/media.web.ts +71 -9
  32. package/src/notifications.native.ts +7 -7
  33. package/src/notifications.web.ts +18 -7
  34. package/src/open-url.native.ts +45 -0
  35. package/src/open-url.web.ts +15 -0
  36. package/src/permissions.native.ts +10 -10
  37. package/src/permissions.web.ts +13 -31
  38. package/src/safe-area.native.ts +1 -1
  39. package/src/safe-area.native.tsrx +76 -17
  40. package/src/safe-area.web.ts +1 -1
  41. package/src/safe-area.web.tsrx +3 -0
  42. package/src/screen.native.ts +1 -1
  43. package/src/screen.native.tsrx +1 -0
  44. package/src/screen.web.ts +1 -1
  45. package/src/screen.web.tsrx +1 -0
  46. package/src/secure-storage.native.ts +5 -5
  47. package/src/secure-storage.web.ts +2 -2
  48. package/src/share.native.ts +6 -6
  49. package/src/share.web.ts +17 -13
  50. package/src/storage.native.ts +8 -4
  51. package/src/storage.web.ts +7 -3
  52. package/src/system-bars.native.ts +10 -8
  53. package/src/system-bars.web.ts +9 -6
  54. package/src/types.ts +101 -32
package/src/media.web.ts CHANGED
@@ -1,10 +1,72 @@
1
- // Media picking — web leaf. <input type=file accept="image/*"> → FileRef
2
- // (object URL). Multi-select stays out of v1.
3
- import type { FileRef } from './types';
4
- import { files } from './files';
5
-
6
- export const media = {
7
- async pickImage(): Promise<FileRef | null> {
8
- return files.pick('image/*');
1
+ // Media picking — web leaf. Keep an object URL for previews and a data URL for
2
+ // APIs that persist image payloads. The file input supports multiple photos.
3
+ import type { MediaImpl, MediaPermissionKind, PermissionResult, PickedImage } from './types'
4
+
5
+ function selectFiles(multiple: boolean): Promise<File[]> {
6
+ return new Promise((resolve) => {
7
+ const input = document.createElement('input')
8
+ input.type = 'file'
9
+ input.accept = 'image/*'
10
+ input.multiple = multiple
11
+ input.onchange = () => resolve(Array.from(input.files ?? []))
12
+ input.oncancel = () => resolve([])
13
+ input.click()
14
+ })
15
+ }
16
+
17
+ function readDataUrl(file: File): Promise<string> {
18
+ return new Promise((resolve, reject) => {
19
+ const reader = new FileReader()
20
+ reader.onload = () => {
21
+ if (typeof reader.result === 'string') {
22
+ resolve(reader.result)
23
+ } else {
24
+ reject(new Error('Could not read the selected image'))
25
+ }
26
+ }
27
+
28
+ reader.onerror = () => reject(reader.error ?? new Error('Could not read the selected image'))
29
+
30
+ reader.readAsDataURL(file)
31
+ })
32
+ }
33
+
34
+ async function pickFiles(multiple: boolean): Promise<PickedImage[]> {
35
+ const selected = await selectFiles(multiple)
36
+ return Promise.all(
37
+ selected.map(async (file) => ({
38
+ name: file.name,
39
+ uri: URL.createObjectURL(file),
40
+ dataUrl: await readDataUrl(file),
41
+ })),
42
+ )
43
+ }
44
+
45
+ export const media: MediaImpl = {
46
+ /** Opens the browser image picker; returns null when selection is canceled. */
47
+ async pickImage(): Promise<PickedImage | null> {
48
+ const [image] = await pickFiles(false)
49
+ return image ?? null
50
+ },
51
+ /** Opens the browser image picker with multiple selection enabled. */
52
+ pickImages() {
53
+ return pickFiles(true)
54
+ },
55
+ async ensure(kind: MediaPermissionKind): Promise<PermissionResult> {
56
+ if (kind === 'photos') {
57
+ return typeof document !== 'undefined' ? 'granted' : 'unsupported'
58
+ }
59
+
60
+ if (!navigator.mediaDevices?.getUserMedia) {
61
+ return 'unsupported'
62
+ }
63
+
64
+ try {
65
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true })
66
+ stream.getTracks().forEach((track) => track.stop())
67
+ return 'granted'
68
+ } catch {
69
+ return 'denied'
70
+ }
9
71
  },
10
- };
72
+ }
@@ -1,18 +1,18 @@
1
1
  // Notifications — local notifications via plugin; push (APNs/FCM) is an
2
2
  // app-level concern, not this seam.
3
- import { LocalNotifications } from '@nativescript/local-notifications';
4
- import type { Capability } from './types';
5
- import type { NotificationsImpl } from './types';
3
+ import { LocalNotifications } from '@nativescript/local-notifications'
4
+ import type { Capability } from './types'
5
+ import type { NotificationsImpl } from './types'
6
6
 
7
7
  export const notifications: Capability<NotificationsImpl> = {
8
8
  supported: true,
9
9
  async ensure() {
10
- const granted = await LocalNotifications.requestPermission();
11
- return granted ? 'granted' : 'denied';
10
+ const granted = await LocalNotifications.requestPermission()
11
+ return granted ? 'granted' : 'denied'
12
12
  },
13
13
  impl: {
14
14
  notify(title, body) {
15
- LocalNotifications.schedule([{ id: Date.now() % 2147483647, title, body: body ?? '' }]);
15
+ LocalNotifications.schedule([{ id: Date.now() % 2147483647, title, body: body ?? '' }])
16
16
  },
17
17
  },
18
- };
18
+ }
@@ -1,18 +1,29 @@
1
1
  // Notifications — Web Notification API. ensure() maps permission state;
2
2
  // notify() posts an immediate local notification (no push).
3
- import type { Capability, NotificationsImpl } from './types';
3
+ import type { Capability, NotificationsImpl } from './types'
4
4
 
5
5
  export const notifications: Capability<NotificationsImpl> = {
6
6
  supported: typeof Notification !== 'undefined',
7
7
  async ensure() {
8
- if (typeof Notification === 'undefined') return 'unsupported';
9
- if (Notification.permission === 'granted') return 'granted';
10
- if (Notification.permission === 'denied') return 'denied';
11
- return (await Notification.requestPermission()) === 'granted' ? 'granted' : 'denied';
8
+ if (typeof Notification === 'undefined') {
9
+ return 'unsupported'
10
+ }
11
+
12
+ if (Notification.permission === 'granted') {
13
+ return 'granted'
14
+ }
15
+
16
+ if (Notification.permission === 'denied') {
17
+ return 'denied'
18
+ }
19
+
20
+ return (await Notification.requestPermission()) === 'granted' ? 'granted' : 'denied'
12
21
  },
13
22
  impl: {
14
23
  notify(title, body) {
15
- if (Notification.permission === 'granted') new Notification(title, { body });
24
+ if (Notification.permission === 'granted') {
25
+ new Notification(title, { body })
26
+ }
16
27
  },
17
28
  },
18
- };
29
+ }
@@ -0,0 +1,45 @@
1
+ // Outbound links and app settings — native leaf. Settings use the public
2
+ // per-app settings route on each platform, not private iOS prefs URLs.
3
+ import { Application, Utils } from '@nativescript/core'
4
+ import type { Capability, OpenSettingsImpl } from './types'
5
+
6
+ export function openUrl(url: string): boolean {
7
+ return Utils.openUrl(url)
8
+ }
9
+
10
+ function openSettingsPage(): boolean {
11
+ try {
12
+ if (Application.ios) {
13
+ const settingsUrl = (globalThis as any).UIApplicationOpenSettingsURLString ?? 'app-settings:'
14
+ return Utils.openUrl(settingsUrl)
15
+ }
16
+
17
+ if (Application.android) {
18
+ const activity = Utils.android.getCurrentActivity()
19
+
20
+ if (!activity) {
21
+ return false
22
+ }
23
+
24
+ const intent = new android.content.Intent(
25
+ (android.provider.Settings as any).ACTION_APPLICATION_DETAILS_SETTINGS,
26
+ )
27
+
28
+ intent.setData(android.net.Uri.parse(`package:${Utils.android.getPackageName()}`))
29
+ activity.startActivity(intent)
30
+ return true
31
+ }
32
+ } catch {
33
+ return false
34
+ }
35
+
36
+ return false
37
+ }
38
+
39
+ export const openSettings: Capability<OpenSettingsImpl> = {
40
+ supported: true,
41
+ async ensure() {
42
+ return 'granted'
43
+ },
44
+ impl: { open: openSettingsPage },
45
+ }
@@ -0,0 +1,15 @@
1
+ // Outbound links — web leaf. Settings are an app-level concept and are
2
+ // explicitly unsupported in a browser.
3
+ import type { Capability, OpenSettingsImpl } from './types'
4
+
5
+ export function openUrl(url: string): boolean {
6
+ return window.open(url, '_blank', 'noopener,noreferrer') !== null
7
+ }
8
+
9
+ export const openSettings: Capability<OpenSettingsImpl> = {
10
+ supported: false,
11
+ async ensure() {
12
+ return 'unsupported'
13
+ },
14
+ impl: null,
15
+ }
@@ -1,20 +1,20 @@
1
- // Runtime permissions — native leaf. Maps the shared kind union onto the
2
- // owning plugin's request call; add kinds as services land.
3
- import { LocalNotifications } from '@nativescript/local-notifications';
4
- import type { PermissionKind } from './types';
1
+ // Runtime permissions — native leaf. Delegate each kind to the service that
2
+ // owns its plugin request, so this generic seam cannot drift from reality.
3
+ import { geolocation } from './geolocation'
4
+ import { media } from './media'
5
+ import { notifications } from './notifications'
6
+ import type { PermissionKind } from './types'
5
7
 
6
8
  export const permissions = {
7
9
  async ensure(kind: PermissionKind): Promise<'granted' | 'denied' | 'unsupported'> {
8
10
  switch (kind) {
9
11
  case 'notifications':
10
- return (await LocalNotifications.requestPermission()) ? 'granted' : 'denied';
11
- // camera/photos/location have dedicated plugins — the owning
12
- // service leaf (media, geolocation) owns its own ensure(); this
13
- // generic seam reports them unsupported until wired.
12
+ return notifications.ensure()
14
13
  case 'camera':
15
14
  case 'photos':
15
+ return media.ensure(kind)
16
16
  case 'location':
17
- return 'unsupported';
17
+ return geolocation.ensure()
18
18
  }
19
19
  },
20
- };
20
+ }
@@ -1,38 +1,20 @@
1
- // Runtime permissions — web leaf. Browsers grant most capabilities per-API
2
- // (clipboard/notification have their own ensure()); this generic seam covers
3
- // feature-detection for the rest.
4
- export type PermissionKind = 'notifications' | 'camera' | 'photos' | 'location';
1
+ // Runtime permissions — web leaf. Each kind delegates to its owning service;
2
+ // browser feature detection and prompts therefore stay at the platform edge.
3
+ import { geolocation } from './geolocation'
4
+ import { media } from './media'
5
+ import { notifications } from './notifications'
6
+ import type { PermissionKind } from './types'
5
7
 
6
8
  export const permissions = {
7
9
  async ensure(kind: PermissionKind): Promise<'granted' | 'denied' | 'unsupported'> {
8
10
  switch (kind) {
9
- case 'notifications': {
10
- if (typeof Notification === 'undefined') return 'unsupported';
11
- if (Notification.permission === 'granted') return 'granted';
12
- if (Notification.permission === 'denied') return 'denied';
13
- return (await Notification.requestPermission()) === 'granted' ? 'granted' : 'denied';
14
- }
11
+ case 'notifications':
12
+ return notifications.ensure()
15
13
  case 'camera':
16
- case 'photos': {
17
- if (!navigator.mediaDevices?.getUserMedia) return 'unsupported';
18
- try {
19
- const s = await navigator.mediaDevices.getUserMedia({ video: true });
20
- s.getTracks().forEach((t) => t.stop());
21
- return 'granted';
22
- } catch {
23
- return 'denied';
24
- }
25
- }
26
- case 'location': {
27
- if (!('geolocation' in navigator)) return 'unsupported';
28
- return new Promise((resolve) =>
29
- navigator.geolocation.getCurrentPosition(
30
- () => resolve('granted'),
31
- (e) => resolve(e.code === e.PERMISSION_DENIED ? 'denied' : 'unsupported'),
32
- { timeout: 15000 },
33
- ),
34
- );
35
- }
14
+ case 'photos':
15
+ return media.ensure(kind)
16
+ case 'location':
17
+ return geolocation.ensure()
36
18
  }
37
19
  },
38
- };
20
+ }
@@ -1 +1 @@
1
- export { useSafeAreaInsets } from './safe-area.native.tsrx';
1
+ export { useSafeAreaInsets } from './safe-area.native.tsrx'
@@ -1,27 +1,86 @@
1
- // Safe-area insets — native leaf. Reads the window's safe area after layout;
2
- // falls back to zero before it exists.
3
- import { useEffect, useState } from 'octane';
4
- import type { Insets } from './types';
5
- import { Application } from '@nativescript/core';
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ // Safe-area insets — native leaf. iOS reads UIView.safeAreaInsets; Android
3
+ // reads the foreground activity's root WindowInsets in device-independent px.
4
+ import { useEffect, useState } from 'octane'
5
+ import { Application, Screen } from '@nativescript/core'
6
+ import type { Insets } from './types'
6
7
 
8
+ const zeroInsets = (): Insets => ({ top: 0, bottom: 0, left: 0, right: 0 })
9
+
10
+ function androidActivity(): any {
11
+ return Application.android?.foregroundActivity ?? Application.android?.startActivity
12
+ }
13
+
14
+ function readAndroidInsets(windowInsets?: any): Insets {
15
+ const activity = androidActivity()
16
+ const decorView = activity?.getWindow?.()?.getDecorView?.()
17
+ const insets = windowInsets ?? decorView?.getRootWindowInsets?.()
18
+ if (!insets) return zeroInsets()
19
+
20
+ const scale = Screen.mainScreen.scale || 1
21
+ if (android.os.Build.VERSION.SDK_INT >= 30) {
22
+ const systemBars =
23
+ android.view.WindowInsets.Type.systemBars() | android.view.WindowInsets.Type.displayCutout()
24
+
25
+ const i = insets.getInsets(systemBars)
26
+ return {
27
+ top: i.top / scale,
28
+ bottom: i.bottom / scale,
29
+ left: i.left / scale,
30
+ right: i.right / scale,
31
+ }
32
+ }
33
+
34
+ return {
35
+ top: insets.getSystemWindowInsetTop() / scale,
36
+ bottom: insets.getSystemWindowInsetBottom() / scale,
37
+ left: insets.getSystemWindowInsetLeft() / scale,
38
+ right: insets.getSystemWindowInsetRight() / scale,
39
+ }
40
+ }
7
41
 
8
42
  function readInsets(): Insets {
9
43
  if (Application.ios) {
10
- const v = Application.ios.rootController?.view;
11
- const i = v?.safeAreaInsets;
12
- if (i) return { top: i.top, bottom: i.bottom, left: i.left, right: i.right };
44
+ const i = Application.ios.rootController?.view?.safeAreaInsets
45
+ if (i) return { top: i.top, bottom: i.bottom, left: i.left, right: i.right }
13
46
  }
14
- // Android: WindowInsets need an attached view — return 0 until a screen
15
- // supplies its own; orientation change re-reads.
16
- return { top: 0, bottom: 0, left: 0, right: 0 };
47
+
48
+ if (Application.android) return readAndroidInsets()
49
+ return zeroInsets()
50
+ }
51
+
52
+ function watchAndroidInsets(update: (insets: Insets) => void): () => void {
53
+ const decorView = androidActivity()?.getWindow?.()?.getDecorView?.()
54
+ if (!decorView) return () => {}
55
+
56
+ const listener = new android.view.View.OnApplyWindowInsetsListener({
57
+ onApplyWindowInsets(_view: any, insets: any) {
58
+ update(readAndroidInsets(insets))
59
+ return insets
60
+ },
61
+ })
62
+
63
+ decorView.setOnApplyWindowInsetsListener(listener)
64
+ decorView.requestApplyInsets?.()
65
+
66
+ return () => decorView.setOnApplyWindowInsetsListener(null)
17
67
  }
18
68
 
19
69
  export function useSafeAreaInsets(): Insets {
20
- const [insets, setInsets] = useState<Insets>(readInsets);
70
+ const [insets, setInsets] = useState<Insets>(readInsets)
21
71
  useEffect(() => {
22
- const update = () => setInsets(readInsets());
23
- Application.on(Application.orientationChangedEvent, update);
24
- return () => Application.off(Application.orientationChangedEvent, update);
25
- }, []);
26
- return insets;
72
+ const update = () => setInsets(readInsets())
73
+ Application.on(Application.orientationChangedEvent, update)
74
+ Application.on(Application.resumeEvent, update)
75
+ const stopWatchingAndroid = Application.android ? watchAndroidInsets(setInsets) : () => {}
76
+ update()
77
+
78
+ return () => {
79
+ Application.off(Application.orientationChangedEvent, update)
80
+ Application.off(Application.resumeEvent, update)
81
+ stopWatchingAndroid()
82
+ }
83
+ }, [])
84
+
85
+ return insets
27
86
  }
@@ -1 +1 @@
1
- export { useSafeAreaInsets } from './safe-area.web.tsrx';
1
+ export { useSafeAreaInsets } from './safe-area.web.tsrx'
@@ -10,6 +10,7 @@ function measure(): Insets {
10
10
  'position:fixed;top:0;left:0;pointer-events:none;visibility:hidden;' +
11
11
  'padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom);' +
12
12
  'padding-left:env(safe-area-inset-left);padding-right:env(safe-area-inset-right);';
13
+
13
14
  document.body.appendChild(probe);
14
15
  const cs = getComputedStyle(probe);
15
16
  const insets = {
@@ -18,6 +19,7 @@ function measure(): Insets {
18
19
  left: parseFloat(cs.paddingLeft) || 0,
19
20
  right: parseFloat(cs.paddingRight) || 0,
20
21
  };
22
+
21
23
  probe.remove();
22
24
  return insets;
23
25
  }
@@ -30,5 +32,6 @@ export function useSafeAreaInsets(): Insets {
30
32
  window.addEventListener('resize', update);
31
33
  return () => window.removeEventListener('resize', update);
32
34
  }, []);
35
+
33
36
  return insets;
34
37
  }
@@ -1 +1 @@
1
- export { useWindowSize } from './screen.native.tsrx';
1
+ export { useWindowSize } from './screen.native.tsrx'
@@ -21,5 +21,6 @@ export function useWindowSize(): WindowSize {
21
21
  Application.on(Application.orientationChangedEvent, update);
22
22
  return () => Application.off(Application.orientationChangedEvent, update);
23
23
  }, []);
24
+
24
25
  return size;
25
26
  }
package/src/screen.web.ts CHANGED
@@ -1 +1 @@
1
- export { useWindowSize } from './screen.web.tsrx';
1
+ export { useWindowSize } from './screen.web.tsrx'
@@ -22,5 +22,6 @@ export function useWindowSize(): WindowSize {
22
22
  window.removeEventListener('orientationchange', update);
23
23
  };
24
24
  }, []);
25
+
25
26
  return size;
26
27
  }
@@ -1,9 +1,9 @@
1
1
  // Secure storage — Keychain (iOS) / Keystore (Android) via plugin.
2
- import { SecureStorage } from '@nativescript/secure-storage';
3
- import type { Capability } from './types';
4
- import type { SecureStore } from './types';
2
+ import { SecureStorage } from '@nativescript/secure-storage'
3
+ import type { Capability } from './types'
4
+ import type { SecureStore } from './types'
5
5
 
6
- const store = new SecureStorage();
6
+ const store = new SecureStorage()
7
7
 
8
8
  export const secureStorage: Capability<SecureStore> = {
9
9
  supported: true,
@@ -14,4 +14,4 @@ export const secureStorage: Capability<SecureStore> = {
14
14
  set: (key, value) => store.set({ key, value }),
15
15
  remove: (key) => store.remove({ key }),
16
16
  },
17
- };
17
+ }
@@ -1,10 +1,10 @@
1
1
  // Secure storage — web has no equivalent trust boundary (IndexedDB is not
2
2
  // secure enclave storage). Declared unsupported per the Capability contract;
3
3
  // callers must branch on `supported` rather than catching.
4
- import type { Capability, SecureStore } from './types';
4
+ import type { Capability, SecureStore } from './types'
5
5
 
6
6
  export const secureStorage: Capability<SecureStore> = {
7
7
  supported: false,
8
8
  ensure: async () => 'unsupported',
9
9
  impl: null,
10
- };
10
+ }
@@ -1,13 +1,13 @@
1
1
  // Share — native share sheet via @nativescript/social-share.
2
- import { shareText, shareUrl } from '@nativescript/social-share';
2
+ import { shareText, shareUrl } from '@nativescript/social-share'
3
3
 
4
4
  export const share = {
5
5
  async text(text: string, subject?: string): Promise<'shared'> {
6
- shareText(text, subject);
7
- return 'shared';
6
+ shareText(text, subject)
7
+ return 'shared'
8
8
  },
9
9
  async url(url: string, title?: string): Promise<'shared'> {
10
- shareUrl(url, title ?? url);
11
- return 'shared';
10
+ shareUrl(url, title ?? url)
11
+ return 'shared'
12
12
  },
13
- };
13
+ }
package/src/share.web.ts CHANGED
@@ -2,27 +2,31 @@
2
2
  // desktop degrades to clipboard copy so the call still does something useful.
3
3
  export const share = {
4
4
  async text(text: string, subject?: string): Promise<'shared' | 'copied' | 'unavailable'> {
5
- const nav = navigator as any;
5
+ const nav = navigator as any
6
6
  if (nav.share) {
7
- await nav.share({ text, title: subject });
8
- return 'shared';
7
+ await nav.share({ text, title: subject })
8
+ return 'shared'
9
9
  }
10
+
10
11
  if (nav.clipboard) {
11
- await nav.clipboard.writeText(text);
12
- return 'copied';
12
+ await nav.clipboard.writeText(text)
13
+ return 'copied'
13
14
  }
14
- return 'unavailable';
15
+
16
+ return 'unavailable'
15
17
  },
16
18
  async url(url: string, title?: string): Promise<'shared' | 'copied' | 'unavailable'> {
17
- const nav = navigator as any;
19
+ const nav = navigator as any
18
20
  if (nav.share) {
19
- await nav.share({ url, title });
20
- return 'shared';
21
+ await nav.share({ url, title })
22
+ return 'shared'
21
23
  }
24
+
22
25
  if (nav.clipboard) {
23
- await nav.clipboard.writeText(url);
24
- return 'copied';
26
+ await nav.clipboard.writeText(url)
27
+ return 'copied'
25
28
  }
26
- return 'unavailable';
29
+
30
+ return 'unavailable'
27
31
  },
28
- };
32
+ }
@@ -1,10 +1,14 @@
1
- import { ApplicationSettings } from '@nativescript/core';
1
+ import { ApplicationSettings } from '@nativescript/core'
2
2
 
3
3
  /** Sync KV seam — NS ApplicationSettings (NSUserDefaults) on native,
4
4
  * the web leaf uses the DOM's string store. String-only for v1;
5
5
  * serialize objects at the edge. */
6
6
  export const storage = {
7
7
  getString: (k: string): string | null => ApplicationSettings.getString(k) ?? null,
8
- setString: (k: string, v: string): void => { ApplicationSettings.setString(k, v); },
9
- remove: (k: string): void => { ApplicationSettings.remove(k); },
10
- };
8
+ setString: (k: string, v: string): void => {
9
+ ApplicationSettings.setString(k, v)
10
+ },
11
+ remove: (k: string): void => {
12
+ ApplicationSettings.remove(k)
13
+ },
14
+ }
@@ -1,6 +1,10 @@
1
1
  /** Sync KV seam — localStorage on web. */
2
2
  export const storage = {
3
3
  getString: (k: string): string | null => localStorage.getItem(k),
4
- setString: (k: string, v: string): void => { localStorage.setItem(k, v); },
5
- remove: (k: string): void => { localStorage.removeItem(k); },
6
- };
4
+ setString: (k: string, v: string): void => {
5
+ localStorage.setItem(k, v)
6
+ },
7
+ remove: (k: string): void => {
8
+ localStorage.removeItem(k)
9
+ },
10
+ }
@@ -1,22 +1,24 @@
1
1
  // Status/nav bars — native leaf. iOS status-bar style + Android
2
2
  // navigation-bar color via the application window.
3
- import { Application, Color } from '@nativescript/core';
3
+ import { Application, Color } from '@nativescript/core'
4
4
 
5
5
  export const systemBars = {
6
6
  setStatusBarStyle(style: 'light' | 'dark'): void {
7
7
  if (Application.ios) {
8
- const app = Application.ios.nativeApp;
8
+ const app = Application.ios.nativeApp
9
+
9
10
  // NS 9: per-page statusBarStyle is the preferred seam; fall back to
10
11
  // the app-level setter on older systems.
11
- (app as any)?.setStatusBarStyle?.(style === 'light' ? 1 : 0);
12
+
13
+ ;(app as any)?.setStatusBarStyle?.(style === 'light' ? 1 : 0)
12
14
  }
13
15
  },
14
16
  setColor(color: string): void {
15
- const c = new Color(color);
17
+ const c = new Color(color)
16
18
  if (Application.android?.startActivity) {
17
- const win = Application.android.startActivity.getWindow();
18
- win.setStatusBarColor(c.android);
19
- win.setNavigationBarColor(c.android);
19
+ const win = Application.android.startActivity.getWindow()
20
+ win.setStatusBarColor(c.android)
21
+ win.setNavigationBarColor(c.android)
20
22
  }
21
23
  },
22
- };
24
+ }
@@ -1,14 +1,17 @@
1
1
  // Status/nav bars — web leaf. No status bar on web, but theme-color is the
2
2
  // honest equivalent: it tints mobile browser chrome.
3
3
  export const systemBars = {
4
+ // Browsers do not expose a status-bar icon-style API. `setColor` below is
5
+ // the available theme-color equivalent; this method intentionally no-ops.
4
6
  setStatusBarStyle(_style: 'light' | 'dark'): void {},
5
7
  setColor(color: string): void {
6
- let meta = document.querySelector('meta[name="theme-color"]');
8
+ let meta = document.querySelector('meta[name="theme-color"]')
7
9
  if (!meta) {
8
- meta = document.createElement('meta');
9
- meta.setAttribute('name', 'theme-color');
10
- document.head.appendChild(meta);
10
+ meta = document.createElement('meta')
11
+ meta.setAttribute('name', 'theme-color')
12
+ document.head.appendChild(meta)
11
13
  }
12
- meta.setAttribute('content', color);
14
+
15
+ meta.setAttribute('content', color)
13
16
  },
14
- };
17
+ }