fix mobile image save to photos
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m40s
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m40s
This commit is contained in:
parent
747ec7a89e
commit
2bbdb88ceb
7 changed files with 105 additions and 8 deletions
|
|
@ -9,8 +9,8 @@ android {
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
// Version values below are overwritten by scripts/release.sh from
|
// Version values below are overwritten by scripts/release.sh from
|
||||||
// the root package.json. versionCode auto-increments per release.
|
// the root package.json. versionCode auto-increments per release.
|
||||||
versionCode 714002
|
versionCode 714003
|
||||||
versionName "7.14.2"
|
versionName "7.14.3"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
package com.pedshub.scribe;
|
package com.pedshub.scribe;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
import android.print.PrintAttributes;
|
import android.print.PrintAttributes;
|
||||||
import android.print.PrintDocumentAdapter;
|
import android.print.PrintDocumentAdapter;
|
||||||
import android.print.PrintManager;
|
import android.print.PrintManager;
|
||||||
|
import android.provider.MediaStore;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.webkit.PermissionRequest;
|
import android.webkit.PermissionRequest;
|
||||||
import android.webkit.WebChromeClient;
|
import android.webkit.WebChromeClient;
|
||||||
|
|
@ -20,6 +26,10 @@ import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
|
|
||||||
private static final int MIC_PERMISSION_CODE = 1001;
|
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.
|
// Register JS interface for Android's print / Save as PDF flow.
|
||||||
setupPrintBridge();
|
setupPrintBridge();
|
||||||
|
|
||||||
|
// Register JS interface for saving generated visuals to Photos.
|
||||||
|
setupFileBridge();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── WebView Microphone Permission ──────────────────────────
|
// ── WebView Microphone Permission ──────────────────────────
|
||||||
|
|
@ -95,6 +108,11 @@ public class MainActivity extends BridgeActivity {
|
||||||
webView.addJavascriptInterface(new PrintBridge(this), "NativePrint");
|
webView.addJavascriptInterface(new PrintBridge(this), "NativePrint");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupFileBridge() {
|
||||||
|
WebView webView = this.bridge.getWebView();
|
||||||
|
webView.addJavascriptInterface(new FileBridge(this), "NativeFiles");
|
||||||
|
}
|
||||||
|
|
||||||
public static class RecordingBridge {
|
public static class RecordingBridge {
|
||||||
private final MainActivity activity;
|
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) {
|
private void printHtmlFromBase64(String title, String base64Html) {
|
||||||
try {
|
try {
|
||||||
byte[] decoded = Base64.decode(base64Html, Base64.DEFAULT);
|
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);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
mobile/package-lock.json
generated
4
mobile/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "pedscribe-mobile",
|
"name": "pedscribe-mobile",
|
||||||
"version": "7.14.2",
|
"version": "7.14.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pedscribe-mobile",
|
"name": "pedscribe-mobile",
|
||||||
"version": "7.14.2",
|
"version": "7.14.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aparajita/capacitor-biometric-auth": "^8.0.0",
|
"@aparajita/capacitor-biometric-auth": "^8.0.0",
|
||||||
"@capacitor/android": "^6.0.0",
|
"@capacitor/android": "^6.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "pedscribe-mobile",
|
"name": "pedscribe-mobile",
|
||||||
"version": "7.14.2",
|
"version": "7.14.3",
|
||||||
"description": "PedScribe native mobile app — Capacitor wrapper for Pediatric AI Scribe",
|
"description": "PedScribe native mobile app — Capacitor wrapper for Pediatric AI Scribe",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "pediatric-ai-scribe",
|
"name": "pediatric-ai-scribe",
|
||||||
"version": "7.14.2",
|
"version": "7.14.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pediatric-ai-scribe",
|
"name": "pediatric-ai-scribe",
|
||||||
"version": "7.14.2",
|
"version": "7.14.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marp-team/marp-cli": "^4.3.1",
|
"@marp-team/marp-cli": "^4.3.1",
|
||||||
"@marp-team/marp-core": "^4.3.0",
|
"@marp-team/marp-core": "^4.3.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "pediatric-ai-scribe",
|
"name": "pediatric-ai-scribe",
|
||||||
"version": "7.14.2",
|
"version": "7.14.3",
|
||||||
"description": "AI-powered pediatric clinical documentation platform",
|
"description": "AI-powered pediatric clinical documentation platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ export function createAssistantImageStore() {
|
||||||
|
|
||||||
async function saveWithNativeShare(src) {
|
async function saveWithNativeShare(src) {
|
||||||
if (isNativeApp()) {
|
if (isNativeApp()) {
|
||||||
|
if (await saveWithNativeImageBridge(src)) return true;
|
||||||
return await saveWithCapacitorShare(src);
|
return await saveWithCapacitorShare(src);
|
||||||
}
|
}
|
||||||
var webShare = await shareWithWebFile(src);
|
var webShare = await shareWithWebFile(src);
|
||||||
|
|
@ -85,6 +86,25 @@ async function saveWithNativeShare(src) {
|
||||||
return await saveWithCapacitorShare(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) {
|
async function saveWithCapacitorShare(src) {
|
||||||
var plugins = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins : null;
|
var plugins = window.Capacitor && window.Capacitor.Plugins ? window.Capacitor.Plugins : null;
|
||||||
if (!plugins) return false;
|
if (!plugins) return false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue