00121aaec9
CI / test (push) Has been cancelled
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>
1 line
5.1 KiB
JavaScript
1 line
5.1 KiB
JavaScript
import{D as e,E as t,F as n,H as r,K as i,L as a,M as o,R as s,S as c,_ as l,a as u,d,f,g as p,m,q as h,r as g,t as _,v,x as y,y as b,z as x}from"./_plugin-vue_export-helper-eepT72yB.js";var S=g(`devices`,()=>{let e=r([]),t=r(!1),n=r(null);async function i(r={}){r.silent||(t.value=!0),n.value=null;try{let t=await fetch(`/api/devices`);if(!t.ok)throw Error(`Failed to load devices`);e.value=await t.json()}catch(e){n.value=e instanceof Error?e.message:`Unknown error`}finally{t.value=!1}}async function a(t,n){let r=await fetch(`/api/devices/${t}`,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(n)});if(!r.ok)throw Error(`Failed to update device`);let i=await r.json(),a=e.value.findIndex(e=>e.id===t);return a!==-1&&(e.value[a]=i),i}async function o(t,n){let r=await fetch(`/api/devices/${t}/lock`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:JSON.stringify({imageId:n})});if(!r.ok)throw Error(`Failed to lock image`);let i=await r.json(),a=e.value.findIndex(e=>e.id===t);return a!==-1&&(e.value[a]=i),i}async function s(t){if(!(await fetch(`/api/devices/${t}`,{method:`DELETE`})).ok)throw Error(`Failed to remove device`);e.value=e.value.filter(e=>e.id!==t)}async function c(t){let n=await fetch(`/api/devices/${t}/lock`,{method:`DELETE`});if(!n.ok)throw Error(`Failed to unlock`);let r=await n.json(),i=e.value.findIndex(e=>e.id===t);return i!==-1&&(e.value[i]=r),r}return{devices:e,loading:t,error:n,fetchDevices:i,updateDevice:a,removeDevice:s,lockImage:o,unlockImage:c}}),C=g(`upload`,()=>{let e=r(null),t=r(null),n=r(null),i=r(null),a=r(null),o=r(null),s=r([]),c=r(null),l=r([]),u=r(null);function d(n,r){_(),e.value=n,t.value=URL.createObjectURL(n),c.value=r??null,l.value=r?[r]:[]}async function f(n,r){_();let i=await(await fetch(n.originalUrl)).blob();e.value=new File([i],n.originalFilename,{type:i.type}),t.value=URL.createObjectURL(i),u.value=n.id,a.value=n.cropParams??null,o.value=n.cropOrientation??null,s.value=n.stickerState?[...n.stickerState]:[],l.value=n.approvedDeviceIds,c.value=r??null}function p(e,t,r){i.value&&URL.revokeObjectURL(i.value),n.value=e,i.value=URL.createObjectURL(e),a.value=t,o.value=r}function m(e){s.value=[...s.value,e]}function h(e,t){s.value=s.value.map(n=>n.id===e?{...n,...t}:n)}function g(e){s.value=s.value.filter(t=>t.id!==e)}function _(){t.value&&URL.revokeObjectURL(t.value),i.value&&URL.revokeObjectURL(i.value),e.value=null,t.value=null,n.value=null,i.value=null,a.value=null,o.value=null,s.value=[],c.value=null,l.value=[],u.value=null}return{originalFile:e,originalUrl:t,croppedBlob:n,croppedUrl:i,cropParams:a,cropOrientation:o,stickers:s,contextDeviceId:c,selectedDeviceIds:l,editingImageId:u,init:d,initEdit:f,setCrop:p,addSticker:m,updateSticker:h,removeSticker:g,cleanup:_}}),w={key:0,class:`btn__spinner`,"aria-hidden":`true`},T=_(c({__name:`BaseButton`,props:{variant:{default:`primary`},tag:{default:`button`},type:{default:`button`},disabled:{type:Boolean,default:!1},loading:{type:Boolean,default:!1}},setup(e){return(r,i)=>(o(),l(a(e.tag),t({type:e.tag===`button`?e.type:void 0,disabled:e.disabled||e.loading,class:[`btn`,`btn--${e.variant}`,{"btn--loading":e.loading}]},r.$attrs),{default:x(()=>[e.loading?(o(),b(`span`,w)):v(``,!0),n(r.$slots,`default`,{},void 0,!0)]),_:3},16,[`type`,`disabled`,`class`]))}}),[[`__scopeId`,`data-v-7d3f1e61`]]),E=[`aria-label`],D=80,O=_(c({__name:`BaseBottomSheet`,props:{modelValue:{type:Boolean},label:{}},emits:[`update:modelValue`],setup(t,{emit:a}){let c=t,g=a,_=r(null),S=r(0),C=r(!1),w=0,T=null,O=null;function k(){g(`update:modelValue`,!1)}function A(e){w=e.touches[0].clientY,C.value=!0,S.value=0}function j(e){if(!C.value)return;let t=e.touches[0].clientY-w;S.value=t>0?t:0}function M(){C.value&&(C.value=!1,S.value>D&&k(),S.value=0)}function N(e){e.pointerType!==`touch`&&(w=e.clientY,C.value=!0,S.value=0,T=e.pointerId,e.currentTarget.setPointerCapture(e.pointerId),window.addEventListener(`pointermove`,P),window.addEventListener(`pointerup`,F),window.addEventListener(`pointercancel`,F))}function P(e){if(!C.value||e.pointerId!==T)return;let t=e.clientY-w;S.value=t>0?t:0}function F(e){e.pointerId===T&&(T=null,window.removeEventListener(`pointermove`,P),window.removeEventListener(`pointerup`,F),window.removeEventListener(`pointercancel`,F),M())}return s(()=>c.modelValue,async t=>{t?(O=document.activeElement,await e(),_.value?.focus()):(O?.focus(),O=null,S.value=0,C.value=!1)}),(e,r)=>(o(),l(m,{to:`body`},[y(u,{name:`sheet`},{default:x(()=>[t.modelValue?(o(),b(`div`,{key:0,class:`sheet-overlay`,role:`dialog`,"aria-label":t.label,"aria-modal":`true`,onClick:f(k,[`self`]),onKeydown:d(k,[`esc`])},[p(`div`,{ref_key:`sheetRef`,ref:_,class:i([`sheet`,{"sheet--dragging":C.value}]),style:h(S.value>0?{transform:`translateY(${S.value}px)`}:void 0),tabindex:`-1`},[p(`div`,{class:`sheet__handle-target`,onTouchstartPassive:A,onTouchmovePassive:j,onTouchend:M,onTouchcancel:M,onPointerdown:N,"aria-hidden":`true`},[...r[0]||=[p(`div`,{class:`sheet__handle`},null,-1)]],32),n(e.$slots,`default`,{},void 0,!0)],6)],40,E)):v(``,!0)]),_:3})]))}}),[[`__scopeId`,`data-v-967683c3`]]);export{S as i,T as n,C as r,O as t}; |