From 2bbdb88ceb0825a545bf1c69c3dc115fc5ccf1c7 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 11 May 2026 01:28:43 +0200 Subject: [PATCH] fix mobile image save to photos --- mobile/android/app/build.gradle | 4 +- .../java/com/pedshub/scribe/MainActivity.java | 77 +++++++++++++++++++ mobile/package-lock.json | 4 +- mobile/package.json | 2 +- package-lock.json | 4 +- package.json | 2 +- public/js/assistant/images.js | 20 +++++ 7 files changed, 105 insertions(+), 8 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 0387929..581397d 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -9,8 +9,8 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion // Version values below are overwritten by scripts/release.sh from // the root package.json. versionCode auto-increments per release. - versionCode 714002 - versionName "7.14.2" + versionCode 714003 + versionName "7.14.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java b/mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java index 3fabeab..4934d10 100644 --- a/mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java +++ b/mobile/android/app/src/main/java/com/pedshub/scribe/MainActivity.java @@ -1,13 +1,19 @@ package com.pedshub.scribe; import android.Manifest; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintManager; +import android.provider.MediaStore; import android.util.Base64; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; @@ -20,6 +26,10 @@ import androidx.core.content.ContextCompat; import com.getcapacitor.BridgeActivity; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; + public class MainActivity extends BridgeActivity { private static final int MIC_PERMISSION_CODE = 1001; @@ -45,6 +55,9 @@ public class MainActivity extends BridgeActivity { // Register JS interface for Android's print / Save as PDF flow. setupPrintBridge(); + + // Register JS interface for saving generated visuals to Photos. + setupFileBridge(); } // ── WebView Microphone Permission ────────────────────────── @@ -95,6 +108,11 @@ public class MainActivity extends BridgeActivity { webView.addJavascriptInterface(new PrintBridge(this), "NativePrint"); } + private void setupFileBridge() { + WebView webView = this.bridge.getWebView(); + webView.addJavascriptInterface(new FileBridge(this), "NativeFiles"); + } + public static class RecordingBridge { private final MainActivity activity; @@ -129,6 +147,19 @@ public class MainActivity extends BridgeActivity { } } + public static class FileBridge { + private final MainActivity activity; + + FileBridge(MainActivity activity) { + this.activity = activity; + } + + @android.webkit.JavascriptInterface + public String saveImage(String filename, String base64Png) { + return activity.saveImageToPictures(filename, base64Png); + } + } + private void printHtmlFromBase64(String title, String base64Html) { try { byte[] decoded = Base64.decode(base64Html, Base64.DEFAULT); @@ -147,4 +178,50 @@ public class MainActivity extends BridgeActivity { android.util.Log.e("PedScribe", "Native print failed", e); } } + + private String saveImageToPictures(String filename, String base64Png) { + String safeName = sanitizeFilename(filename, "clinical-visual.png"); + try { + byte[] imageBytes = Base64.decode(base64Png, Base64.DEFAULT); + Uri uri; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ContentResolver resolver = getContentResolver(); + ContentValues values = new ContentValues(); + values.put(MediaStore.Images.Media.DISPLAY_NAME, safeName); + values.put(MediaStore.Images.Media.MIME_TYPE, "image/png"); + values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/PedScribe"); + values.put(MediaStore.Images.Media.IS_PENDING, 1); + uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + if (uri == null) return "error:Could not create image file"; + try (OutputStream out = resolver.openOutputStream(uri)) { + if (out == null) return "error:Could not open image file"; + out.write(imageBytes); + } + values.clear(); + values.put(MediaStore.Images.Media.IS_PENDING, 0); + resolver.update(uri, values, null, null); + } else { + File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "PedScribe"); + if (!dir.exists() && !dir.mkdirs()) return "error:Could not create Pictures/PedScribe"; + File file = new File(dir, safeName); + try (OutputStream out = new FileOutputStream(file)) { + out.write(imageBytes); + } + uri = Uri.fromFile(file); + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)); + } + return "saved:" + uri.toString(); + } catch (Exception e) { + android.util.Log.e("PedScribe", "Native image save failed", e); + return "error:" + (e.getMessage() != null ? e.getMessage() : "Image save failed"); + } + } + + private String sanitizeFilename(String filename, String fallback) { + String value = filename != null ? filename : fallback; + value = value.replaceAll("[^A-Za-z0-9._-]", "-"); + if (value.length() == 0) value = fallback; + if (!value.toLowerCase(java.util.Locale.US).endsWith(".png")) value = value + ".png"; + return value; + } } diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 045905d..3ba7dce 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -1,12 +1,12 @@ { "name": "pedscribe-mobile", - "version": "7.14.2", + "version": "7.14.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pedscribe-mobile", - "version": "7.14.2", + "version": "7.14.3", "dependencies": { "@aparajita/capacitor-biometric-auth": "^8.0.0", "@capacitor/android": "^6.0.0", diff --git a/mobile/package.json b/mobile/package.json index b7be2ec..08b3f57 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,6 +1,6 @@ { "name": "pedscribe-mobile", - "version": "7.14.2", + "version": "7.14.3", "description": "PedScribe native mobile app — Capacitor wrapper for Pediatric AI Scribe", "private": true, "scripts": { diff --git a/package-lock.json b/package-lock.json index f7e4c79..76ad8ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pediatric-ai-scribe", - "version": "7.14.2", + "version": "7.14.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pediatric-ai-scribe", - "version": "7.14.2", + "version": "7.14.3", "dependencies": { "@marp-team/marp-cli": "^4.3.1", "@marp-team/marp-core": "^4.3.0", diff --git a/package.json b/package.json index 2a15d52..e06b2c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pediatric-ai-scribe", - "version": "7.14.2", + "version": "7.14.3", "description": "AI-powered pediatric clinical documentation platform", "main": "server.js", "scripts": { diff --git a/public/js/assistant/images.js b/public/js/assistant/images.js index 3af6648..f571740 100644 --- a/public/js/assistant/images.js +++ b/public/js/assistant/images.js @@ -77,6 +77,7 @@ export function createAssistantImageStore() { async function saveWithNativeShare(src) { if (isNativeApp()) { + if (await saveWithNativeImageBridge(src)) return true; return await saveWithCapacitorShare(src); } var webShare = await shareWithWebFile(src); @@ -85,6 +86,25 @@ async function saveWithNativeShare(src) { return await saveWithCapacitorShare(src); } +async function saveWithNativeImageBridge(src) { + if (!window.NativeFiles || typeof window.NativeFiles.saveImage !== 'function') return false; + try { + var base64 = await imageSourceToBase64(src); + var name = 'clinical-visual-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '.png'; + var result = String(window.NativeFiles.saveImage(name, base64) || ''); + if (result.indexOf('saved:') === 0) { + if (typeof window.showToast === 'function') window.showToast('Image saved to Photos', 'success'); + return true; + } + if (result.indexOf('error:') === 0 && typeof window.showToast === 'function') { + window.showToast(result.slice(6) || 'Native image save failed', 'error'); + } + } catch (e) { + if (typeof window.showToast === 'function') window.showToast(e.message || 'Native image save failed', 'error'); + } + return false; +} + async function saveWithCapacitorShare(src) { var plugins = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins : null; if (!plugins) return false;