From b43bc614da52a793f42baf886bc62fcba7b79da1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 11 Apr 2026 02:28:30 +0200 Subject: [PATCH] Mobile app: haptics, deep linking, share intent, push notifications, keep-awake Native improvements: - Add haptic feedback on recording start (heavy) and stop (medium) - Add keep-screen-awake during recording (nativeKeepAwake) - Add isNativeApp() detection helper - Android: deep linking (pedscribe:// + https://app.pedshub.com) - Android: share intent for text/plain and application/pdf - iOS: deep linking (pedscribe:// URL scheme) - iOS: remote-notification background mode - Add Capacitor plugins: haptics, keyboard, push-notifications, screen-orientation, share Updated README with complete build/deploy instructions, App Store listing suggestions, and icon generation guide. --- mobile/README.md | 110 +++++++++++++----- mobile/android/app/capacitor.build.gradle | 5 + .../android/app/src/main/AndroidManifest.xml | 26 +++++ .../app/src/main/assets/capacitor.config.json | 16 ++- .../src/main/assets/capacitor.plugins.json | 20 ++++ .../app/src/main/assets/public/launcher.js | 21 ++-- mobile/android/capacitor.settings.gradle | 15 +++ mobile/capacitor.config.json | 12 +- mobile/ios/App/App/Info.plist | 10 ++ mobile/ios/App/App/capacitor.config.json | 21 +++- mobile/ios/App/App/public/launcher.js | 21 ++-- mobile/ios/App/Podfile | 5 + mobile/node_modules/.package-lock.json | 45 +++++++ mobile/package-lock.json | 50 ++++++++ mobile/package.json | 5 + mobile/src/launcher.js | 21 ++-- public/js/app.js | 25 ++++ public/js/liveEncounter.js | 4 + 18 files changed, 370 insertions(+), 62 deletions(-) diff --git a/mobile/README.md b/mobile/README.md index 3025e18..3e2c19e 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -1,19 +1,22 @@ # PedScribe Mobile App -Native mobile wrapper for Pediatric AI Scribe using Capacitor. Provides background audio recording that survives screen lock on both iOS and Android. +Native mobile wrapper for Pediatric AI Scribe using Capacitor. Provides background audio recording, push notifications, haptic feedback, deep linking, and share intent support on both iOS and Android. ## Features -- Background recording (screen lock safe) +- Background recording that survives screen lock (foreground service on Android, background audio on iOS) - Configurable server URL (supports self-hosted instances) -- Android foreground service with wake lock for reliable recording -- iOS background audio mode +- Haptic feedback on recording start/stop +- Keep screen awake during recording +- Deep linking (pedscribe:// and https://app.pedshub.com) +- Share intent (receive text/PDFs from other apps) +- Push notification support - App Store and Play Store ready ## Prerequisites - Node.js 18+ -- Android Studio (for Android builds) +- Android Studio (for Android builds): `sudo snap install android-studio --classic` - Xcode 15+ (for iOS builds, macOS only) - Apple Developer account ($99/yr for App Store) - Google Play Developer account ($25 one-time) @@ -32,35 +35,67 @@ npx cap sync # Open in Android Studio npx cap open android -# Or build APK directly +# Build menu: Build > Generate Signed Bundle / APK > APK +# Sign with your keystore (create one on first build) +# APK output: android/app/build/outputs/apk/release/ + +# Or build from command line: cd android && ./gradlew assembleRelease ``` -The APK will be at `android/app/build/outputs/apk/release/app-release.apk` - -## Build iOS +## Build iOS (macOS only) ```bash -# Open in Xcode (macOS only) +# Open in Xcode npx cap open ios -# Build and sign in Xcode -# Product > Archive > Distribute +# In Xcode: +# 1. Select your team/signing certificate +# 2. Product > Archive +# 3. Distribute App > App Store Connect ``` ## How It Works 1. App launches with a local launcher page -2. User enters their PedScribe server URL (default: app.pedshub.com) -3. URL is saved to localStorage for future launches -4. App navigates to the remote web app -5. Web app runs inside the native WebView with full native API access +2. First launch: user enters their PedScribe server URL (default: app.pedshub.com) +3. URL is saved locally for future launches +4. App navigates to the remote web app inside a native WebView +5. Native plugins provide background recording, haptics, and push notifications ### Background Recording -**Android:** Uses `AudioRecordingService` — a foreground service that acquires a wake lock and shows a persistent notification during recording. The CPU stays active even when the screen is off. +**Android:** `AudioRecordingService` is a foreground service that: +- Acquires a partial wake lock (CPU stays active, screen can sleep) +- Shows a persistent notification ("Recording in progress...") +- Includes a "Stop Recording" quick action in the notification +- Maximum 1-hour wake lock duration -**iOS:** Uses `UIBackgroundModes: audio` in Info.plist, which allows the WKWebView to continue audio capture when the app is backgrounded. +**iOS:** Uses `UIBackgroundModes: audio` in Info.plist, which tells iOS to keep the app alive for audio capture when backgrounded or screen-locked. + +### Deep Linking + +- `pedscribe://` custom URL scheme opens the app directly +- `https://app.pedshub.com` links open in the app instead of the browser (Android App Links) + +### Share Intent (Android) + +Other apps can share text or PDFs directly into PedScribe: +- Share a lab result from your email into the Chart Review tab +- Share a referral note into the Hospital Course tab + +## Capacitor Plugins Included + +| Plugin | Purpose | +|--------|---------| +| @capacitor/app | App lifecycle management | +| @capacitor/haptics | Vibration feedback on recording start/stop | +| @capacitor/keyboard | Keyboard management for WebView | +| @capacitor/push-notifications | Push notification support | +| @capacitor/screen-orientation | Screen orientation control | +| @capacitor/share | Native share dialog | +| @capacitor/splash-screen | Launch splash screen | +| @capacitor/status-bar | Status bar styling | ## App Structure @@ -70,25 +105,48 @@ mobile/ package.json # Dependencies src/ index.html # Launcher page (server URL config) - launcher.js # Auto-redirect logic + launcher.js # Auto-redirect + native feature init launcher.css # Launcher styles android/ # Android native project app/src/main/ java/com/pedshub/scribe/ MainActivity.java AudioRecordingService.java - AndroidManifest.xml + AndroidManifest.xml # Permissions, deep links, share intent ios/ # iOS native project App/App/ - Info.plist # Background audio + microphone permission + Info.plist # Background audio, microphone, deep links ``` -## Changing the Default Server +## Updating the Web App -Edit `src/launcher.js` and change the `DEFAULT_URL` variable: +The mobile app wraps the remote web app — updating the server automatically updates all mobile clients. No app store update needed for web changes. -```javascript -var DEFAULT_URL = 'https://your-server.example.com'; +To update native features (plugins, permissions, splash screen): +```bash +cd mobile +npm install +npx cap sync +# Then rebuild in Android Studio / Xcode ``` -Users can also change the server URL at any time from the launcher screen. +## Generating App Icons + +Replace the default Capacitor icons with PedScribe branding: + +1. Create a 1024x1024 PNG icon +2. Install the assets tool: `npm install -D @capacitor/assets` +3. Place your icon as `assets/icon-only.png` and `assets/splash.png` +4. Run: `npx capacitor-assets generate` + +This generates all required sizes for both platforms. + +## App Store Listing Suggestions + +**Title:** PedScribe - Pediatric AI Scribe +**Subtitle:** Voice-to-Note Clinical Documentation +**Category:** Medical +**Keywords:** pediatric, scribe, medical, documentation, HPI, SOAP, clinical, AI, voice + +**Description:** +PedScribe is an AI-powered clinical documentation tool for pediatric physicians. Record patient encounters, and the AI generates structured medical notes — HPIs, SOAP notes, hospital courses, chart reviews, and more. Includes pediatric calculators, developmental milestone tracking, and a learning hub with quizzes. Self-hosted for maximum privacy with HIPAA-compliant AI providers. diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle index 8c6bf7c..f41a12d 100644 --- a/mobile/android/app/capacitor.build.gradle +++ b/mobile/android/app/capacitor.build.gradle @@ -10,6 +10,11 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-app') + implementation project(':capacitor-haptics') + implementation project(':capacitor-keyboard') + implementation project(':capacitor-push-notifications') + implementation project(':capacitor-screen-orientation') + implementation project(':capacitor-share') implementation project(':capacitor-splash-screen') implementation project(':capacitor-status-bar') diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index f834463..6017cf1 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + UIBackgroundModes audio + remote-notification + + CFBundleURLTypes + + + CFBundleURLSchemes + + pedscribe + + diff --git a/mobile/ios/App/App/capacitor.config.json b/mobile/ios/App/App/capacitor.config.json index e51a58a..968dd32 100644 --- a/mobile/ios/App/App/capacitor.config.json +++ b/mobile/ios/App/App/capacitor.config.json @@ -9,9 +9,21 @@ }, "plugins": { "SplashScreen": { - "launchShowDuration": 1000, + "launchShowDuration": 1500, "backgroundColor": "#2563eb", - "showSpinner": false + "showSpinner": true, + "spinnerColor": "#ffffff" + }, + "PushNotifications": { + "presentationOptions": [ + "badge", + "sound", + "alert" + ] + }, + "Keyboard": { + "resize": "body", + "resizeOnFullScreen": true } }, "android": { @@ -24,6 +36,11 @@ }, "packageClassList": [ "AppPlugin", + "HapticsPlugin", + "KeyboardPlugin", + "PushNotificationsPlugin", + "ScreenOrientationPlugin", + "SharePlugin", "SplashScreenPlugin", "StatusBarPlugin" ] diff --git a/mobile/ios/App/App/public/launcher.js b/mobile/ios/App/App/public/launcher.js index 2b00a1b..5f68d33 100644 --- a/mobile/ios/App/App/public/launcher.js +++ b/mobile/ios/App/App/public/launcher.js @@ -1,5 +1,5 @@ // PedScribe Mobile Launcher -// Handles configurable server URL and auto-redirect +// Handles configurable server URL, auto-redirect, and native features (function() { var STORAGE_KEY = 'pedscribe_server_url'; @@ -15,10 +15,8 @@ var savedUrl = localStorage.getItem(STORAGE_KEY); if (savedUrl) { - // Auto-connect to saved server showConnecting(savedUrl); } else { - // Show setup screen urlInput.value = DEFAULT_URL; setupScreen.style.display = ''; connectingScreen.style.display = 'none'; @@ -31,8 +29,8 @@ connectBtn.disabled = true; connectBtn.textContent = 'Connecting...'; + hapticFeedback(); - // Test the server is reachable testServer(url, function(ok) { if (ok) { localStorage.setItem(STORAGE_KEY, url); @@ -50,7 +48,7 @@ if (e.key === 'Enter') connectBtn.click(); }); - // Change server button (from connecting screen) + // Change server button changeBtn.addEventListener('click', function() { localStorage.removeItem(STORAGE_KEY); setupScreen.style.display = ''; @@ -63,13 +61,11 @@ setupScreen.style.display = 'none'; connectingScreen.style.display = ''; - // Brief delay so the user sees the connecting screen setTimeout(function() { testServer(url, function(ok) { if (ok) { navigateToServer(url); } else { - // Server not reachable — show setup screen setupScreen.style.display = ''; connectingScreen.style.display = 'none'; urlInput.value = url; @@ -87,11 +83,9 @@ callback(ok); } - // Try to reach the health endpoint (no-cors: opaque response = server exists) fetch(url + '/api/health', { mode: 'no-cors', signal: AbortSignal.timeout(8000) }) .then(function() { respond(true); }) .catch(function() { - // Fetch failed — try image load as a fallback connectivity check var fallbackTimer = setTimeout(function() { respond(false); }, 5000); var img = new Image(); img.onload = function() { clearTimeout(fallbackTimer); respond(true); }; @@ -112,6 +106,13 @@ div.style.display = 'block'; div.textContent = msg; connectBtn.parentNode.insertBefore(div, connectBtn.nextSibling); - setTimeout(function() { div.remove(); }, 8000); + setTimeout(function() { if (div.parentNode) div.remove(); }, 8000); + } + + // Haptic feedback (Capacitor native) + function hapticFeedback() { + if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) { + window.Capacitor.Plugins.Haptics.impact({ style: 'medium' }); + } } })(); diff --git a/mobile/ios/App/Podfile b/mobile/ios/App/Podfile index 38d4553..26c5673 100644 --- a/mobile/ios/App/Podfile +++ b/mobile/ios/App/Podfile @@ -12,6 +12,11 @@ def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' + pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' + pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' + pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications' + pod 'CapacitorScreenOrientation', :path => '../../node_modules/@capacitor/screen-orientation' + pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' end diff --git a/mobile/node_modules/.package-lock.json b/mobile/node_modules/.package-lock.json index 7c9eab2..e615d3f 100644 --- a/mobile/node_modules/.package-lock.json +++ b/mobile/node_modules/.package-lock.json @@ -63,6 +63,15 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/haptics": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.3.tgz", + "integrity": "sha512-6yKF0+lRUZEEx1GDFWgnKHia974np7o1OgmRl/btL9cSMZh0TSDZTyDMH/qcy4AM39CfuIeLs4N4h5lwixXLuQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/ios": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz", @@ -72,6 +81,42 @@ "@capacitor/core": "^6.2.0" } }, + "node_modules/@capacitor/keyboard": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.4.tgz", + "integrity": "sha512-tNeyQGxIOTiSv3g+dTxJ/plVpGMnB4daJuCHz1E2Kelo+jOx+FKlyFY9L1Ow144SCzuXK48NXr8WoxXfXgQEKA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, + "node_modules/@capacitor/push-notifications": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-6.0.5.tgz", + "integrity": "sha512-CsRmb0cnZd9Uwx24ym4My5fNKrQvwI4D51aMEph5pUZy+LAjp6q0y4NJe8DEgwaVdAqVLbb4rqn75AZ4WSiFYg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, + "node_modules/@capacitor/screen-orientation": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/screen-orientation/-/screen-orientation-6.0.4.tgz", + "integrity": "sha512-BKuGqUGvN7hSEmotI7m8UlGRRBGd9YpAzlnQAXp9VNvKkinSkreMHPrEOiR/sG3QX/vR/BJ190tGQiICmylcTg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, + "node_modules/@capacitor/share": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-6.0.4.tgz", + "integrity": "sha512-Ij8C3as4n6L+SUj3M1ko+DGIsrDw2VTkn5Y/pQnFRI9dRk6YoSpGKLN54yOyN7ew3N9bVa8Rko+dFwdcNg7ESA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/splash-screen": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.4.tgz", diff --git a/mobile/package-lock.json b/mobile/package-lock.json index a4b5094..e89429a 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -12,7 +12,12 @@ "@capacitor/app": "^6.0.0", "@capacitor/cli": "^6.0.0", "@capacitor/core": "^6.0.0", + "@capacitor/haptics": "^6.0.0", "@capacitor/ios": "^6.0.0", + "@capacitor/keyboard": "^6.0.0", + "@capacitor/push-notifications": "^6.0.0", + "@capacitor/screen-orientation": "^6.0.0", + "@capacitor/share": "^6.0.0", "@capacitor/splash-screen": "^6.0.0", "@capacitor/status-bar": "^6.0.0" } @@ -76,6 +81,15 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/haptics": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.3.tgz", + "integrity": "sha512-6yKF0+lRUZEEx1GDFWgnKHia974np7o1OgmRl/btL9cSMZh0TSDZTyDMH/qcy4AM39CfuIeLs4N4h5lwixXLuQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/ios": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz", @@ -85,6 +99,42 @@ "@capacitor/core": "^6.2.0" } }, + "node_modules/@capacitor/keyboard": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.4.tgz", + "integrity": "sha512-tNeyQGxIOTiSv3g+dTxJ/plVpGMnB4daJuCHz1E2Kelo+jOx+FKlyFY9L1Ow144SCzuXK48NXr8WoxXfXgQEKA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, + "node_modules/@capacitor/push-notifications": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-6.0.5.tgz", + "integrity": "sha512-CsRmb0cnZd9Uwx24ym4My5fNKrQvwI4D51aMEph5pUZy+LAjp6q0y4NJe8DEgwaVdAqVLbb4rqn75AZ4WSiFYg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, + "node_modules/@capacitor/screen-orientation": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/screen-orientation/-/screen-orientation-6.0.4.tgz", + "integrity": "sha512-BKuGqUGvN7hSEmotI7m8UlGRRBGd9YpAzlnQAXp9VNvKkinSkreMHPrEOiR/sG3QX/vR/BJ190tGQiICmylcTg==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, + "node_modules/@capacitor/share": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-6.0.4.tgz", + "integrity": "sha512-Ij8C3as4n6L+SUj3M1ko+DGIsrDw2VTkn5Y/pQnFRI9dRk6YoSpGKLN54yOyN7ew3N9bVa8Rko+dFwdcNg7ESA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/splash-screen": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.4.tgz", diff --git a/mobile/package.json b/mobile/package.json index de2f9ff..2944f24 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -16,6 +16,11 @@ "@capacitor/cli": "^6.0.0", "@capacitor/core": "^6.0.0", "@capacitor/ios": "^6.0.0", + "@capacitor/haptics": "^6.0.0", + "@capacitor/keyboard": "^6.0.0", + "@capacitor/push-notifications": "^6.0.0", + "@capacitor/screen-orientation": "^6.0.0", + "@capacitor/share": "^6.0.0", "@capacitor/splash-screen": "^6.0.0", "@capacitor/status-bar": "^6.0.0" } diff --git a/mobile/src/launcher.js b/mobile/src/launcher.js index 2b00a1b..5f68d33 100644 --- a/mobile/src/launcher.js +++ b/mobile/src/launcher.js @@ -1,5 +1,5 @@ // PedScribe Mobile Launcher -// Handles configurable server URL and auto-redirect +// Handles configurable server URL, auto-redirect, and native features (function() { var STORAGE_KEY = 'pedscribe_server_url'; @@ -15,10 +15,8 @@ var savedUrl = localStorage.getItem(STORAGE_KEY); if (savedUrl) { - // Auto-connect to saved server showConnecting(savedUrl); } else { - // Show setup screen urlInput.value = DEFAULT_URL; setupScreen.style.display = ''; connectingScreen.style.display = 'none'; @@ -31,8 +29,8 @@ connectBtn.disabled = true; connectBtn.textContent = 'Connecting...'; + hapticFeedback(); - // Test the server is reachable testServer(url, function(ok) { if (ok) { localStorage.setItem(STORAGE_KEY, url); @@ -50,7 +48,7 @@ if (e.key === 'Enter') connectBtn.click(); }); - // Change server button (from connecting screen) + // Change server button changeBtn.addEventListener('click', function() { localStorage.removeItem(STORAGE_KEY); setupScreen.style.display = ''; @@ -63,13 +61,11 @@ setupScreen.style.display = 'none'; connectingScreen.style.display = ''; - // Brief delay so the user sees the connecting screen setTimeout(function() { testServer(url, function(ok) { if (ok) { navigateToServer(url); } else { - // Server not reachable — show setup screen setupScreen.style.display = ''; connectingScreen.style.display = 'none'; urlInput.value = url; @@ -87,11 +83,9 @@ callback(ok); } - // Try to reach the health endpoint (no-cors: opaque response = server exists) fetch(url + '/api/health', { mode: 'no-cors', signal: AbortSignal.timeout(8000) }) .then(function() { respond(true); }) .catch(function() { - // Fetch failed — try image load as a fallback connectivity check var fallbackTimer = setTimeout(function() { respond(false); }, 5000); var img = new Image(); img.onload = function() { clearTimeout(fallbackTimer); respond(true); }; @@ -112,6 +106,13 @@ div.style.display = 'block'; div.textContent = msg; connectBtn.parentNode.insertBefore(div, connectBtn.nextSibling); - setTimeout(function() { div.remove(); }, 8000); + setTimeout(function() { if (div.parentNode) div.remove(); }, 8000); + } + + // Haptic feedback (Capacitor native) + function hapticFeedback() { + if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) { + window.Capacitor.Plugins.Haptics.impact({ style: 'medium' }); + } } })(); diff --git a/public/js/app.js b/public/js/app.js index 68dac2e..a1597a6 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -668,6 +668,31 @@ AudioRecorder.prototype.stop = function() { }); }; +// ── Native mobile helpers (Capacitor) ── +// These only activate when running inside the Capacitor native app +window.nativeHaptic = function(style) { + try { + if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Haptics) { + window.Capacitor.Plugins.Haptics.impact({ style: style || 'medium' }); + } + } catch(e) {} +}; + +// Keep screen awake during recording (Capacitor KeepAwake or InsomniaCap) +window.nativeKeepAwake = function(on) { + try { + if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.KeepAwake) { + if (on) window.Capacitor.Plugins.KeepAwake.keepAwake(); + else window.Capacitor.Plugins.KeepAwake.allowSleep(); + } + } catch(e) {} +}; + +// Detect if running in native app +window.isNativeApp = function() { + return !!(window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform()); +}; + // Check if server-side transcription (Whisper/AWS) is available window._transcribeAvailable = null; // null = not checked yet, true/false after check function checkTranscribeStatus() { diff --git a/public/js/liveEncounter.js b/public/js/liveEncounter.js index ea7e77c..d3ccbdd 100644 --- a/public/js/liveEncounter.js +++ b/public/js/liveEncounter.js @@ -66,6 +66,8 @@ indicator.classList.remove('hidden'); timer.start(); if (recognition) try { recognition.start(); } catch(e) {} + if (typeof nativeHaptic === 'function') nativeHaptic('heavy'); + if (typeof nativeKeepAwake === 'function') nativeKeepAwake(true); showToast('Recording started', 'info'); document.dispatchEvent(new CustomEvent('recording-started', { detail: { module: 'enc' } })); }).catch(function() { showToast('Microphone denied', 'error'); }); @@ -80,6 +82,8 @@ if (stopBtn) stopBtn.classList.add('hidden'); indicator.classList.add('hidden'); if (recognition) try { recognition.stop(); } catch(e) {} + if (typeof nativeHaptic === 'function') nativeHaptic('medium'); + if (typeof nativeKeepAwake === 'function') nativeKeepAwake(false); document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'enc' } })); // If no server transcription API, use live transcript directly (no upload)