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.
This commit is contained in:
parent
1c3ab8d68e
commit
b43bc614da
18 changed files with 370 additions and 62 deletions
110
mobile/README.md
110
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.
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,32 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep linking: pedscribe:// and https://app.pedshub.com -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="pedscribe" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="app.pedshub.com" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share intent: receive text/files from other apps -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/pdf" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<service
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,26 @@
|
|||
"pkg": "@capacitor/app",
|
||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/haptics",
|
||||
"classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/keyboard",
|
||||
"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/push-notifications",
|
||||
"classpath": "com.capacitorjs.plugins.pushnotifications.PushNotificationsPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/screen-orientation",
|
||||
"classpath": "com.capacitorjs.plugins.screenorientation.ScreenOrientationPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/share",
|
||||
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/splash-screen",
|
||||
"classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin"
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,21 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
|||
include ':capacitor-app'
|
||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||
|
||||
include ':capacitor-haptics'
|
||||
project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')
|
||||
|
||||
include ':capacitor-keyboard'
|
||||
project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
|
||||
|
||||
include ':capacitor-push-notifications'
|
||||
project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
|
||||
|
||||
include ':capacitor-screen-orientation'
|
||||
project(':capacitor-screen-orientation').projectDir = new File('../node_modules/@capacitor/screen-orientation/android')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||
|
||||
include ':capacitor-splash-screen'
|
||||
project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,17 @@
|
|||
},
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,16 @@
|
|||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>pedscribe</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
45
mobile/node_modules/.package-lock.json
generated
vendored
45
mobile/node_modules/.package-lock.json
generated
vendored
|
|
@ -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",
|
||||
|
|
|
|||
50
mobile/package-lock.json
generated
50
mobile/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue