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)