4002ff9fbf
CI / test (push) Has been cancelled
Web app: new entities (Image, RenderedAsset, SharedImage, Token, DeviceImageHistory), enums, repositories, controllers, message handlers, migrations, tests, frontend upload/library/sticker UI, Vue components. Firmware: EPD background screen binaries + gen scripts, setup_bg header. Infra: ddev config, test bundle, gitignore coverage dir. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 line
15 KiB
JavaScript
1 line
15 KiB
JavaScript
import{E as e,H as t,K as n,N as r,R as i,T as a,_ as o,d as s,dt as c,f as l,g as u,j as d,k as f,l as p,o as m,p as h,pt as g,t as _,u as v,v as y,w as b,z as x}from"./_plugin-vue_export-helper-DVo1OUMD.js";import{a as S,i as C,s as w}from"./index-6y_HJqaF.js";import{i as T,n as E,r as D,t as O}from"./BaseBottomSheet-YbQyMMAQ.js";import{t as k}from"./DevicePicker-C9BLciJE.js";var A={key:0,class:`crop-editor__label`},j={class:`crop-editor__actions`},M=_(y({__name:`CropEditor`,props:{src:{},orientation:{},deviceName:{},initialParams:{}},emits:[`crop`],setup(n,{emit:r}){let s=n,c=r,d=s.orientation===`landscape`?1600:960,p=s.orientation===`landscape`?960:1600,m=d/p,_=t(),y=t(),b=null,S=null,C=0,w=t(0),T=t(0),D=t(1),O={x:0,y:0,w:0,h:0},k=1;function M(){let e=y.value,t=_.value;if(!e||!t)return;let n=t.getBoundingClientRect(),r=n.height-80,i=n.width;e.width=i,e.height=r,b=e.getContext(`2d`);let a=i-48,o=r-48,s,c;a/o>m?(c=o,s=c*m):(s=a,c=s/m),O={x:(i-s)/2,y:(r-c)/2,w:s,h:c},S&&N()}function N(){S&&(k=Math.max(O.w/S.naturalWidth,O.h/S.naturalHeight),s.initialParams?P(s.initialParams):(D.value=1,w.value=0,T.value=0,I()))}function P(e){if(!S)return;let t=O.w/e.natW;D.value=t/k,w.value=t*(S.naturalWidth/2-e.natX-e.natW/2),T.value=t*(S.naturalHeight/2-e.natY-e.natH/2);let[n,r]=F(w.value,T.value);w.value=n,T.value=r,I()}function F(e,t){if(!S)return[e,t];let n=k*D.value,r=S.naturalWidth*n,i=S.naturalHeight*n,a=(r-O.w)/2,o=(i-O.h)/2;return[Math.max(-a,Math.min(a,e)),Math.max(-o,Math.min(o,t))]}function I(){if(!b||!S||!y.value)return;let{width:e,height:t}=y.value,n=k*D.value,r=S.naturalWidth*n,i=S.naturalHeight*n,a=O.x+O.w/2+w.value,o=O.y+O.h/2+T.value,s=a-r/2,c=o-i/2;b.clearRect(0,0,e,t),b.drawImage(S,s,c,r,i),b.save(),b.fillStyle=`rgba(0,0,0,0.55)`,b.fillRect(0,0,e,t),b.globalCompositeOperation=`destination-out`,b.fillRect(O.x,O.y,O.w,O.h),b.restore(),b.strokeStyle=`#fff`,b.lineWidth=2,b.strokeRect(O.x,O.y,O.w,O.h),b.lineWidth=3,[[O.x,O.y,20,0,0,20],[O.x+O.w,O.y,-20,0,0,20],[O.x,O.y+O.h,20,0,0,-20],[O.x+O.w,O.y+O.h,-20,0,0,-20]].forEach(([e,t,n,r,i,a])=>{b.beginPath(),b.moveTo(e+n,t+r),b.lineTo(e,t),b.lineTo(e+i,t+a),b.stroke()})}let L=new Map,R=0;function z(e){if(y.value?.setPointerCapture(e.pointerId),L.set(e.pointerId,{x:e.clientX,y:e.clientY}),L.size===2){let e=[...L.values()];R=Math.hypot(e[1].x-e[0].x,e[1].y-e[0].y)}}function B(e){if(!L.has(e.pointerId))return;let t=L.get(e.pointerId);if(L.set(e.pointerId,{x:e.clientX,y:e.clientY}),L.size===1){let n=e.clientX-t.x,r=e.clientY-t.y,[i,a]=F(w.value+n,T.value+r);w.value=i,T.value=a,H();return}if(L.size===2){let e=[...L.values()],t=Math.hypot(e[1].x-e[0].x,e[1].y-e[0].y);if(R>0){let e=t/R;D.value=Math.max(1,D.value*e);let[n,r]=F(w.value,T.value);w.value=n,T.value=r,H()}R=t}}function V(e){L.delete(e.pointerId),R=0}function H(){cancelAnimationFrame(C),C=requestAnimationFrame(I)}async function U(){if(!S)return;let e=k*D.value,t=O.x+O.w/2+w.value,n=O.y+O.h/2+T.value,r=t-S.naturalWidth*e/2,i=n-S.naturalHeight*e/2,a=(O.x-r)/e,o=(O.y-i)/e,s=O.w/e,l=O.h/e,u=new OffscreenCanvas(d,p);u.getContext(`2d`).drawImage(S,a,o,s,l,0,0,d,p),c(`crop`,{blob:await u.convertToBlob({type:`image/jpeg`,quality:.92}),params:{natX:a,natY:o,natW:s,natH:l}})}let W=new ResizeObserver(M);return e(()=>{_.value&&W.observe(_.value),M(),S=new Image,S.onload=()=>{M(),N()},S.src=s.src}),i(()=>s.src,e=>{S&&(S.onload=()=>N(),S.src=e)}),a(()=>{W.disconnect(),cancelAnimationFrame(C)}),(e,t)=>(f(),h(`div`,{class:`crop-editor`,ref_key:`containerRef`,ref:_},[v(`canvas`,{ref_key:`canvasRef`,ref:y,class:`crop-editor__canvas`,onPointerdown:z,onPointermove:B,onPointerup:V,onPointercancel:V},null,544),n.deviceName?(f(),h(`div`,A,g(n.deviceName),1)):l(``,!0),v(`div`,j,[o(E,{variant:`primary`,class:`crop-editor__use-btn`,onClick:U},{default:x(()=>[...t[0]||=[u(` Use this crop `,-1)]]),_:1})])],512))}}),[[`__scopeId`,`data-v-ec2eb68c`]]),N=[{id:`seasonal`,label:`Seasonal`},{id:`holidays`,label:`Holidays`},{id:`fun`,label:`Fun`},{id:`family`,label:`Family`},{id:`nature`,label:`Nature`}],P=[{id:`sea-snow`,category:`seasonal`,label:`Snowflake`,emoji:`❄️`},{id:`sea-sun`,category:`seasonal`,label:`Sun`,emoji:`☀️`},{id:`sea-leaves`,category:`seasonal`,label:`Autumn`,emoji:`🍂`},{id:`sea-blossom`,category:`seasonal`,label:`Blossom`,emoji:`🌸`},{id:`sea-snowman`,category:`seasonal`,label:`Snowman`,emoji:`⛄`},{id:`hol-tree`,category:`holidays`,label:`Tree`,emoji:`🎄`},{id:`hol-gift`,category:`holidays`,label:`Gift`,emoji:`🎁`},{id:`hol-heart`,category:`holidays`,label:`Heart`,emoji:`❤️`},{id:`hol-party`,category:`holidays`,label:`Party`,emoji:`🎉`},{id:`hol-cake`,category:`holidays`,label:`Cake`,emoji:`🎂`},{id:`fun-star`,category:`fun`,label:`Star`,emoji:`⭐`},{id:`fun-rainbow`,category:`fun`,label:`Rainbow`,emoji:`🌈`},{id:`fun-balloon`,category:`fun`,label:`Balloon`,emoji:`🎈`},{id:`fun-sparkle`,category:`fun`,label:`Sparkles`,emoji:`✨`},{id:`fun-fire`,category:`fun`,label:`Fire`,emoji:`🔥`},{id:`fam-house`,category:`family`,label:`Home`,emoji:`🏠`},{id:`fam-paw`,category:`family`,label:`Paw`,emoji:`🐾`},{id:`fam-camera`,category:`family`,label:`Camera`,emoji:`📷`},{id:`fam-plane`,category:`family`,label:`Airplane`,emoji:`✈️`},{id:`fam-music`,category:`family`,label:`Music`,emoji:`🎵`},{id:`nat-tree`,category:`nature`,label:`Tree`,emoji:`🌲`},{id:`nat-flower`,category:`nature`,label:`Flower`,emoji:`🌺`},{id:`nat-bee`,category:`nature`,label:`Bee`,emoji:`🐝`},{id:`nat-fly`,category:`nature`,label:`Butterfly`,emoji:`🦋`},{id:`nat-moon`,category:`nature`,label:`Moon`,emoji:`🌙`}],F={class:`sticker-tray`},I={class:`sticker-tray__cats`,role:`tablist`},L=[`onClick`],R={class:`sticker-tray__grid`,role:`tabpanel`},z=[`aria-label`,`onClick`],B={class:`sticker-tray__emoji`,"aria-hidden":`true`},V={class:`sticker-tray__label`},H=_(y({__name:`StickerTray`,props:{modelValue:{type:Boolean}},emits:[`update:modelValue`,`pick`],setup(e){let r=t(`seasonal`),i=p(()=>P.filter(e=>e.category===r.value));return(t,a)=>(f(),s(O,{"model-value":e.modelValue,label:`Add sticker`,"onUpdate:modelValue":a[0]||=e=>t.$emit(`update:modelValue`,e)},{default:x(()=>[v(`div`,F,[v(`div`,I,[(f(!0),h(m,null,d(n(N),e=>(f(),h(`button`,{key:e.id,type:`button`,role:`tab`,class:c([`sticker-tray__cat`,{"sticker-tray__cat--active":r.value===e.id}]),onClick:t=>r.value=e.id},g(e.label),11,L))),128))]),v(`div`,R,[(f(!0),h(m,null,d(i.value,e=>(f(),h(`button`,{key:e.id,type:`button`,class:`sticker-tray__item`,"aria-label":e.label,onClick:n=>t.$emit(`pick`,e.id)},[v(`span`,B,g(e.emoji),1),v(`span`,V,g(e.label),1)],8,z))),128))])])]),_:1},8,[`model-value`]))}}),[[`__scopeId`,`data-v-7eada75b`]]),U={class:`sticker-canvas__bar`},W=52,G=_(y({__name:`StickerCanvas`,props:{croppedUrl:{},orientation:{},stickers:{}},emits:[`add-sticker`,`update-sticker`,`remove-sticker`,`done`],setup(n,{emit:c}){let g=n,_=c,y=t(),S=t(),C=t(),w=t(),T=t(!1),D=t(null),O=t(375),k=t(225),A=g.orientation===`landscape`?1600/960:960/1600;function j(){if(!y.value)return;let{width:e,height:t}=y.value.getBoundingClientRect(),n=t-72;e/n>A?(k.value=n,O.value=n*A):(O.value=e,k.value=e/A),F()}let M=new ResizeObserver(j);e(()=>{y.value&&M.observe(y.value),j(),ne()}),a(()=>{M.disconnect(),re()});let N=t(null);function F(){let e=new Image;e.onload=()=>{N.value=e},e.src=g.croppedUrl}i(()=>g.croppedUrl,()=>F(),{immediate:!0});let I=p(()=>({width:O.value,height:k.value})),L=p(()=>({image:N.value,x:0,y:0,width:O.value,height:k.value})),R={enabledAnchors:[`top-left`,`top-right`,`bottom-left`,`bottom-right`],rotateEnabled:!0,borderStroke:`rgba(255,255,255,0.8)`,anchorFill:`#fff`,anchorSize:18,keepRatio:!0,boundBoxFunc:(e,t)=>t};function z(e){return{id:e.id,text:B(e.type),fontSize:W,fontFamily:`"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif`,x:e.x,y:e.y,scaleX:e.scale,scaleY:e.scale,rotation:e.rotation,draggable:!0,offsetX:W/2,offsetY:W/2}}function B(e){return P.find(t=>t.id===e)?.emoji??`⭐`}function V(e,t){t.cancelBubble=!0,D.value=e,b(()=>{let t=(w.value?.getNode())?.findOne(`#${e}`),n=C.value?.getNode();t&&n&&n.nodes([t])})}function G(e){e.target===e.target.getStage()&&(D.value=null,C.value?.getNode()?.nodes([]))}function K(){D.value&&(_(`remove-sticker`,D.value),D.value=null,C.value?.getNode()?.nodes([]))}function q(e,t){_(`update-sticker`,e,{x:t.target.x(),y:t.target.y()})}function J(e,t){_(`update-sticker`,e,{x:t.target.x(),y:t.target.y(),scale:t.target.scaleX(),rotation:t.target.rotation()})}function ee(e){let t={id:`${e}-${Date.now()}`,type:e,x:O.value/2,y:k.value/2,scale:1,rotation:0};_(`add-sticker`,t),T.value=!1,b(()=>V(t.id,{cancelBubble:!1}))}let Y=0,X=1;function Z(e){let t=e[0].clientX-e[1].clientX,n=e[0].clientY-e[1].clientY;return Math.hypot(t,n)}function Q(e){e.touches.length!==2||!D.value||(Y=Z(e.touches),X=g.stickers.find(e=>e.id===D.value)?.scale??1)}function $(e){if(e.touches.length!==2||!D.value||Y===0)return;e.preventDefault();let t=Math.max(.2,Math.min(6,X*(Z(e.touches)/Y)));_(`update-sticker`,D.value,{scale:t})}function te(){Y=0,X=1}function ne(){let e=y.value;e&&(e.addEventListener(`touchstart`,Q,{passive:!0}),e.addEventListener(`touchmove`,$,{passive:!1}),e.addEventListener(`touchend`,te,{passive:!0}))}function re(){let e=y.value;e&&(e.removeEventListener(`touchstart`,Q),e.removeEventListener(`touchmove`,$),e.removeEventListener(`touchend`,te))}async function ie(){D.value=null,C.value?.getNode()?.nodes([]),await b();let e=S.value?.getNode();if(!e)return;let t=(g.orientation===`landscape`?1600:960)/O.value,n=await e.toBlob({pixelRatio:t,mimeType:`image/jpeg`,quality:.92});n&&_(`done`,n)}return(e,t)=>{let i=r(`v-image`),a=r(`v-layer`),c=r(`v-text`),p=r(`v-transformer`),g=r(`v-stage`);return f(),h(`div`,{class:`sticker-canvas`,ref_key:`containerRef`,ref:y},[o(g,{ref_key:`stageRef`,ref:S,config:I.value,onClick:G,onTap:G},{default:x(()=>[o(a,null,{default:x(()=>[o(i,{config:L.value},null,8,[`config`])]),_:1}),o(a,{ref_key:`stickerLayerRef`,ref:w},{default:x(()=>[(f(!0),h(m,null,d(n.stickers,e=>(f(),s(c,{key:e.id,config:z(e),onClick:t=>V(e.id,t),onTap:t=>V(e.id,t),onDragend:t=>q(e.id,t),onTransformend:t=>J(e.id,t)},null,8,[`config`,`onClick`,`onTap`,`onDragend`,`onTransformend`]))),128)),o(p,{ref_key:`transformerRef`,ref:C,config:R},null,512)]),_:1},512)]),_:1},8,[`config`]),D.value?(f(),h(`button`,{key:0,class:`sticker-canvas__delete`,type:`button`,"aria-label":`Remove sticker`,onClick:K},[...t[2]||=[v(`svg`,{width:`16`,height:`16`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},[v(`line`,{x1:`18`,y1:`6`,x2:`6`,y2:`18`}),v(`line`,{x1:`6`,y1:`6`,x2:`18`,y2:`18`})],-1)]])):l(``,!0),v(`div`,U,[v(`button`,{class:`sticker-canvas__add-btn`,type:`button`,onClick:t[0]||=e=>T.value=!0},[...t[3]||=[v(`svg`,{width:`20`,height:`20`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2`,"aria-hidden":`true`},[v(`circle`,{cx:`12`,cy:`12`,r:`10`}),v(`line`,{x1:`12`,y1:`8`,x2:`12`,y2:`16`}),v(`line`,{x1:`8`,y1:`12`,x2:`16`,y2:`12`})],-1),u(` Add sticker `,-1)]]),o(E,{variant:`primary`,class:`sticker-canvas__next-btn`,onClick:ie},{default:x(()=>[...t[4]||=[u(`Next`,-1)]]),_:1})]),o(H,{modelValue:T.value,"onUpdate:modelValue":t[1]||=e=>T.value=e,onPick:ee},null,8,[`modelValue`])],512)}}}),[[`__scopeId`,`data-v-fb52db70`]]),K={class:`upload-view`},q={class:`upload-view__header`},J=[`aria-label`],ee={key:0,width:`20`,height:`20`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},Y={key:1,width:`20`,height:`20`,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"aria-hidden":`true`},X={class:`upload-view__step-label`},Z={key:2,class:`upload-view__done`},Q={class:`upload-view__done-title`},$=_(y({__name:`UploadView`,setup(r){let i=w(),a=D(),c=T(),d=S(),m=C(),_=t(`crop`),y=t(!1),b=t(!1),O=null,A=p(()=>a.editingImageId!==null);e(async()=>{if(await c.fetchDevices(),!a.originalFile){i.replace(`/`);return}_.value=`crop`});let j=p(()=>a.contextDeviceId?c.devices.find(e=>e.id===a.contextDeviceId):c.devices[0]),N=p(()=>j.value?.orientation??`landscape`),P=p(()=>j.value?.name),F=p(()=>_.value===`crop`?A.value?`Edit crop`:`Crop photo`:_.value===`stickers`?`Add stickers`:A.value?`Updated`:`Added`);function I({blob:e,params:t}){a.setCrop(e,t),_.value=`stickers`}function L(){a.croppedBlob&&(O=a.croppedBlob,A.value?B():b.value=!0)}function R(e){O=e,A.value?B():b.value=!0}function z(){if(_.value===`crop`){a.cleanup(),i.replace(`/library`);return}_.value===`stickers`&&(_.value=`crop`)}async function B(){if(O){y.value=!0;try{let e=new File([O],`photo.jpg`,{type:`image/jpeg`});if(A.value){await d.reprocessImage(a.editingImageId,e,{cropParams:a.cropParams??void 0,stickerState:a.stickers}),b.value=!1,_.value=`done`;return}let t=await d.uploadImage(e,{original:a.originalFile??void 0,cropParams:a.cropParams??void 0,stickerState:a.stickers});await Promise.all(a.selectedDeviceIds.map(e=>d.setApproval(t.id,e,!0))),b.value=!1,_.value=`done`}catch(e){m.show(e instanceof Error?e.message:`Upload failed`,`error`)}finally{y.value=!1}}}function V(){a.cleanup(),i.replace(`/library`)}return(e,t)=>(f(),h(`div`,K,[v(`header`,q,[_.value===`done`?l(``,!0):(f(),h(`button`,{key:0,class:`upload-view__back`,type:`button`,"aria-label":_.value===`crop`?`Cancel`:`Back`,onClick:z},[_.value===`crop`?(f(),h(`svg`,ee,[...t[2]||=[v(`line`,{x1:`18`,y1:`6`,x2:`6`,y2:`18`},null,-1),v(`line`,{x1:`6`,y1:`6`,x2:`18`,y2:`18`},null,-1)]])):(f(),h(`svg`,Y,[...t[3]||=[v(`polyline`,{points:`15 18 9 12 15 6`},null,-1)]]))],8,J)),v(`span`,X,g(F.value),1),_.value===`stickers`?(f(),h(`button`,{key:1,class:`upload-view__skip`,type:`button`,onClick:L},`Skip`)):l(``,!0)]),_.value===`crop`&&n(a).originalUrl?(f(),s(M,{key:0,src:n(a).originalUrl,orientation:N.value,"device-name":P.value,"initial-params":n(a).cropParams,class:`upload-view__stage`,onCrop:I},null,8,[`src`,`orientation`,`device-name`,`initial-params`])):_.value===`stickers`&&n(a).croppedUrl?(f(),s(G,{key:1,"cropped-url":n(a).croppedUrl,orientation:N.value,stickers:n(a).stickers,class:`upload-view__stage`,onAddSticker:n(a).addSticker,onUpdateSticker:n(a).updateSticker,onRemoveSticker:n(a).removeSticker,onDone:R},null,8,[`cropped-url`,`orientation`,`stickers`,`onAddSticker`,`onUpdateSticker`,`onRemoveSticker`])):_.value===`done`?(f(),h(`div`,Z,[t[5]||=v(`div`,{class:`upload-view__done-icon`,"aria-hidden":`true`},`🎉`,-1),v(`p`,Q,g(A.value?`Photo updated!`:`Photo added!`),1),t[6]||=v(`p`,{class:`upload-view__done-sub`},`It'll appear on your frame at the next update.`,-1),o(E,{variant:`primary`,class:`upload-view__done-btn`,onClick:V},{default:x(()=>[...t[4]||=[u(`Done`,-1)]]),_:1})])):l(``,!0),A.value?l(``,!0):(f(),s(k,{key:3,modelValue:b.value,"onUpdate:modelValue":t[0]||=e=>b.value=e,devices:n(c).devices,selected:n(a).selectedDeviceIds,uploading:y.value,"onUpdate:selected":t[1]||=e=>n(a).selectedDeviceIds=e,onConfirm:B},null,8,[`modelValue`,`devices`,`selected`,`uploading`]))]))}}),[[`__scopeId`,`data-v-fca2e263`]]);export{$ as default}; |