Fix Photos save for images + add native Quick Look file preview
Photos/Files bug: the lightbox download handed nativeSaveFile a bare "/files/<id>" with no name, extension, or mime. Empty mime made bzSaveToPhotos short-circuit as "notmedia" — so saveToAlbum (and its permission prompt) never ran, and the image landed in the generic Files folder as an extension-less blob. Now nativeSaveFile trusts the server's Content-Type when the mime is unknown and appends a real extension (bzExtForMime/bzEnsureExt), so images reach the Images folder AND Photos. File preview: replace the @capacitor/share "share sheet" open with a new native FileOpener plugin (QLPreviewController). bzOpenFile now prefers a real Quick Look preview and only falls back to the share sheet if the plugin isn't in the build. Wired file-opener into mobile/package.json and the codemagic SPM diagnostics loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+31
-5
@@ -2809,6 +2809,23 @@ function bzMimeFromName(n){
|
||||
if(/^(mp3|wav|m4a|aac|ogg|opus)$/.test(e)) return 'audio/'+e;
|
||||
return '';
|
||||
}
|
||||
// Map a MIME type to a file extension. Needed because some downloads arrive without one — the lightbox
|
||||
// hands us a bare "/files/<id>" (no name, no extension) — and a file saved with no extension is one that
|
||||
// neither the Photos app will accept nor the Files app / Quick Look can open.
|
||||
function bzExtForMime(mime){
|
||||
const m=String(mime||'').toLowerCase().split(';')[0].trim();
|
||||
const map={'image/jpeg':'jpg','image/jpg':'jpg','image/png':'png','image/gif':'gif','image/webp':'webp','image/heic':'heic','image/heif':'heif','image/bmp':'bmp','image/svg+xml':'svg',
|
||||
'video/mp4':'mp4','video/quicktime':'mov','video/webm':'webm','video/3gpp':'3gp','video/x-matroska':'mkv','video/x-msvideo':'avi',
|
||||
'audio/mpeg':'mp3','audio/mp4':'m4a','audio/aac':'aac','audio/wav':'wav','audio/x-wav':'wav','audio/ogg':'ogg','audio/opus':'opus',
|
||||
'application/pdf':'pdf','application/zip':'zip','text/plain':'txt'};
|
||||
if(map[m]) return map[m];
|
||||
return (m.split('/')[1]||'').replace(/\+.*$/,'').replace(/[^a-z0-9]+/g,'').slice(0,5); // e.g. "svg+xml"→"svg"
|
||||
}
|
||||
function bzEnsureExt(name, mime){
|
||||
const n=String(name||'file');
|
||||
if(/\.[a-z0-9]{1,6}$/i.test(n)) return n; // already carries an extension
|
||||
const ext=bzExtForMime(mime); return ext ? n+'.'+ext : n;
|
||||
}
|
||||
function bzLibSrc(r){ try{ return r&&r.uri ? window.Capacitor.convertFileSrc(r.uri) : null; }catch(_){ return null; } }
|
||||
function bzPhotosOn(){ try{ return localStorage.getItem('bzc.photos')!=='off'; }catch(_){ return true; } }
|
||||
// Also put saved photos/videos in the Photos app, in a "Connect" album — the way WhatsApp does. The Files
|
||||
@@ -2889,11 +2906,14 @@ function bzSyncFileTiles(only){
|
||||
el.title = saved ? 'Open' : 'Download to this device';
|
||||
});
|
||||
}
|
||||
// Open an already-downloaded file via the iOS share/preview sheet (Quick Look / open in another app). We have
|
||||
// no dedicated file-opener plugin, so @capacitor/share on the local file URI is the reliable "open".
|
||||
// Open an already-downloaded file. Prefer a native Quick Look PREVIEW (the FileOpener plugin) — that's the iOS
|
||||
// "look at this file" surface (swipe, pinch, print, its own share button), which is what people expect when
|
||||
// they tap a file. Only if the preview plugin isn't in this build do we fall back to the share sheet (which
|
||||
// offers "open in another app"), then to a new browser tab.
|
||||
async function bzOpenFile(rec, name){
|
||||
const uri=rec&&rec.uri; if(!uri) return;
|
||||
const P=window.Capacitor&&window.Capacitor.Plugins;
|
||||
if(P&&P.FileOpener&&P.FileOpener.preview){ try{ await P.FileOpener.preview({ path:uri }); return; }catch(_){ /* fall through to the share sheet */ } }
|
||||
if(P&&P.Share){ try{ await P.Share.share({ title:name||'File', url:uri }); }catch(_){} return; } // catch = user cancelled
|
||||
try{ const src=bzLibSrc(rec); if(src) window.open(src, '_blank'); }catch(_){}
|
||||
}
|
||||
@@ -2901,13 +2921,19 @@ async function nativeSaveFile(url, name, meta){
|
||||
const C=window.Capacitor, P=C&&C.Plugins; const Fs=P&&P.Filesystem;
|
||||
if(!Fs) return false;
|
||||
const id=(meta&&meta.id) || (String(url).split('?')[0].split('/').pop()||'');
|
||||
const fname=(String(name||url.split('/').pop()||'file').replace(/[\/\\:*?"<>|\r\n]+/g,'_')).slice(0,120)||'file';
|
||||
const mime=(meta&&meta.mime)||bzMimeFromName(fname);
|
||||
let fname=(String(name||url.split('/').pop()||'file').replace(/[\/\\:*?"<>|\r\n]+/g,'_')).slice(0,120)||'file';
|
||||
let mime=(meta&&meta.mime)||bzMimeFromName(fname);
|
||||
const onPct=(meta&&meta.onPct)||null;
|
||||
const box=onPct?null:bzDlShow(fname); // a video reports % inside its own tile, so no floating chip
|
||||
const lpath=bzLibPath(fname, mime, id);
|
||||
try{
|
||||
const res=await fetch(url, {credentials:'include'}); if(!res.ok) throw new Error('http '+res.status);
|
||||
// The lightbox / avatar downloads hand us a bare "/files/<id>" — no name, no extension, no mime — so an
|
||||
// image was landing in the generic "Files" folder as an extension-less blob and never reaching Photos.
|
||||
// Trust the server's Content-Type here, then give the file a real extension, so it goes to the right
|
||||
// folder, opens in Quick Look, AND qualifies for the Photos save below.
|
||||
if(!mime){ mime=((res.headers.get('Content-Type')||'').split(';')[0]||'').trim().toLowerCase(); }
|
||||
fname=bzEnsureExt(fname, mime);
|
||||
const lpath=bzLibPath(fname, mime, id);
|
||||
const total=+(res.headers.get('Content-Length')||0);
|
||||
let uri=null, wrote=0;
|
||||
if(res.body && res.body.getReader){
|
||||
|
||||
Reference in New Issue
Block a user