From 747ec7a89ec3af995d7ffe71bb4027d415777da7 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 10 May 2026 23:56:51 +0200 Subject: [PATCH] fix mobile export save actions --- mobile/android/app/build.gradle | 4 +- .../java/com/pedshub/scribe/MainActivity.java | 47 +++++++++++++++++++ .../app/src/main/res/xml/file_paths.xml | 4 +- mobile/package-lock.json | 4 +- mobile/package.json | 2 +- package-lock.json | 4 +- package.json | 2 +- public/js/assistant/export.js | 20 ++++++-- public/js/assistant/images.js | 12 ++++- 9 files changed, 85 insertions(+), 14 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 18f1a93..0387929 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 714001 - versionName "7.14.1" + versionCode 714002 + versionName "7.14.2" 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 19932c0..3fabeab 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,11 +1,17 @@ package com.pedshub.scribe; import android.Manifest; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintManager; +import android.util.Base64; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; +import android.webkit.WebViewClient; import android.webkit.WebView; import androidx.annotation.NonNull; @@ -18,6 +24,7 @@ public class MainActivity extends BridgeActivity { private static final int MIC_PERMISSION_CODE = 1001; private PermissionRequest pendingPermissionRequest; + private WebView printWebView; @Override protected void onCreate(Bundle savedInstanceState) { @@ -35,6 +42,9 @@ public class MainActivity extends BridgeActivity { // Register JS interface for foreground service control setupRecordingBridge(); + + // Register JS interface for Android's print / Save as PDF flow. + setupPrintBridge(); } // ── WebView Microphone Permission ────────────────────────── @@ -80,6 +90,11 @@ public class MainActivity extends BridgeActivity { webView.addJavascriptInterface(new RecordingBridge(this), "NativeRecording"); } + private void setupPrintBridge() { + WebView webView = this.bridge.getWebView(); + webView.addJavascriptInterface(new PrintBridge(this), "NativePrint"); + } + public static class RecordingBridge { private final MainActivity activity; @@ -100,4 +115,36 @@ public class MainActivity extends BridgeActivity { activity.startService(intent); } } + + public static class PrintBridge { + private final MainActivity activity; + + PrintBridge(MainActivity activity) { + this.activity = activity; + } + + @android.webkit.JavascriptInterface + public void printHtml(String title, String base64Html) { + activity.runOnUiThread(() -> activity.printHtmlFromBase64(title, base64Html)); + } + } + + private void printHtmlFromBase64(String title, String base64Html) { + try { + byte[] decoded = Base64.decode(base64Html, Base64.DEFAULT); + String html = new String(decoded, java.nio.charset.StandardCharsets.UTF_8); + printWebView = new WebView(this); + printWebView.setWebViewClient(new WebViewClient() { + @Override + public void onPageFinished(WebView view, String url) { + PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); + PrintDocumentAdapter adapter = view.createPrintDocumentAdapter(title != null && !title.isEmpty() ? title : "Clinical Assistant Export"); + printManager.print(title != null && !title.isEmpty() ? title : "Clinical Assistant Export", adapter, new PrintAttributes.Builder().build()); + } + }); + printWebView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null); + } catch (Exception e) { + android.util.Log.e("PedScribe", "Native print failed", e); + } + } } diff --git a/mobile/android/app/src/main/res/xml/file_paths.xml b/mobile/android/app/src/main/res/xml/file_paths.xml index bd0c4d8..c7bcaad 100644 --- a/mobile/android/app/src/main/res/xml/file_paths.xml +++ b/mobile/android/app/src/main/res/xml/file_paths.xml @@ -2,4 +2,6 @@ - \ No newline at end of file + + + diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 27a14b6..045905d 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -1,12 +1,12 @@ { "name": "pedscribe-mobile", - "version": "7.14.1", + "version": "7.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pedscribe-mobile", - "version": "7.14.1", + "version": "7.14.2", "dependencies": { "@aparajita/capacitor-biometric-auth": "^8.0.0", "@capacitor/android": "^6.0.0", diff --git a/mobile/package.json b/mobile/package.json index 1b652de..b7be2ec 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,6 +1,6 @@ { "name": "pedscribe-mobile", - "version": "7.14.1", + "version": "7.14.2", "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 51d7896..f7e4c79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pediatric-ai-scribe", - "version": "7.14.1", + "version": "7.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pediatric-ai-scribe", - "version": "7.14.1", + "version": "7.14.2", "dependencies": { "@marp-team/marp-cli": "^4.3.1", "@marp-team/marp-core": "^4.3.0", diff --git a/package.json b/package.json index db4cad6..2a15d52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pediatric-ai-scribe", - "version": "7.14.1", + "version": "7.14.2", "description": "AI-powered pediatric clinical documentation platform", "main": "server.js", "scripts": { diff --git a/public/js/assistant/export.js b/public/js/assistant/export.js index fc8d194..2944b2a 100644 --- a/public/js/assistant/export.js +++ b/public/js/assistant/export.js @@ -78,7 +78,9 @@ export function createAssistantExporter(options) { var originalText = printBtn.textContent; printBtn.textContent = 'Preparing export...'; try { - if (!(await saveInlineExport(buildPrintableChatHtml(items, imageSrc, false)))) window.print(); + var html = buildPrintableChatHtml(items, imageSrc, false); + if (await printWithNativeBridge(html)) return; + if (!(await saveInlineExport(html))) window.print(); } finally { printBtn.disabled = false; printBtn.textContent = originalText; @@ -150,7 +152,7 @@ async function saveInlineExport(html) { if (!plugins || !plugins.Filesystem) return false; try { var name = 'clinical-assistant-export-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '.html'; - var saved = await plugins.Filesystem.writeFile({ path: name, data: utf8ToBase64(html), directory: 'DOCUMENTS' }); + var saved = await plugins.Filesystem.writeFile({ path: name, data: utf8ToBase64(html), directory: 'CACHE' }); if (plugins.Share && saved && saved.uri) { try { await plugins.Share.share({ title: 'Clinical Assistant Export', text: 'Open this export and use Print to save as PDF.', url: saved.uri, dialogTitle: 'Save or share export' }); @@ -158,9 +160,21 @@ async function saveInlineExport(html) { if (!isShareCancel(e)) throw e; } } - if (typeof window.showToast === 'function') window.showToast('Export saved to Documents', 'success'); + if (typeof window.showToast === 'function') window.showToast('Export prepared', 'success'); return true; } catch (e) { + if (typeof window.showToast === 'function') window.showToast(e.message || 'Export failed', 'error'); + return false; + } +} + +async function printWithNativeBridge(html) { + if (!window.NativePrint || typeof window.NativePrint.printHtml !== 'function') return false; + try { + window.NativePrint.printHtml('Clinical Assistant Export', utf8ToBase64(html)); + return true; + } catch (e) { + if (typeof window.showToast === 'function') window.showToast(e.message || 'Native print failed', 'error'); return false; } } diff --git a/public/js/assistant/images.js b/public/js/assistant/images.js index be04d32..3af6648 100644 --- a/public/js/assistant/images.js +++ b/public/js/assistant/images.js @@ -76,8 +76,16 @@ export function createAssistantImageStore() { } async function saveWithNativeShare(src) { + if (isNativeApp()) { + return await saveWithCapacitorShare(src); + } var webShare = await shareWithWebFile(src); if (webShare !== 'unavailable') return true; + + return await saveWithCapacitorShare(src); +} + +async function saveWithCapacitorShare(src) { var plugins = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins : null; if (!plugins) return false; if (!plugins.Filesystem && plugins.Share && !/^data:image\//i.test(src)) { @@ -90,7 +98,7 @@ async function saveWithNativeShare(src) { try { var base64 = await imageSourceToBase64(src); var name = 'clinical-visual-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '.png'; - var saved = await plugins.Filesystem.writeFile({ path: name, data: base64, directory: 'DOCUMENTS' }); + var saved = await plugins.Filesystem.writeFile({ path: name, data: base64, directory: 'CACHE' }); if (plugins.Share && saved && saved.uri) { try { await plugins.Share.share({ title: 'Clinical visual', text: 'Clinical Assistant generated visual', url: saved.uri, dialogTitle: 'Save or share clinical visual' }); @@ -98,7 +106,7 @@ async function saveWithNativeShare(src) { if (!isShareCancel(e)) throw e; } } - if (typeof window.showToast === 'function') window.showToast('Image saved to Documents', 'success'); + if (typeof window.showToast === 'function') window.showToast('Image prepared', 'success'); return true; } catch (e) { if (typeof window.showToast === 'function') window.showToast('Could not save with the app. Opening browser download instead.', 'error');