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
d079c6d6c9
commit
aa33a55d0b
18 changed files with 370 additions and 62 deletions
110
mobile/README.md
110
mobile/README.md
|
|
@ -1,19 +1,22 @@
|
||||||
# PedScribe Mobile App
|
# 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
|
## 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)
|
- Configurable server URL (supports self-hosted instances)
|
||||||
- Android foreground service with wake lock for reliable recording
|
- Haptic feedback on recording start/stop
|
||||||
- iOS background audio mode
|
- 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
|
- App Store and Play Store ready
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Node.js 18+
|
- 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)
|
- Xcode 15+ (for iOS builds, macOS only)
|
||||||
- Apple Developer account ($99/yr for App Store)
|
- Apple Developer account ($99/yr for App Store)
|
||||||
- Google Play Developer account ($25 one-time)
|
- Google Play Developer account ($25 one-time)
|
||||||
|
|
@ -32,35 +35,67 @@ npx cap sync
|
||||||
# Open in Android Studio
|
# Open in Android Studio
|
||||||
npx cap open android
|
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
|
cd android && ./gradlew assembleRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
The APK will be at `android/app/build/outputs/apk/release/app-release.apk`
|
## Build iOS (macOS only)
|
||||||
|
|
||||||
## Build iOS
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Open in Xcode (macOS only)
|
# Open in Xcode
|
||||||
npx cap open ios
|
npx cap open ios
|
||||||
|
|
||||||
# Build and sign in Xcode
|
# In Xcode:
|
||||||
# Product > Archive > Distribute
|
# 1. Select your team/signing certificate
|
||||||
|
# 2. Product > Archive
|
||||||
|
# 3. Distribute App > App Store Connect
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. App launches with a local launcher page
|
1. App launches with a local launcher page
|
||||||
2. User enters their PedScribe server URL (default: app.pedshub.com)
|
2. First launch: user enters their PedScribe server URL (default: app.pedshub.com)
|
||||||
3. URL is saved to localStorage for future launches
|
3. URL is saved locally for future launches
|
||||||
4. App navigates to the remote web app
|
4. App navigates to the remote web app inside a native WebView
|
||||||
5. Web app runs inside the native WebView with full native API access
|
5. Native plugins provide background recording, haptics, and push notifications
|
||||||
|
|
||||||
### Background Recording
|
### 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
|
## App Structure
|
||||||
|
|
||||||
|
|
@ -70,25 +105,48 @@ mobile/
|
||||||
package.json # Dependencies
|
package.json # Dependencies
|
||||||
src/
|
src/
|
||||||
index.html # Launcher page (server URL config)
|
index.html # Launcher page (server URL config)
|
||||||
launcher.js # Auto-redirect logic
|
launcher.js # Auto-redirect + native feature init
|
||||||
launcher.css # Launcher styles
|
launcher.css # Launcher styles
|
||||||
android/ # Android native project
|
android/ # Android native project
|
||||||
app/src/main/
|
app/src/main/
|
||||||
java/com/pedshub/scribe/
|
java/com/pedshub/scribe/
|
||||||
MainActivity.java
|
MainActivity.java
|
||||||
AudioRecordingService.java
|
AudioRecordingService.java
|
||||||
AndroidManifest.xml
|
AndroidManifest.xml # Permissions, deep links, share intent
|
||||||
ios/ # iOS native project
|
ios/ # iOS native project
|
||||||
App/App/
|
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
|
To update native features (plugins, permissions, splash screen):
|
||||||
var DEFAULT_URL = 'https://your-server.example.com';
|
```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"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':capacitor-app')
|
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-splash-screen')
|
||||||
implementation project(':capacitor-status-bar')
|
implementation project(':capacitor-status-bar')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,32 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,21 @@
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"SplashScreen": {
|
"SplashScreen": {
|
||||||
"launchShowDuration": 1000,
|
"launchShowDuration": 1500,
|
||||||
"backgroundColor": "#2563eb",
|
"backgroundColor": "#2563eb",
|
||||||
"showSpinner": false
|
"showSpinner": true,
|
||||||
|
"spinnerColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"PushNotifications": {
|
||||||
|
"presentationOptions": [
|
||||||
|
"badge",
|
||||||
|
"sound",
|
||||||
|
"alert"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Keyboard": {
|
||||||
|
"resize": "body",
|
||||||
|
"resizeOnFullScreen": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,26 @@
|
||||||
"pkg": "@capacitor/app",
|
"pkg": "@capacitor/app",
|
||||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
"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",
|
"pkg": "@capacitor/splash-screen",
|
||||||
"classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin"
|
"classpath": "com.capacitorjs.plugins.splashscreen.SplashScreenPlugin"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// PedScribe Mobile Launcher
|
// PedScribe Mobile Launcher
|
||||||
// Handles configurable server URL and auto-redirect
|
// Handles configurable server URL, auto-redirect, and native features
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var STORAGE_KEY = 'pedscribe_server_url';
|
var STORAGE_KEY = 'pedscribe_server_url';
|
||||||
|
|
@ -15,10 +15,8 @@
|
||||||
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
||||||
|
|
||||||
if (savedUrl) {
|
if (savedUrl) {
|
||||||
// Auto-connect to saved server
|
|
||||||
showConnecting(savedUrl);
|
showConnecting(savedUrl);
|
||||||
} else {
|
} else {
|
||||||
// Show setup screen
|
|
||||||
urlInput.value = DEFAULT_URL;
|
urlInput.value = DEFAULT_URL;
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
connectingScreen.style.display = 'none';
|
connectingScreen.style.display = 'none';
|
||||||
|
|
@ -31,8 +29,8 @@
|
||||||
|
|
||||||
connectBtn.disabled = true;
|
connectBtn.disabled = true;
|
||||||
connectBtn.textContent = 'Connecting...';
|
connectBtn.textContent = 'Connecting...';
|
||||||
|
hapticFeedback();
|
||||||
|
|
||||||
// Test the server is reachable
|
|
||||||
testServer(url, function(ok) {
|
testServer(url, function(ok) {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
localStorage.setItem(STORAGE_KEY, url);
|
localStorage.setItem(STORAGE_KEY, url);
|
||||||
|
|
@ -50,7 +48,7 @@
|
||||||
if (e.key === 'Enter') connectBtn.click();
|
if (e.key === 'Enter') connectBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Change server button (from connecting screen)
|
// Change server button
|
||||||
changeBtn.addEventListener('click', function() {
|
changeBtn.addEventListener('click', function() {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
|
|
@ -63,13 +61,11 @@
|
||||||
setupScreen.style.display = 'none';
|
setupScreen.style.display = 'none';
|
||||||
connectingScreen.style.display = '';
|
connectingScreen.style.display = '';
|
||||||
|
|
||||||
// Brief delay so the user sees the connecting screen
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
testServer(url, function(ok) {
|
testServer(url, function(ok) {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
navigateToServer(url);
|
navigateToServer(url);
|
||||||
} else {
|
} else {
|
||||||
// Server not reachable — show setup screen
|
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
connectingScreen.style.display = 'none';
|
connectingScreen.style.display = 'none';
|
||||||
urlInput.value = url;
|
urlInput.value = url;
|
||||||
|
|
@ -87,11 +83,9 @@
|
||||||
callback(ok);
|
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) })
|
fetch(url + '/api/health', { mode: 'no-cors', signal: AbortSignal.timeout(8000) })
|
||||||
.then(function() { respond(true); })
|
.then(function() { respond(true); })
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
// Fetch failed — try image load as a fallback connectivity check
|
|
||||||
var fallbackTimer = setTimeout(function() { respond(false); }, 5000);
|
var fallbackTimer = setTimeout(function() { respond(false); }, 5000);
|
||||||
var img = new Image();
|
var img = new Image();
|
||||||
img.onload = function() { clearTimeout(fallbackTimer); respond(true); };
|
img.onload = function() { clearTimeout(fallbackTimer); respond(true); };
|
||||||
|
|
@ -112,6 +106,13 @@
|
||||||
div.style.display = 'block';
|
div.style.display = 'block';
|
||||||
div.textContent = msg;
|
div.textContent = msg;
|
||||||
connectBtn.parentNode.insertBefore(div, connectBtn.nextSibling);
|
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'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
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'
|
include ':capacitor-splash-screen'
|
||||||
project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
|
project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,17 @@
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"SplashScreen": {
|
"SplashScreen": {
|
||||||
"launchShowDuration": 1000,
|
"launchShowDuration": 1500,
|
||||||
"backgroundColor": "#2563eb",
|
"backgroundColor": "#2563eb",
|
||||||
"showSpinner": false
|
"showSpinner": true,
|
||||||
|
"spinnerColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"PushNotifications": {
|
||||||
|
"presentationOptions": ["badge", "sound", "alert"]
|
||||||
|
},
|
||||||
|
"Keyboard": {
|
||||||
|
"resize": "body",
|
||||||
|
"resizeOnFullScreen": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,16 @@
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>pedscribe</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,21 @@
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"SplashScreen": {
|
"SplashScreen": {
|
||||||
"launchShowDuration": 1000,
|
"launchShowDuration": 1500,
|
||||||
"backgroundColor": "#2563eb",
|
"backgroundColor": "#2563eb",
|
||||||
"showSpinner": false
|
"showSpinner": true,
|
||||||
|
"spinnerColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"PushNotifications": {
|
||||||
|
"presentationOptions": [
|
||||||
|
"badge",
|
||||||
|
"sound",
|
||||||
|
"alert"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Keyboard": {
|
||||||
|
"resize": "body",
|
||||||
|
"resizeOnFullScreen": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
|
@ -24,6 +36,11 @@
|
||||||
},
|
},
|
||||||
"packageClassList": [
|
"packageClassList": [
|
||||||
"AppPlugin",
|
"AppPlugin",
|
||||||
|
"HapticsPlugin",
|
||||||
|
"KeyboardPlugin",
|
||||||
|
"PushNotificationsPlugin",
|
||||||
|
"ScreenOrientationPlugin",
|
||||||
|
"SharePlugin",
|
||||||
"SplashScreenPlugin",
|
"SplashScreenPlugin",
|
||||||
"StatusBarPlugin"
|
"StatusBarPlugin"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// PedScribe Mobile Launcher
|
// PedScribe Mobile Launcher
|
||||||
// Handles configurable server URL and auto-redirect
|
// Handles configurable server URL, auto-redirect, and native features
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var STORAGE_KEY = 'pedscribe_server_url';
|
var STORAGE_KEY = 'pedscribe_server_url';
|
||||||
|
|
@ -15,10 +15,8 @@
|
||||||
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
||||||
|
|
||||||
if (savedUrl) {
|
if (savedUrl) {
|
||||||
// Auto-connect to saved server
|
|
||||||
showConnecting(savedUrl);
|
showConnecting(savedUrl);
|
||||||
} else {
|
} else {
|
||||||
// Show setup screen
|
|
||||||
urlInput.value = DEFAULT_URL;
|
urlInput.value = DEFAULT_URL;
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
connectingScreen.style.display = 'none';
|
connectingScreen.style.display = 'none';
|
||||||
|
|
@ -31,8 +29,8 @@
|
||||||
|
|
||||||
connectBtn.disabled = true;
|
connectBtn.disabled = true;
|
||||||
connectBtn.textContent = 'Connecting...';
|
connectBtn.textContent = 'Connecting...';
|
||||||
|
hapticFeedback();
|
||||||
|
|
||||||
// Test the server is reachable
|
|
||||||
testServer(url, function(ok) {
|
testServer(url, function(ok) {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
localStorage.setItem(STORAGE_KEY, url);
|
localStorage.setItem(STORAGE_KEY, url);
|
||||||
|
|
@ -50,7 +48,7 @@
|
||||||
if (e.key === 'Enter') connectBtn.click();
|
if (e.key === 'Enter') connectBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Change server button (from connecting screen)
|
// Change server button
|
||||||
changeBtn.addEventListener('click', function() {
|
changeBtn.addEventListener('click', function() {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
|
|
@ -63,13 +61,11 @@
|
||||||
setupScreen.style.display = 'none';
|
setupScreen.style.display = 'none';
|
||||||
connectingScreen.style.display = '';
|
connectingScreen.style.display = '';
|
||||||
|
|
||||||
// Brief delay so the user sees the connecting screen
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
testServer(url, function(ok) {
|
testServer(url, function(ok) {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
navigateToServer(url);
|
navigateToServer(url);
|
||||||
} else {
|
} else {
|
||||||
// Server not reachable — show setup screen
|
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
connectingScreen.style.display = 'none';
|
connectingScreen.style.display = 'none';
|
||||||
urlInput.value = url;
|
urlInput.value = url;
|
||||||
|
|
@ -87,11 +83,9 @@
|
||||||
callback(ok);
|
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) })
|
fetch(url + '/api/health', { mode: 'no-cors', signal: AbortSignal.timeout(8000) })
|
||||||
.then(function() { respond(true); })
|
.then(function() { respond(true); })
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
// Fetch failed — try image load as a fallback connectivity check
|
|
||||||
var fallbackTimer = setTimeout(function() { respond(false); }, 5000);
|
var fallbackTimer = setTimeout(function() { respond(false); }, 5000);
|
||||||
var img = new Image();
|
var img = new Image();
|
||||||
img.onload = function() { clearTimeout(fallbackTimer); respond(true); };
|
img.onload = function() { clearTimeout(fallbackTimer); respond(true); };
|
||||||
|
|
@ -112,6 +106,13 @@
|
||||||
div.style.display = 'block';
|
div.style.display = 'block';
|
||||||
div.textContent = msg;
|
div.textContent = msg;
|
||||||
connectBtn.parentNode.insertBefore(div, connectBtn.nextSibling);
|
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 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
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 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
|
||||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||||
end
|
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"
|
"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": {
|
"node_modules/@capacitor/ios": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
|
||||||
|
|
@ -72,6 +81,42 @@
|
||||||
"@capacitor/core": "^6.2.0"
|
"@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": {
|
"node_modules/@capacitor/splash-screen": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.4.tgz",
|
"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/app": "^6.0.0",
|
||||||
"@capacitor/cli": "^6.0.0",
|
"@capacitor/cli": "^6.0.0",
|
||||||
"@capacitor/core": "^6.0.0",
|
"@capacitor/core": "^6.0.0",
|
||||||
|
"@capacitor/haptics": "^6.0.0",
|
||||||
"@capacitor/ios": "^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/splash-screen": "^6.0.0",
|
||||||
"@capacitor/status-bar": "^6.0.0"
|
"@capacitor/status-bar": "^6.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -76,6 +81,15 @@
|
||||||
"tslib": "^2.1.0"
|
"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": {
|
"node_modules/@capacitor/ios": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz",
|
||||||
|
|
@ -85,6 +99,42 @@
|
||||||
"@capacitor/core": "^6.2.0"
|
"@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": {
|
"node_modules/@capacitor/splash-screen": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-6.0.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,11 @@
|
||||||
"@capacitor/cli": "^6.0.0",
|
"@capacitor/cli": "^6.0.0",
|
||||||
"@capacitor/core": "^6.0.0",
|
"@capacitor/core": "^6.0.0",
|
||||||
"@capacitor/ios": "^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/splash-screen": "^6.0.0",
|
||||||
"@capacitor/status-bar": "^6.0.0"
|
"@capacitor/status-bar": "^6.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// PedScribe Mobile Launcher
|
// PedScribe Mobile Launcher
|
||||||
// Handles configurable server URL and auto-redirect
|
// Handles configurable server URL, auto-redirect, and native features
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var STORAGE_KEY = 'pedscribe_server_url';
|
var STORAGE_KEY = 'pedscribe_server_url';
|
||||||
|
|
@ -15,10 +15,8 @@
|
||||||
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
var savedUrl = localStorage.getItem(STORAGE_KEY);
|
||||||
|
|
||||||
if (savedUrl) {
|
if (savedUrl) {
|
||||||
// Auto-connect to saved server
|
|
||||||
showConnecting(savedUrl);
|
showConnecting(savedUrl);
|
||||||
} else {
|
} else {
|
||||||
// Show setup screen
|
|
||||||
urlInput.value = DEFAULT_URL;
|
urlInput.value = DEFAULT_URL;
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
connectingScreen.style.display = 'none';
|
connectingScreen.style.display = 'none';
|
||||||
|
|
@ -31,8 +29,8 @@
|
||||||
|
|
||||||
connectBtn.disabled = true;
|
connectBtn.disabled = true;
|
||||||
connectBtn.textContent = 'Connecting...';
|
connectBtn.textContent = 'Connecting...';
|
||||||
|
hapticFeedback();
|
||||||
|
|
||||||
// Test the server is reachable
|
|
||||||
testServer(url, function(ok) {
|
testServer(url, function(ok) {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
localStorage.setItem(STORAGE_KEY, url);
|
localStorage.setItem(STORAGE_KEY, url);
|
||||||
|
|
@ -50,7 +48,7 @@
|
||||||
if (e.key === 'Enter') connectBtn.click();
|
if (e.key === 'Enter') connectBtn.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Change server button (from connecting screen)
|
// Change server button
|
||||||
changeBtn.addEventListener('click', function() {
|
changeBtn.addEventListener('click', function() {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
|
|
@ -63,13 +61,11 @@
|
||||||
setupScreen.style.display = 'none';
|
setupScreen.style.display = 'none';
|
||||||
connectingScreen.style.display = '';
|
connectingScreen.style.display = '';
|
||||||
|
|
||||||
// Brief delay so the user sees the connecting screen
|
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
testServer(url, function(ok) {
|
testServer(url, function(ok) {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
navigateToServer(url);
|
navigateToServer(url);
|
||||||
} else {
|
} else {
|
||||||
// Server not reachable — show setup screen
|
|
||||||
setupScreen.style.display = '';
|
setupScreen.style.display = '';
|
||||||
connectingScreen.style.display = 'none';
|
connectingScreen.style.display = 'none';
|
||||||
urlInput.value = url;
|
urlInput.value = url;
|
||||||
|
|
@ -87,11 +83,9 @@
|
||||||
callback(ok);
|
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) })
|
fetch(url + '/api/health', { mode: 'no-cors', signal: AbortSignal.timeout(8000) })
|
||||||
.then(function() { respond(true); })
|
.then(function() { respond(true); })
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
// Fetch failed — try image load as a fallback connectivity check
|
|
||||||
var fallbackTimer = setTimeout(function() { respond(false); }, 5000);
|
var fallbackTimer = setTimeout(function() { respond(false); }, 5000);
|
||||||
var img = new Image();
|
var img = new Image();
|
||||||
img.onload = function() { clearTimeout(fallbackTimer); respond(true); };
|
img.onload = function() { clearTimeout(fallbackTimer); respond(true); };
|
||||||
|
|
@ -112,6 +106,13 @@
|
||||||
div.style.display = 'block';
|
div.style.display = 'block';
|
||||||
div.textContent = msg;
|
div.textContent = msg;
|
||||||
connectBtn.parentNode.insertBefore(div, connectBtn.nextSibling);
|
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
|
// Check if server-side transcription (Whisper/AWS) is available
|
||||||
window._transcribeAvailable = null; // null = not checked yet, true/false after check
|
window._transcribeAvailable = null; // null = not checked yet, true/false after check
|
||||||
function checkTranscribeStatus() {
|
function checkTranscribeStatus() {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@
|
||||||
indicator.classList.remove('hidden');
|
indicator.classList.remove('hidden');
|
||||||
timer.start();
|
timer.start();
|
||||||
if (recognition) try { recognition.start(); } catch(e) {}
|
if (recognition) try { recognition.start(); } catch(e) {}
|
||||||
|
if (typeof nativeHaptic === 'function') nativeHaptic('heavy');
|
||||||
|
if (typeof nativeKeepAwake === 'function') nativeKeepAwake(true);
|
||||||
showToast('Recording started', 'info');
|
showToast('Recording started', 'info');
|
||||||
document.dispatchEvent(new CustomEvent('recording-started', { detail: { module: 'enc' } }));
|
document.dispatchEvent(new CustomEvent('recording-started', { detail: { module: 'enc' } }));
|
||||||
}).catch(function() { showToast('Microphone denied', 'error'); });
|
}).catch(function() { showToast('Microphone denied', 'error'); });
|
||||||
|
|
@ -80,6 +82,8 @@
|
||||||
if (stopBtn) stopBtn.classList.add('hidden');
|
if (stopBtn) stopBtn.classList.add('hidden');
|
||||||
indicator.classList.add('hidden');
|
indicator.classList.add('hidden');
|
||||||
if (recognition) try { recognition.stop(); } catch(e) {}
|
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' } }));
|
document.dispatchEvent(new CustomEvent('recording-stopped', { detail: { module: 'enc' } }));
|
||||||
|
|
||||||
// If no server transcription API, use live transcript directly (no upload)
|
// If no server transcription API, use live transcript directly (no upload)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue