84642ed13f
CI / test (push) Has been cancelled
Library was rendering one approval chip per device per photo PLUS one lock chip per approved device. That's O(photos × devices) buttons — fine at one or two frames, breaks at four+ (see _bmad-output/.../library-many-frames-design-ideas.md). Concept A from the design memo: - Each photo card stays a square thumb + a single "Manage" row. - Manage row summarises state: "3/5 frames · 🔒 Mom's Place". - A corner-lock badge sits on the thumb itself when any frame has the image locked, so the lock status is glanceable from the grid. - Tapping Manage opens the new ManageImageSheet bottom sheet, which lists every frame with an approve toggle + per-frame lock pill. Lock pill is disabled until the frame is approved. Per-photo widgets drop from O(photos × devices) to O(photos). Works identically at 1 or 50 frames. Curation principle stays "manage photos TO the frame" — same store calls (imagesStore.setApproval, devicesStore.lockImage/unlockImage), just routed through the sheet instead of inline chip rows. 10 new ManageImageSheet unit tests + LibraryView tests rewritten to cover the sheet-open + event-forwarding flow. 358/358 frontend tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 line
16 KiB
JavaScript
1 line
16 KiB
JavaScript
import{A as e,B as t,C as n,F as r,K as i,N as a,S as o,U as s,V as c,Y as l,_ as u,d,f,g as p,h as m,p as h,q as g,t as _,u as v,v as y,x as b,y as x,z as S}from"./_plugin-vue_export-helper-BNDVmFr7.js";import{a as C,i as ee,o as te,s as w}from"./index-Ds9OAB3e.js";import{i as ne,n as T,r as re,t as E}from"./BaseBottomSheet-Bsol3Sat.js";import{t as ie}from"./PullToRefresh-CSjUpm5h.js";import{t as D}from"./DevicePicker-BnLOaG74.js";var O={class:`approve-card`},k=[`src`,`alt`],A={class:`approve-card__body`},ae={class:`approve-card__from`},j={class:`approve-card__date`},M={key:0,class:`approve-card__status`},N={class:`approve-card__actions`},oe=_(n({__name:`ApproveCard`,props:{item:{}},emits:[`updated`],setup(e,{emit:n}){let r=e,c=n,d=C(),f=ne(),_=s(!1),v=s(!1),S=s([]),ee=m(()=>new Date(r.item.sharedAt).toLocaleDateString(void 0,{month:`short`,day:`numeric`,year:`numeric`}));async function te(){_.value=!1,v.value=!0;try{c(`updated`,await d.approveShared(r.item.id,S.value))}finally{v.value=!1,S.value=[]}}async function w(){v.value=!0;try{c(`updated`,await d.declineShared(r.item.id))}finally{v.value=!1}}return(n,r)=>(a(),x(h,null,[p(`div`,O,[p(`img`,{src:e.item.thumbnailUrl,alt:`Photo from ${e.item.sharedBy}`,class:`approve-card__thumb`,loading:`lazy`},null,8,k),p(`div`,A,[p(`p`,ae,[r[3]||=b(`From `,-1),p(`strong`,null,l(e.item.sharedBy),1)]),p(`p`,j,l(ee.value),1),e.item.status===`pending`?y(``,!0):(a(),x(`div`,M,[p(`span`,{class:g([`approve-card__badge`,`approve-card__badge--${e.item.status}`])},l(e.item.status),3)])),p(`div`,N,[e.item.status===`pending`||e.item.status===`declined`?(a(),u(T,{key:0,variant:`primary`,size:`sm`,disabled:v.value,onClick:r[0]||=e=>_.value=!0},{default:t(()=>[b(l(e.item.status===`declined`?`Add anyway`:`Add to frame`),1)]),_:1},8,[`disabled`])):y(``,!0),e.item.status===`pending`||e.item.status===`approved`?(a(),u(T,{key:1,variant:`ghost`,size:`sm`,disabled:v.value,onClick:w},{default:t(()=>[b(l(e.item.status===`approved`?`Remove`:`Decline`),1)]),_:1},8,[`disabled`])):y(``,!0)])])]),o(D,{modelValue:_.value,"onUpdate:modelValue":r[1]||=e=>_.value=e,devices:i(f).devices,selected:S.value,uploading:v.value,"confirm-label":`Add to frames`,"onUpdate:selected":r[2]||=e=>S.value=e,onConfirm:te},null,8,[`modelValue`,`devices`,`selected`,`uploading`])],64))}}),[[`__scopeId`,`data-v-6d3dd8b4`]]),P={key:0,class:`manage__empty`},F={key:1,class:`manage__list`},I={class:`manage__device`},se={class:`manage__device-name`},L={class:`manage__device-meta`},ce=[`disabled`,`aria-label`,`onClick`],R={width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},le={key:0,d:`M7 11V7a5 5 0 0 1 10 0v4`},z={key:1,d:`M7 11V7a5 5 0 0 1 9.9-1`},ue=[`disabled`,`aria-label`,`onClick`],de=_(n({__name:`ManageImageSheet`,props:{modelValue:{type:Boolean},image:{},devices:{}},emits:[`update:modelValue`,`approval`,`lock`],setup(e,{emit:n}){let i=e,c=n,d=s(null),f=s(null);function m(e){return!!i.image?.approvedDeviceIds.includes(e)}function _(e){if(!i.image)return;d.value=e.id;let t=!m(e.id);c(`approval`,{imageId:i.image.id,deviceId:e.id,approved:t}),setTimeout(()=>{d.value=null},200)}function v(e){if(!i.image||!m(e.id))return;f.value=e.id;let t=e.lockedImageId!==i.image.id;c(`lock`,{imageId:i.image.id,deviceId:e.id,locked:t}),setTimeout(()=>{f.value=null},200)}return(n,i)=>(a(),u(E,{"model-value":e.modelValue,label:`Manage frames for this photo`,"onUpdate:modelValue":i[1]||=e=>n.$emit(`update:modelValue`,e)},{default:t(()=>[i[4]||=p(`h2`,{class:`manage__title`},`Manage frames`,-1),i[5]||=p(`p`,{class:`manage__sub`},` Toggle which frames show this photo, or lock it to a frame so it stays visible until you unlock it. `,-1),e.devices.length?(a(),x(`div`,F,[(a(!0),x(h,null,r(e.devices,t=>(a(),x(`div`,{key:t.id,class:`manage__row`},[p(`div`,I,[p(`span`,se,l(t.name),1),p(`span`,L,l(t.orientation),1)]),p(`button`,{type:`button`,class:g([`manage__lock`,{"manage__lock--on":t.lockedImageId===e.image?.id}]),disabled:!m(t.id)||f.value===t.id,"aria-label":t.lockedImageId===e.image?.id?`Unlock from ${t.name}`:`Lock to ${t.name}`,onClick:e=>v(t)},[(a(),x(`svg`,R,[i[2]||=p(`rect`,{x:`3`,y:`11`,width:`18`,height:`11`,rx:`2`,ry:`2`},null,-1),t.lockedImageId===e.image?.id?(a(),x(`path`,le)):(a(),x(`path`,z))])),p(`span`,null,l(t.lockedImageId===e.image?.id?`Locked`:`Rotate`),1)],10,ce),p(`button`,{type:`button`,class:g([`manage__toggle`,{"manage__toggle--on":m(t.id)}]),disabled:d.value===t.id,"aria-label":m(t.id)?`Remove this photo from ${t.name}`:`Add this photo to ${t.name}`,onClick:e=>_(t)},null,10,ue)]))),128))])):(a(),x(`div`,P,` You don't have any frames set up yet. `)),o(T,{variant:`primary`,class:`manage__done`,onClick:i[0]||=e=>n.$emit(`update:modelValue`,!1)},{default:t(()=>[...i[3]||=[b(` Done `,-1)]]),_:1})]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-b562a367`]]),B={class:`share-sheet__field`},V=[`onKeydown`],H={key:0,class:`share-sheet__error`},U={key:1,class:`share-sheet__success`},fe=_(n({__name:`ShareSheet`,props:{modelValue:{type:Boolean},imageId:{}},emits:[`update:modelValue`],setup(e,{emit:n}){let r=e,i=C(),m=s(``),h=s(!1),g=s(``),_=s(``);async function S(){if(g.value=``,_.value=``,m.value.trim()){h.value=!0;try{await i.shareImage(r.imageId,m.value.trim()),_.value=`Invite sent to ${m.value.trim()}`,m.value=``}catch(e){g.value=e instanceof Error?e.message:`Failed to send`}finally{h.value=!1}}}return(n,r)=>(a(),u(E,{"model-value":e.modelValue,label:`Share photo`,"onUpdate:modelValue":r[1]||=e=>n.$emit(`update:modelValue`,e)},{default:t(()=>[r[2]||=p(`h2`,{class:`share-sheet__title`},`Share with someone`,-1),r[3]||=p(`p`,{class:`share-sheet__sub`},`They'll get an email and can add it to their frame.`,-1),p(`div`,B,[c(p(`input`,{"onUpdate:modelValue":r[0]||=e=>m.value=e,type:`email`,class:`share-sheet__input`,placeholder:`their@email.com`,autocomplete:`email`,onKeydown:d(f(S,[`prevent`]),[`enter`])},null,40,V),[[v,m.value]])]),g.value?(a(),x(`p`,H,l(g.value),1)):y(``,!0),_.value?(a(),x(`p`,U,l(_.value),1)):y(``,!0),o(T,{variant:`primary`,class:`share-sheet__btn`,disabled:h.value||!m.value.trim(),onClick:S},{default:t(()=>[b(l(h.value?`Sending…`:`Send invite`),1)]),_:1},8,[`disabled`])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-24296e7b`]]),pe={class:`library`},me={class:`library__header`},he={class:`library__tabs`,role:`tablist`},ge=[`aria-selected`,`onClick`],_e={key:0,class:`library__loading`},ve={key:0,class:`library__empty`},ye={key:1,class:`library__grid`},be={class:`library__thumb`},xe=[`src`,`alt`],Se=[`title`],Ce={class:`library__thumb-actions`},we=[`aria-label`,`title`,`onClick`],Te=[`aria-label`,`disabled`,`onClick`],Ee={key:0,width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},De={key:1,width:`13`,height:`13`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"aria-hidden":`true`},Oe=[`aria-label`,`onClick`],ke=[`onClick`],Ae=[`aria-label`,`onClick`],je={class:`library__manage-summary`},Me={key:0,class:`library__manage-lock`},Ne={class:`library__subtabs`,role:`tablist`},Pe=[`aria-selected`,`onClick`],Fe={key:0,class:`library__loading`},Ie={key:1,class:`library__shared-empty`},Le={class:`library__empty-title`},Re={class:`library__empty-sub`},ze={key:2,class:`library__shared-list`},Be={key:3,class:`library__pagination`},Ve=[`disabled`],He={class:`library__page-info`},Ue=[`disabled`],We={class:`library__sheet-actions`},W=_(n({__name:`LibraryView`,setup(n){let c=w(),d=C(),f=ne(),_=re(),v=ee(),D=te(),O=[{id:`all`,label:`All`},{id:`mine`,label:`Mine`},{id:`shared`,label:`Shared`}];function k(e){return O.some(t=>t.id===e)?e:`all`}let A=s(k(D.query.tab));S(()=>D.query.tab,e=>{let t=k(e);t!==A.value&&(A.value=t,t===`shared`&&I(j.value))});let ae=[{id:`pending`,label:`Pending`},{id:`approved`,label:`Approved`},{id:`declined`,label:`Declined`}],j=s(`pending`),M=s([]),N=s(!1),P=s(1),F=s(1);async function I(e,t=1){N.value=!0;try{let n=await d.fetchSharedImages(e,t);M.value=n.items,P.value=n.page,F.value=n.totalPages}finally{N.value=!1}}function se(e){j.value=e,I(e,1)}function L(e){I(j.value,e)}function ce(e){let t=M.value.findIndex(t=>t.id===e.id);t!==-1&&(M.value[t]=e)}e(()=>{d.fetchImages(),f.fetchDevices(),d.fetchPendingCount(),A.value===`shared`&&I(j.value)});let R=s(null);function le(){R.value?.click()}function z(e){let t=e.target,n=t.files?.[0];t.value=``,n&&(_.init(n),c.push(`/upload`))}function ue(){return window.scrollY===0}async function B(){await Promise.all([d.fetchImages({silent:!0}),d.fetchPendingCount(),f.fetchDevices({silent:!0}),A.value===`shared`?I(j.value,P.value):Promise.resolve()])}let V=m(()=>d.images),H=s(!1),U=s(null);function W(e){U.value=e,H.value=!0}let G=s(null);async function K(e,t){if(!G.value){G.value=e.id;try{await _.initEdit(e,t),c.push(`/upload`)}catch{v.show(`Could not load photo for editing`,`error`)}finally{G.value=null}}}function Ge(e){if(e.cropOrientation)return e.cropOrientation;let t=e.cropParams;return!t?.natW||!t?.natH?null:t.natW>=t.natH?`landscape`:`portrait`}function q(e){let t=Ge(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}function J(e){return f.devices.find(t=>t.lockedImageId===e.id)??null}async function Ke(e,t,n){try{await d.setApproval(e,t,n)}catch{v.show(`Failed to update frame approval`,`error`)}}async function qe(e,t,n){try{n?await f.lockImage(t,e):await f.unlockImage(t)}catch{v.show(`Failed to update lock`,`error`)}}let Y=s(!1),X=s(null),Je=m(()=>X.value===null?null:d.images.find(e=>e.id===X.value)??null);function Ye(e){X.value=e.id,Y.value=!0}function Xe(e){Ke(e.imageId,e.deviceId,e.approved)}function Ze(e){qe(e.imageId,e.deviceId,e.locked)}let Z=s(!1),Q=s(null),$=s(!1);function Qe(e){Q.value=e,Z.value=!0}async function $e(){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`,pe,[o(ie,{"is-at-top":ue,"on-refresh":B},{default:t(()=>[p(`div`,me,[o(T,{variant:`primary`,class:`library__add-btn`,onClick:le},{default:t(()=>[...n[6]||=[b(` + Add Photo `,-1)]]),_:1})]),p(`div`,he,[(a(),x(h,null,r(O,e=>p(`button`,{key:e.id,type:`button`,role:`tab`,"aria-selected":A.value===e.id,class:g([`library__tab`,{"library__tab--active":A.value===e.id}]),onClick:t=>A.value=e.id},l(e.label),11,ge)),64))]),i(d).loading?(a(),x(`div`,_e,`Loading…`)):A.value===`shared`?(a(),x(h,{key:2},[p(`div`,Ne,[(a(),x(h,null,r(ae,e=>p(`button`,{key:e.id,type:`button`,role:`tab`,"aria-selected":j.value===e.id,class:g([`library__subtab`,{"library__subtab--active":j.value===e.id}]),onClick:t=>se(e.id)},l(e.label),11,Pe)),64))]),N.value?(a(),x(`div`,Fe,`Loading…`)):M.value.length===0?(a(),x(`div`,Ie,[n[15]||=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`,Le,l(j.value===`pending`?`No pending photos`:j.value===`approved`?`No approved photos`:`No declined photos`),1),p(`p`,Re,l(j.value===`pending`?`Photos shared with you will appear here.`:`Photos you've added to a frame will appear here.`),1)])):(a(),x(`div`,ze,[(a(!0),x(h,null,r(M.value,e=>(a(),u(oe,{key:e.id,item:e,onUpdated:ce},null,8,[`item`]))),128))])),F.value>1?(a(),x(`div`,Be,[p(`button`,{class:`library__page-btn`,disabled:P.value<=1,onClick:n[0]||=e=>L(P.value-1)},`← Prev`,8,Ve),p(`span`,He,l(P.value)+` / `+l(F.value),1),p(`button`,{class:`library__page-btn`,disabled:P.value>=F.value,onClick:n[1]||=e=>L(P.value+1)},`Next →`,8,Ue)])):y(``,!0)],64)):(a(),x(h,{key:1},[V.value.length===0?(a(),x(`div`,ve,[...n[7]||=[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`,ye,[(a(!0),x(h,null,r(V.value,e=>(a(),x(`div`,{key:e.id,class:`library__item`},[p(`div`,be,[p(`img`,{src:e.thumbnailUrl,alt:e.originalFilename,class:`library__img`,loading:`lazy`},null,8,xe),J(e)?(a(),x(`div`,{key:0,class:`library__thumb-lock`,title:`Locked on ${J(e).name}`,"aria-hidden":`true`},[...n[8]||=[p(`svg`,{width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},[p(`rect`,{x:`3`,y:`11`,width:`18`,height:`11`,rx:`2`,ry:`2`}),p(`path`,{d:`M7 11V7a5 5 0 0 1 10 0v4`})],-1)]],8,Se)):y(``,!0),p(`div`,Ce,[q(e)?(a(),x(`button`,{key:0,class:`library__action-btn library__action-btn--warn`,type:`button`,"aria-label":`Crop orientation does not match ${q(e).name}; tap to re-crop`,title:`Cropped ${Ge(e)}, but ${q(e).name} is set to ${q(e).orientation}.`,onClick:t=>K(e,q(e).id)},[...n[9]||=[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,we)):y(``,!0),p(`button`,{class:`library__action-btn`,type:`button`,"aria-label":`Edit ${e.originalFilename}`,disabled:G.value===e.id,onClick:t=>K(e)},[G.value===e.id?(a(),x(`svg`,De,[...n[11]||=[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`,Ee,[...n[10]||=[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,Te),p(`button`,{class:`library__action-btn`,type:`button`,"aria-label":`Share ${e.originalFilename}`,onClick:t=>W(e.id)},[...n[12]||=[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,Oe),p(`button`,{class:`library__action-btn library__action-btn--danger`,type:`button`,"aria-label":`Delete photo`,onClick:t=>Qe(e.id)},[...n[13]||=[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,ke)])]),i(f).devices.length>0?(a(),x(`button`,{key:0,type:`button`,class:`library__manage`,"aria-label":`Manage frames for ${e.originalFilename}`,onClick:t=>Ye(e)},[p(`span`,je,[p(`b`,null,l(e.approvedDeviceIds.length),1),b(`/`+l(i(f).devices.length)+` `+l(i(f).devices.length===1?`frame`:`frames`)+` `,1),J(e)?(a(),x(`span`,Me,`· 🔒 `+l(J(e).name),1)):y(``,!0)]),n[14]||=p(`span`,{class:`library__manage-action`},`Manage ▸`,-1)],8,Ae)):y(``,!0)]))),128))]))],64))]),_:1}),U.value===null?y(``,!0):(a(),u(fe,{key:0,modelValue:H.value,"onUpdate:modelValue":n[2]||=e=>H.value=e,"image-id":U.value},null,8,[`modelValue`,`image-id`])),o(de,{modelValue:Y.value,"onUpdate:modelValue":n[3]||=e=>Y.value=e,image:Je.value,devices:i(f).devices,onApproval:Xe,onLock:Ze},null,8,[`modelValue`,`image`,`devices`]),o(E,{modelValue:Z.value,"onUpdate:modelValue":n[5]||=e=>Z.value=e,label:`Delete photo`},{default:t(()=>[n[17]||=p(`h2`,{class:`library__sheet-title`},`Delete this photo?`,-1),n[18]||=p(`p`,{class:`library__sheet-sub`},`It will be removed from all frames.`,-1),p(`div`,We,[o(T,{variant:`secondary`,onClick:n[4]||=e=>Z.value=!1},{default:t(()=>[...n[16]||=[b(`Cancel`,-1)]]),_:1}),o(T,{variant:`destructive`,disabled:$.value,onClick:$e},{default:t(()=>[b(l($.value?`Deleting…`:`Delete`),1)]),_:1},8,[`disabled`])])]),_:1},8,[`modelValue`]),p(`input`,{ref_key:`fileInputEl`,ref:R,type:`file`,accept:`image/jpeg,image/png,image/webp,image/gif`,hidden:``,onChange:z},null,544)]))}}),[[`__scopeId`,`data-v-5de304c0`]]);export{W as default}; |