feat(photos): downloaded media also lands in a "Connect" album in Photos
The Files folder is the app's own copy — it powers offline playback and Manage
storage — but Files is not where anyone looks for photos and videos. The Photos app
is, and only PhotoKit can write there; @capacitor/filesystem cannot, because an
app sandbox and the photo library are separate stores. So this adds a small native
plugin, mirroring the existing audio-route one.
- mobile/plugins/media-library: saveToAlbum({path, album, kind}) finds or creates
the album and adds the asset. Uses addResource(with:fileURL:), which is uniform
for photo and video and non-optional, unlike creationRequestForAssetFrom*, which
can silently no-op.
- Requests .readWrite, NOT .addOnly: addOnly can add an asset but cannot look up or
create an ALBUM, which is the whole point here. Both photo-library usage strings
are already set by ios-patch.sh.
- If the album can't be resolved (e.g. "limited" access), the asset is still saved
to the camera roll — landing somewhere beats failing outright. A racing create
from two simultaneous downloads re-looks-up instead of erroring.
- Podspec named MediaLibrary.podspec with s.name = 'MediaLibrary' to match
PascalCase of the package name — the same trap that broke the AudioRoute build.
Checked by a script: pod name, jsName and declared-vs-implemented methods.
Entirely best-effort from the web side: a denied permission or an older app build
never fails a download that is already safe in the app folder. Added a Settings
toggle since this does keep a second copy of the file.
Needs a new iOS build — new native plugin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+21
-2
@@ -1197,7 +1197,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<script src="/icons.js?v=6"></script>
|
||||
<script>window.__BUILD='2026-07-23-batch164';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
<script>window.__BUILD='2026-07-23-batch165';console.log('%cBiz Connect','color:#1F3B73;font-weight:bold','build '+window.__BUILD);
|
||||
// Emoji are rendered with the OS's own (colour) emoji font — instant, zero network.
|
||||
//
|
||||
// We used to run Twemoji over every emoji, which swapped each one for an <img> pulled INDIVIDUALLY from
|
||||
@@ -1479,6 +1479,7 @@ function openSettings(){
|
||||
// NOTE: deliberately NO "web build" row here — users must not have to reason about an app version vs a
|
||||
// web build. New code applies itself (see checkWebBuild/scheduleAutoRefresh); the only version shown is
|
||||
// the desktop app's, which is what the auto-updater manages.
|
||||
+(bzLibOn()?sw('setPhotos','Save photos & videos to the Photos app', bzPhotosOn()):'')
|
||||
+(bzLibOn()?('<label class="gi-setting"><span>Storage<div style="font-size:.72rem;color:var(--muted);font-weight:400;margin-top:.15rem">Photos, videos & files you downloaded to this device</div></span><button class="btn sm" id="setStor">Manage</button></label>'):'')
|
||||
+'<div class="hint" style="margin-top:.4rem">These preferences are saved on this device.</div></div>';
|
||||
document.body.appendChild(ov);
|
||||
@@ -1505,6 +1506,8 @@ function openSettings(){
|
||||
ov.querySelector('#setDm').onchange=e=>setPref('dm', e.target.checked);
|
||||
const permBtn=ov.querySelector('#setPerm'); if(permBtn) permBtn.onclick=async()=>{ try{ const r=await Notification.requestPermission(); if(r==='granted'){ try{ await subscribePush(); }catch(_){} } }catch(_){} ov.remove(); openSettings(); }; // just trigger the browser prompt + refresh the state (no toast)
|
||||
const storBtn=ov.querySelector('#setStor'); if(storBtn) storBtn.onclick=()=>{ ov.remove(); openStorage(); };
|
||||
const phBox=ov.querySelector('#setPhotos'); // second copy in the Photos library — worth being able to turn off
|
||||
if(phBox) phBox.onchange=e=>{ try{ localStorage.setItem('bzc.photos', e.target.checked?'on':'off'); }catch(_){} };
|
||||
}
|
||||
// Manage storage: everything this device has downloaded, with sizes, and a way to remove it. Deleting
|
||||
// here only removes the LOCAL copy — the message and its attachment stay on the server, so anything
|
||||
@@ -2788,6 +2791,21 @@ function bzMimeFromName(n){
|
||||
return '';
|
||||
}
|
||||
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
|
||||
// folder is the app's own copy (it powers offline playback and Manage storage), but Files is not where
|
||||
// people look for media. Only PhotoKit can write to the photo library, hence the media-library plugin.
|
||||
// Best-effort throughout: a denied permission or an older app build must never fail a download that has
|
||||
// already landed safely in the app folder.
|
||||
async function bzSaveToPhotos(uri, mime){
|
||||
if(!uri || !bzPhotosOn()) return false;
|
||||
const isVid=/^video\//.test(mime||'');
|
||||
if(!isVid && !/^image\//.test(mime||'')) return false; // documents don't belong in Photos
|
||||
const ML=window.Capacitor&&window.Capacitor.Plugins&&window.Capacitor.Plugins.MediaLibrary;
|
||||
if(!ML||!ML.saveToAlbum) return false; // app build predates the plugin
|
||||
try{ await ML.saveToAlbum({ path:uri, album:'Connect', kind:isVid?'video':'photo' }); return true; }
|
||||
catch(e){ return false; }
|
||||
}
|
||||
// Confirm the file is still on disk. Returns null (and forgets it) if the user deleted it in Files.
|
||||
async function bzLibVerify(id){
|
||||
const r=bzLibGet(id); if(!r) return null;
|
||||
@@ -2889,7 +2907,8 @@ async function nativeSaveFile(url, name, meta){
|
||||
bzDlSet(box,100); if(onPct) onPct(100);
|
||||
if(id) bzLibPut(id, { path:lpath, uri:uri||'', name:fname, mime, size:wrote||total||0, at:Date.now() });
|
||||
if(id) bzSyncVideoTiles(id);
|
||||
toast('Saved to Biz Connect · '+bzLibFolder(mime));
|
||||
const inPhotos=await bzSaveToPhotos(uri, mime);
|
||||
toast(inPhotos ? 'Saved to Photos · Connect album' : ('Saved to Biz Connect · '+bzLibFolder(mime)));
|
||||
}catch(e){ toast("Couldn't save the file"); }
|
||||
finally{ bzDlHide(box); }
|
||||
return true; // handled either way — never fall through to navigation
|
||||
|
||||
Reference in New Issue
Block a user