Files
pictureFrame-webApp/public/build/assets/LibraryView-E5I_Q1_A.js
T
football2801 00121aaec9
CI / test (push) Has been cancelled
feat(pwa): installable app — manifest + SW + Settings install button
The captive-portal Step-2 QR opens pictureframe.edholm.me in Safari,
which is the perfect moment to also offer "pin this to your home
screen" so the recipient gets one-tap access without typing the URL
again. Two pieces:

* Service worker at /sw.js (document root, scope "/"). Minimal —
  install/activate calls skipWaiting + clients.claim, fetch is
  passthrough. Real offline caching is intentionally out of scope;
  we only need the SW to exist so Chrome's PWA-install heuristic
  fires.

* Settings → Install app section, hidden when display-mode standalone.
  Android Chrome path: native beforeinstallprompt button.
  iOS Safari (and any other non-prompt browser): button opens a
  modal with step-by-step Share → Add to Home Screen instructions.

usePwaInstall composable handles the singleton lifecycle —
beforeinstallprompt fires once per page load and may fire before the
user navigates to Settings, so we register on module import and stash
the event for later.

Tests cover: install button rendered when not standalone, modal opens
on click without a native prompt, modal close button works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:49:12 -04:00

1 line
14 KiB
JavaScript

import{B as e,G as t,H as n,J as r,K as i,M as a,P as o,R as s,S as c,_ as l,b as u,d,f,g as p,h as m,k as h,p as g,t as _,u as v,v as y,x as b,y as x,z as S}from"./_plugin-vue_export-helper-eepT72yB.js";import{a as C,i as w,o as T,s as E}from"./index-BO5caB_f.js";import{i as D,n as O,r as ee,t as k}from"./BaseBottomSheet-BMI-Oljh.js";import{t as te}from"./PullToRefresh-BEXU4J3A.js";import{t as A}from"./DevicePicker-C6ucVR6N.js";var j={class:`approve-card`},M=[`src`,`alt`],N={class:`approve-card__body`},P={class:`approve-card__from`},F={class:`approve-card__date`},I={key:0,class:`approve-card__status`},L={class:`approve-card__actions`},R=_(c({__name:`ApproveCard`,props:{item:{}},emits:[`updated`],setup(e,{emit:o}){let s=e,c=o,d=C(),f=D(),h=n(!1),_=n(!1),v=n([]),w=m(()=>new Date(s.item.sharedAt).toLocaleDateString(void 0,{month:`short`,day:`numeric`,year:`numeric`}));async function T(){h.value=!1,_.value=!0;try{c(`updated`,await d.approveShared(s.item.id,v.value))}finally{_.value=!1,v.value=[]}}async function E(){_.value=!0;try{c(`updated`,await d.declineShared(s.item.id))}finally{_.value=!1}}return(n,o)=>(a(),x(g,null,[p(`div`,j,[p(`img`,{src:e.item.thumbnailUrl,alt:`Photo from ${e.item.sharedBy}`,class:`approve-card__thumb`,loading:`lazy`},null,8,M),p(`div`,N,[p(`p`,P,[o[3]||=u(`From `,-1),p(`strong`,null,r(e.item.sharedBy),1)]),p(`p`,F,r(w.value),1),e.item.status===`pending`?y(``,!0):(a(),x(`div`,I,[p(`span`,{class:i([`approve-card__badge`,`approve-card__badge--${e.item.status}`])},r(e.item.status),3)])),p(`div`,L,[e.item.status===`pending`||e.item.status===`declined`?(a(),l(O,{key:0,variant:`primary`,size:`sm`,disabled:_.value,onClick:o[0]||=e=>h.value=!0},{default:S(()=>[u(r(e.item.status===`declined`?`Add anyway`:`Add to frame`),1)]),_:1},8,[`disabled`])):y(``,!0),e.item.status===`pending`||e.item.status===`approved`?(a(),l(O,{key:1,variant:`ghost`,size:`sm`,disabled:_.value,onClick:E},{default:S(()=>[u(r(e.item.status===`approved`?`Remove`:`Decline`),1)]),_:1},8,[`disabled`])):y(``,!0)])])]),b(A,{modelValue:h.value,"onUpdate:modelValue":o[1]||=e=>h.value=e,devices:t(f).devices,selected:v.value,uploading:_.value,"confirm-label":`Add to frames`,"onUpdate:selected":o[2]||=e=>v.value=e,onConfirm:T},null,8,[`modelValue`,`devices`,`selected`,`uploading`])],64))}}),[[`__scopeId`,`data-v-6d3dd8b4`]]),z={class:`share-sheet__field`},B=[`onKeydown`],V={key:0,class:`share-sheet__error`},H={key:1,class:`share-sheet__success`},ne=_(c({__name:`ShareSheet`,props:{modelValue:{type:Boolean},imageId:{}},emits:[`update:modelValue`],setup(t,{emit:i}){let o=t,s=C(),c=n(``),m=n(!1),h=n(``),g=n(``);async function _(){if(h.value=``,g.value=``,c.value.trim()){m.value=!0;try{await s.shareImage(o.imageId,c.value.trim()),g.value=`Invite sent to ${c.value.trim()}`,c.value=``}catch(e){h.value=e instanceof Error?e.message:`Failed to send`}finally{m.value=!1}}}return(n,i)=>(a(),l(k,{"model-value":t.modelValue,label:`Share photo`,"onUpdate:modelValue":i[1]||=e=>n.$emit(`update:modelValue`,e)},{default:S(()=>[i[2]||=p(`h2`,{class:`share-sheet__title`},`Share with someone`,-1),i[3]||=p(`p`,{class:`share-sheet__sub`},`They'll get an email and can add it to their frame.`,-1),p(`div`,z,[e(p(`input`,{"onUpdate:modelValue":i[0]||=e=>c.value=e,type:`email`,class:`share-sheet__input`,placeholder:`their@email.com`,autocomplete:`email`,onKeydown:d(f(_,[`prevent`]),[`enter`])},null,40,B),[[v,c.value]])]),h.value?(a(),x(`p`,V,r(h.value),1)):y(``,!0),g.value?(a(),x(`p`,H,r(g.value),1)):y(``,!0),b(O,{variant:`primary`,class:`share-sheet__btn`,disabled:m.value||!c.value.trim(),onClick:_},{default:S(()=>[u(r(m.value?`Sending…`:`Send invite`),1)]),_:1},8,[`disabled`])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-24296e7b`]]),re={class:`library`},ie={class:`library__header`},ae={class:`library__tabs`,role:`tablist`},oe=[`aria-selected`,`onClick`],se={key:0,class:`library__loading`},ce={key:0,class:`library__empty`},le={key:1,class:`library__grid`},ue={class:`library__thumb`},de=[`src`,`alt`],fe={class:`library__thumb-actions`},pe=[`aria-label`,`title`,`onClick`],me=[`aria-label`,`disabled`,`onClick`],he={key:0,width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},ge={key:1,width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"aria-hidden":`true`},_e=[`aria-label`,`onClick`],ve=[`onClick`],ye={key:0,class:`library__approvals`},be=[`aria-label`,`onClick`],xe={key:1,class:`library__locks`},Se=[`aria-label`,`onClick`],Ce={width:`10`,height:`10`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},we={key:0,d:`M7 11V7a5 5 0 0 1 10 0v4`},Te={key:1,d:`M7 11V7a5 5 0 0 1 9.9-1`},Ee={class:`library__subtabs`,role:`tablist`},De=[`aria-selected`,`onClick`],Oe={key:0,class:`library__loading`},ke={key:1,class:`library__shared-empty`},Ae={class:`library__empty-title`},je={class:`library__empty-sub`},Me={key:2,class:`library__shared-list`},Ne={key:3,class:`library__pagination`},Pe=[`disabled`],Fe={class:`library__page-info`},Ie=[`disabled`],Le={class:`library__sheet-actions`},U=_(c({__name:`LibraryView`,setup(e){let c=E(),d=C(),f=D(),_=ee(),v=w(),A=T(),j=[{id:`all`,label:`All`},{id:`mine`,label:`Mine`},{id:`shared`,label:`Shared`}];function M(e){return j.some(t=>t.id===e)?e:`all`}let N=n(M(A.query.tab));s(()=>A.query.tab,e=>{let t=M(e);t!==N.value&&(N.value=t,t===`shared`&&V(F.value))});let P=[{id:`pending`,label:`Pending`},{id:`approved`,label:`Approved`},{id:`declined`,label:`Declined`}],F=n(`pending`),I=n([]),L=n(!1),z=n(1),B=n(1);async function V(e,t=1){L.value=!0;try{let n=await d.fetchSharedImages(e,t);I.value=n.items,z.value=n.page,B.value=n.totalPages}finally{L.value=!1}}function H(e){F.value=e,V(e,1)}function U(e){V(F.value,e)}function Re(e){let t=I.value.findIndex(t=>t.id===e.id);t!==-1&&(I.value[t]=e)}h(()=>{d.fetchImages(),f.fetchDevices(),d.fetchPendingCount(),N.value===`shared`&&V(F.value)});function ze(){let e=document.createElement(`input`);e.type=`file`,e.accept=`image/jpeg,image/png,image/webp,image/gif`,e.onchange=()=>{let t=e.files?.[0];t&&(_.init(t),c.push(`/upload`))},e.click()}function Be(){return window.scrollY===0}async function Ve(){await Promise.all([d.fetchImages({silent:!0}),d.fetchPendingCount(),f.fetchDevices({silent:!0}),N.value===`shared`?V(F.value,z.value):Promise.resolve()])}let W=m(()=>d.images),G=n(!1),K=n(null);function He(e){K.value=e,G.value=!0}let q=n(null);async function J(e,t){if(!q.value){q.value=e.id;try{await _.initEdit(e,t),c.push(`/upload`)}catch{v.show(`Could not load photo for editing`,`error`)}finally{q.value=null}}}function Y(e){if(e.cropOrientation)return e.cropOrientation;let t=e.cropParams;return!t?.natW||!t?.natH?null:t.natW>=t.natH?`landscape`:`portrait`}function X(e){let t=Y(e);if(!t)return null;for(let n of e.approvedDeviceIds){let e=f.devices.find(e=>e.id===n);if(e&&e.orientation!==t)return e}return null}async function Ue(e,t){try{t.lockedImageId===e?await f.unlockImage(t.id):await f.lockImage(t.id,e)}catch{v.show(`Failed to update lock`,`error`)}}async function We(e,t,n){try{await d.setApproval(e,t,n)}catch{v.show(`Failed to update frame approval`,`error`)}}let Z=n(!1),Q=n(null),$=n(!1);function Ge(e){Q.value=e,Z.value=!0}async function Ke(){if(Q.value){$.value=!0;try{await d.deleteImage(Q.value),Z.value=!1,v.show(`Photo deleted`,`success`)}catch{v.show(`Delete failed`,`error`)}finally{$.value=!1}}}return(e,n)=>(a(),x(`main`,re,[b(te,{"is-at-top":Be,"on-refresh":Ve},{default:S(()=>[p(`div`,ie,[b(O,{variant:`primary`,class:`library__add-btn`,onClick:ze},{default:S(()=>[...n[5]||=[u(` + Add Photo `,-1)]]),_:1})]),p(`div`,ae,[(a(),x(g,null,o(j,e=>p(`button`,{key:e.id,type:`button`,role:`tab`,"aria-selected":N.value===e.id,class:i([`library__tab`,{"library__tab--active":N.value===e.id}]),onClick:t=>N.value=e.id},r(e.label),11,oe)),64))]),t(d).loading?(a(),x(`div`,se,`Loading…`)):N.value===`shared`?(a(),x(g,{key:2},[p(`div`,Ee,[(a(),x(g,null,o(P,e=>p(`button`,{key:e.id,type:`button`,role:`tab`,"aria-selected":F.value===e.id,class:i([`library__subtab`,{"library__subtab--active":F.value===e.id}]),onClick:t=>H(e.id)},r(e.label),11,De)),64))]),L.value?(a(),x(`div`,Oe,`Loading…`)):I.value.length===0?(a(),x(`div`,ke,[n[13]||=p(`svg`,{width:`48`,height:`48`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.5`,"aria-hidden":`true`},[p(`circle`,{cx:`18`,cy:`5`,r:`3`}),p(`circle`,{cx:`6`,cy:`12`,r:`3`}),p(`circle`,{cx:`18`,cy:`19`,r:`3`}),p(`line`,{x1:`8.59`,y1:`13.51`,x2:`15.42`,y2:`17.49`}),p(`line`,{x1:`15.41`,y1:`6.51`,x2:`8.59`,y2:`10.49`})],-1),p(`p`,Ae,r(F.value===`pending`?`No pending photos`:F.value===`approved`?`No approved photos`:`No declined photos`),1),p(`p`,je,r(F.value===`pending`?`Photos shared with you will appear here.`:`Photos you've added to a frame will appear here.`),1)])):(a(),x(`div`,Me,[(a(!0),x(g,null,o(I.value,e=>(a(),l(R,{key:e.id,item:e,onUpdated:Re},null,8,[`item`]))),128))])),B.value>1?(a(),x(`div`,Ne,[p(`button`,{class:`library__page-btn`,disabled:z.value<=1,onClick:n[0]||=e=>U(z.value-1)},`← Prev`,8,Pe),p(`span`,Fe,r(z.value)+` / `+r(B.value),1),p(`button`,{class:`library__page-btn`,disabled:z.value>=B.value,onClick:n[1]||=e=>U(z.value+1)},`Next →`,8,Ie)])):y(``,!0)],64)):(a(),x(g,{key:1},[W.value.length===0?(a(),x(`div`,ce,[...n[6]||=[p(`svg`,{width:`48`,height:`48`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.5`,"aria-hidden":`true`},[p(`rect`,{x:`3`,y:`3`,width:`18`,height:`18`,rx:`2`}),p(`circle`,{cx:`8.5`,cy:`8.5`,r:`1.5`}),p(`polyline`,{points:`21,15 16,10 5,21`})],-1),p(`p`,{class:`library__empty-title`},`No photos yet`,-1),p(`p`,{class:`library__empty-sub`},`Tap "+ Add Photo" above to upload your first one.`,-1)]])):(a(),x(`div`,le,[(a(!0),x(g,null,o(W.value,e=>(a(),x(`div`,{key:e.id,class:`library__item`},[p(`div`,ue,[p(`img`,{src:e.thumbnailUrl,alt:e.originalFilename,class:`library__img`,loading:`lazy`},null,8,de),p(`div`,fe,[X(e)?(a(),x(`button`,{key:0,class:`library__action-btn library__action-btn--warn`,type:`button`,"aria-label":`Crop orientation does not match ${X(e).name}; tap to re-crop`,title:`Cropped ${Y(e)}, but ${X(e).name} is set to ${X(e).orientation}.`,onClick:t=>J(e,X(e).id)},[...n[7]||=[p(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},[p(`path`,{d:`M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z`}),p(`line`,{x1:`12`,y1:`9`,x2:`12`,y2:`13`}),p(`line`,{x1:`12`,y1:`17`,x2:`12.01`,y2:`17`})],-1)]],8,pe)):y(``,!0),p(`button`,{class:`library__action-btn`,type:`button`,"aria-label":`Edit ${e.originalFilename}`,disabled:q.value===e.id,onClick:t=>J(e)},[q.value===e.id?(a(),x(`svg`,ge,[...n[9]||=[p(`circle`,{cx:`12`,cy:`12`,r:`10`},null,-1),p(`line`,{x1:`12`,y1:`8`,x2:`12`,y2:`12`},null,-1),p(`line`,{x1:`12`,y1:`16`,x2:`12.01`,y2:`16`},null,-1)]])):(a(),x(`svg`,he,[...n[8]||=[p(`path`,{d:`M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7`},null,-1),p(`path`,{d:`M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z`},null,-1)]]))],8,me),p(`button`,{class:`library__action-btn`,type:`button`,"aria-label":`Share ${e.originalFilename}`,onClick:t=>He(e.id)},[...n[10]||=[p(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},[p(`circle`,{cx:`18`,cy:`5`,r:`3`}),p(`circle`,{cx:`6`,cy:`12`,r:`3`}),p(`circle`,{cx:`18`,cy:`19`,r:`3`}),p(`line`,{x1:`8.59`,y1:`13.51`,x2:`15.42`,y2:`17.49`}),p(`line`,{x1:`15.41`,y1:`6.51`,x2:`8.59`,y2:`10.49`})],-1)]],8,_e),p(`button`,{class:`library__action-btn library__action-btn--danger`,type:`button`,"aria-label":`Delete photo`,onClick:t=>Ge(e.id)},[...n[11]||=[p(`svg`,{width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},[p(`polyline`,{points:`3 6 5 6 21 6`}),p(`path`,{d:`M19 6l-1 14H6L5 6`}),p(`path`,{d:`M10 11v6M14 11v6`}),p(`path`,{d:`M9 6V4h6v2`})],-1)]],8,ve)])]),t(f).devices.length>0?(a(),x(`div`,ye,[(a(!0),x(g,null,o(t(f).devices,t=>(a(),x(`button`,{key:t.id,class:i([`library__approval-chip`,{"library__approval-chip--on":e.approvedDeviceIds.includes(t.id)}]),type:`button`,"aria-label":`${e.approvedDeviceIds.includes(t.id)?`Remove from`:`Add to`} ${t.name}`,onClick:n=>We(e.id,t.id,!e.approvedDeviceIds.includes(t.id))},r(t.name),11,be))),128))])):y(``,!0),t(f).devices.length>0?(a(),x(`div`,xe,[(a(!0),x(g,null,o(t(f).devices.filter(t=>e.approvedDeviceIds.includes(t.id)),t=>(a(),x(`button`,{key:t.id,class:i([`library__lock-chip`,{"library__lock-chip--on":t.lockedImageId===e.id}]),type:`button`,"aria-label":`${t.lockedImageId===e.id?`Unlock from`:`Lock to`} ${t.name}`,onClick:n=>Ue(e.id,t)},[(a(),x(`svg`,Ce,[n[12]||=p(`rect`,{x:`3`,y:`11`,width:`18`,height:`11`,rx:`2`,ry:`2`},null,-1),t.lockedImageId===e.id?(a(),x(`path`,we)):(a(),x(`path`,Te))])),u(` `+r(t.name),1)],10,Se))),128))])):y(``,!0)]))),128))]))],64))]),_:1}),K.value===null?y(``,!0):(a(),l(ne,{key:0,modelValue:G.value,"onUpdate:modelValue":n[2]||=e=>G.value=e,"image-id":K.value},null,8,[`modelValue`,`image-id`])),b(k,{modelValue:Z.value,"onUpdate:modelValue":n[4]||=e=>Z.value=e,label:`Delete photo`},{default:S(()=>[n[15]||=p(`h2`,{class:`library__sheet-title`},`Delete this photo?`,-1),n[16]||=p(`p`,{class:`library__sheet-sub`},`It will be removed from all frames.`,-1),p(`div`,Le,[b(O,{variant:`secondary`,onClick:n[3]||=e=>Z.value=!1},{default:S(()=>[...n[14]||=[u(`Cancel`,-1)]]),_:1}),b(O,{variant:`destructive`,disabled:$.value,onClick:Ke},{default:S(()=>[u(r($.value?`Deleting…`:`Delete`),1)]),_:1},8,[`disabled`])])]),_:1},8,[`modelValue`])]))}}),[[`__scopeId`,`data-v-d22a9085`]]);export{U as default};