diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 6d36be4..d66849c 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 714013 - versionName "7.14.13" + versionCode 714014 + versionName "7.14.14" 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/package-lock.json b/mobile/package-lock.json index e0bd616..5c14c1a 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -1,12 +1,12 @@ { "name": "pedscribe-mobile", - "version": "7.14.13", + "version": "7.14.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pedscribe-mobile", - "version": "7.14.13", + "version": "7.14.14", "dependencies": { "@aparajita/capacitor-biometric-auth": "^8.0.0", "@capacitor/android": "^6.0.0", diff --git a/mobile/package.json b/mobile/package.json index e8bfe92..a3dac63 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,6 +1,6 @@ { "name": "pedscribe-mobile", - "version": "7.14.13", + "version": "7.14.14", "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 ab9464e..7305413 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pediatric-ai-scribe", - "version": "7.14.13", + "version": "7.14.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pediatric-ai-scribe", - "version": "7.14.13", + "version": "7.14.14", "dependencies": { "@marp-team/marp-cli": "^4.3.1", "@marp-team/marp-core": "^4.3.0", diff --git a/package.json b/package.json index 4ccacc2..72bc66c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pediatric-ai-scribe", - "version": "7.14.13", + "version": "7.14.14", "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 ab5d2ee..17dc514 100644 --- a/public/js/assistant/images.js +++ b/public/js/assistant/images.js @@ -47,11 +47,11 @@ export function createAssistantImageStore() { var downloadUrl = item && item.downloadUrl; if (!src) return; try { - if (await saveWithNativeShare(src)) return; if (downloadUrl) { - downloadFromServer(downloadUrl); + await downloadFromServer(downloadUrl); return; } + if (await saveWithNativeShare(src)) return; if (isNativeApp()) { if (typeof window.showToast === 'function') window.showToast('Image saving is not available in this app build yet. Update the app and try again.', 'error'); return; @@ -147,7 +147,7 @@ async function shareWithWebFile(src) { if (!navigator.canShare({ files: [file] })) return 'unavailable'; await navigator.share({ files: [file], title: 'Clinical visual', text: 'Clinical Assistant generated visual' }); return 'shared'; - } catch (e) { return isShareCancel(e) ? 'cancelled' : 'unavailable'; } + } catch (e) { return isShareCancel(e) ? 'unavailable' : 'unavailable'; } } async function downloadWithBrowser(src) { @@ -167,14 +167,77 @@ async function downloadWithBrowser(src) { setTimeout(function() { URL.revokeObjectURL(objectUrl); }, 60000); } -function downloadFromServer(url) { +async function downloadFromServer(url) { + var response = await fetch(url, { headers: authHeadersForDownload(), credentials: 'same-origin' }); + if (!response.ok) throw new Error('Image download failed'); + var blob = await response.blob(); + var name = 'clinical-visual-' + new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) + '.png'; + if (await saveBlobWithNativeImageBridge(blob, name)) return; + if (await saveBlobWithCapacitorShare(blob, name)) return; + if (await shareBlobWithWebFile(blob, name)) return; + if (isMobileBrowser()) { + if (typeof window.showToast === 'function') window.showToast('Could not start download here. Use Preview and long-press the image to save it.', 'error'); + return; + } + downloadBlobWithAnchor(blob, name); +} + +function authHeadersForDownload() { + var headers = window.getAuthHeaders ? window.getAuthHeaders() : {}; + var clean = {}; + Object.keys(headers || {}).forEach(function(key) { + if (key.toLowerCase() !== 'content-type') clean[key] = headers[key]; + }); + return clean; +} + +async function saveBlobWithNativeImageBridge(blob, name) { + if (!window.NativeFiles || typeof window.NativeFiles.saveImage !== 'function') return false; + try { + var base64 = await blobToBase64(blob); + 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; + } + } catch (e) {} + return false; +} + +async function saveBlobWithCapacitorShare(blob, name) { + var plugins = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins : null; + if (!plugins || !plugins.Filesystem) return false; + try { + var base64 = await blobToBase64(blob); + var saved = await plugins.Filesystem.writeFile({ path: name, data: base64, directory: 'CACHE' }); + if (plugins.Share && saved && saved.uri) { + await plugins.Share.share({ title: 'Clinical visual', text: 'Clinical Assistant generated visual', url: saved.uri, dialogTitle: 'Save or share clinical visual' }); + } + if (typeof window.showToast === 'function') window.showToast('Image prepared', 'success'); + return true; + } catch (e) { return false; } +} + +async function shareBlobWithWebFile(blob, name) { + if (!navigator.share || !navigator.canShare || typeof File === 'undefined') return false; + try { + var file = new File([blob], name, { type: blob.type || 'image/png' }); + if (!navigator.canShare({ files: [file] })) return false; + await navigator.share({ files: [file], title: 'Clinical visual', text: 'Clinical Assistant generated visual' }); + return true; + } catch (e) { return false; } +} + +function downloadBlobWithAnchor(blob, name) { + var objectUrl = URL.createObjectURL(blob); var a = document.createElement('a'); - a.href = url; - a.download = 'clinical-visual.png'; + a.href = objectUrl; + a.download = name; a.rel = 'noopener'; document.body.appendChild(a); a.click(); a.remove(); + setTimeout(function() { URL.revokeObjectURL(objectUrl); }, 60000); } function isMobileBrowser() { @@ -193,6 +256,10 @@ function isShareCancel(error) { async function imageSourceToBase64(src) { if (/^data:image\//i.test(src)) return src.split(',')[1] || ''; var blob = await imageSourceToBlob(src); + return blobToBase64(blob); +} + +async function blobToBase64(blob) { return new Promise(function(resolve, reject) { var reader = new FileReader(); reader.onload = function() { resolve(String(reader.result || '').split(',')[1] || ''); };