lecodes-cli 2.4.1 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +36134 -11203
- package/package.json +10 -9
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/ar-D6oDL4pw.js +1 -0
- package/runtime/web/assets/ar-mVXX3kRQ.js +2 -0
- package/runtime/web/assets/qrAnchorWorker-Cb5E8cF-.js +1 -0
- package/runtime/web/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web/index.html +1 -0
- package/runtime/web/player.js +12 -12
- package/runtime/web-lite/assets/{index-B2zEJlWS.js → index-oPsDD47E.js} +1 -1
- package/runtime/web-lite/assets/scene3d-BAl_QQyi.js +1105 -0
- package/runtime/web-lite/features.json +2 -2
- package/runtime/web-lite/index.html +2 -1
- package/runtime/web-lite/lite.js +9 -9
- package/runtime/web-lite/plugins/ar/_virtual_lecodes-half-resources-CNYmVR88.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qrAnchorWorker-DcxVKAqo.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web-lite/plugins/ar/index-C2uuuLhz.js +2 -0
- package/runtime/web-lite/plugins.json +1 -1
- package/src/commands/app/android.ts +68 -19
- package/src/commands/app/index.ts +84 -17
- package/src/commands/app/shared.ts +157 -24
- package/src/commands/app/splash.ts +251 -0
- package/src/commands/app/templates/android.ts +148 -25
- package/src/commands/app/templates/ios.ts +123 -12
- package/src/commands/init/index.ts +195 -195
- package/src/commands/plugin.ts +3 -3
- package/src/commands/push.ts +2 -2
- package/src/commands/shared.ts +79 -79
- package/src/commands/thumbs.ts +185 -185
- package/src/commands/web.ts +12 -5
- package/src/compile/collect.ts +2 -3
- package/src/compile/collectLocal.ts +13 -2
- package/src/compile/designCompile.ts +2 -3
- package/src/compile/projectCompile.ts +3 -0
- package/src/compile/screenEntry.ts +123 -123
- package/src/dev/devServer.ts +63 -7
- package/src/dev/https.ts +50 -0
- package/src/dev/serve.ts +15 -9
- package/src/dev/webRunner.ts +11 -3
- package/src/platform/serverDiff.ts +2 -3
- package/src/plugin/emitPackage.ts +18 -5
- package/src/plugin/emitWeb.ts +17 -8
- package/src/plugin/firstParty.ts +30 -0
- package/src/plugin/generate.ts +52 -24
- package/src/plugin/manifest.ts +66 -7
- package/src/project/ignore.ts +164 -164
- package/src/project/localFiles.ts +44 -7
- package/src/web/build.ts +33 -9
- package/src/web/halves.ts +76 -0
- package/runtime/web/assets/ar-CHvaraG4.js +0 -1
- package/runtime/web-lite/assets/scene3d-Bew8g7Ws.js +0 -1081
- package/runtime/web-lite/plugins/ar/index-CHvaraG4.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const r=""+new URL("assets/qrAnchorWorker-DcxVKAqo.js",import.meta.url).href,o={qrAnchorWorker:r};export{o as resources};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class y{__destroy_into_raw(){const t=this.__wbg_ptr;return this.__wbg_ptr=0,S.unregister(this),t}free(){const t=this.__destroy_into_raw();s.__wbg_qranchor_free(t,0)}clear_hints(){s.qranchor_clear_hints(this.__wbg_ptr)}frame_height(){return s.qranchor_frame_height(this.__wbg_ptr)>>>0}frame_ptr(t,r){return s.qranchor_frame_ptr(this.__wbg_ptr,t,r)>>>0}frame_width(){return s.qranchor_frame_width(this.__wbg_ptr)>>>0}gray_from_rgba(){s.qranchor_gray_from_rgba(this.__wbg_ptr)}constructor(t){const r=R(t,s.__wbindgen_malloc,s.__wbindgen_realloc),n=b,o=s.qranchor_new(r,n);if(o[2])throw k(o[1]);return this.__wbg_ptr=o[0],S.register(this,this.__wbg_ptr,this),this}output_len(){return s.qranchor_output_len(this.__wbg_ptr)>>>0}output_ptr(){return s.qranchor_output_ptr(this.__wbg_ptr)>>>0}process(t,r,n,o){return s.qranchor_process(this.__wbg_ptr,t,r,n,o)>>>0}process_gl(t,r,n,o){return s.qranchor_process_gl(this.__wbg_ptr,t,r,n,o)>>>0}reset(){s.qranchor_reset(this.__wbg_ptr)}rgba_ptr(t,r){return s.qranchor_rgba_ptr(this.__wbg_ptr,t,r)>>>0}set_hint(t,r,n,o,c){s.qranchor_set_hint(this.__wbg_ptr,t,r,n,o,c)}set_targets(t){const r=R(t,s.__wbindgen_malloc,s.__wbindgen_realloc),n=b,o=s.qranchor_set_targets(this.__wbg_ptr,r,n);if(o[2])throw k(o[1]);return o[0]>>>0}}Symbol.dispose&&(y.prototype[Symbol.dispose]=y.prototype.free);function x(){return s.output_layout_version()>>>0}function T(){return s.output_stride()>>>0}function C(){let e,t;try{const r=s.version();return e=r[0],t=r[1],g(r[0],r[1])}finally{s.__wbindgen_free(e,t,1)}}function $(){return{__proto__:null,"./qr_anchor_wasm_bg.js":{__proto__:null,__wbg_Error_30c8987f7c2ed4e2:function(t,r){return Error(g(t,r))},__wbg___wbindgen_throw_41e9ee4f547fc59a:function(t,r){throw new Error(g(t,r))},__wbg_error_757e9472f8410341:function(t,r){let n,o;try{n=t,o=r,console.error(g(t,r))}finally{s.__wbindgen_free(n,o,1)}},__wbg_new_227d7c05414eb861:function(){return new Error},__wbg_stack_3b0d974bbf31e44f:function(t,r){const n=r.stack,o=R(n,s.__wbindgen_malloc,s.__wbindgen_realloc),c=b;M().setInt32(t+4,c,!0),M().setInt32(t+0,o,!0)},__wbindgen_init_externref_table:function(){const t=s.__wbindgen_externrefs,r=t.grow(4);t.set(0,void 0),t.set(r+0,void 0),t.set(r+1,null),t.set(r+2,!0),t.set(r+3,!1)}}}}const S=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(e=>s.__wbg_qranchor_free(e,1));let f=null;function M(){return(f===null||f.buffer.detached===!0||f.buffer.detached===void 0&&f.buffer!==s.memory.buffer)&&(f=new DataView(s.memory.buffer)),f}function g(e,t){return z(e>>>0,t)}let u=null;function h(){return(u===null||u.byteLength===0)&&(u=new Uint8Array(s.memory.buffer)),u}function R(e,t,r){if(r===void 0){const _=l.encode(e),a=t(_.length,1)>>>0;return h().subarray(a,a+_.length).set(_),b=_.length,a}let n=e.length,o=t(n,1)>>>0;const c=h();let i=0;for(;i<n;i++){const _=e.charCodeAt(i);if(_>127)break;c[o+i]=_}if(i!==n){i!==0&&(e=e.slice(i)),o=r(o,n,n=i+e.length*3,1)>>>0;const _=h().subarray(o+i,o+n),a=l.encodeInto(e,_);i+=a.written,o=r(o,n,i,1)>>>0}return b=i,o}function k(e){const t=s.__wbindgen_externrefs.get(e);return s.__externref_table_dealloc(e),t}let d=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});d.decode();const j=2146435072;let E=0;function z(e,t){return E+=t,E>=j&&(d=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),d.decode(),E=t),d.decode(h().subarray(e,e+t))}const l=new TextEncoder;"encodeInto"in l||(l.encodeInto=function(e,t){const r=l.encode(e);return t.set(r),{read:e.length,written:r.length}});let b=0,s;function B(e,t){return s=e.exports,f=null,u=null,s.__wbindgen_start(),s}async function N(e,t){if(typeof Response=="function"&&e instanceof Response){if(!e.ok)throw new Error(`failed to fetch Wasm: ${e.status} ${e.statusText} fetching '${e.url}'`);if(typeof WebAssembly.instantiateStreaming=="function")try{return await WebAssembly.instantiateStreaming(e,t)}catch(o){if(r(e.type)&&e.headers.get("Content-Type")!=="application/wasm")console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",o);else throw o}const n=await e.arrayBuffer();return await WebAssembly.instantiate(n,t)}else{const n=await WebAssembly.instantiate(e,t);return n instanceof WebAssembly.Instance?{instance:n,module:e}:n}function r(n){switch(n){case"basic":case"cors":case"default":return!0}return!1}}async function P(e){if(s!==void 0)return s;e!==void 0&&(Object.getPrototypeOf(e)===Object.prototype?{module_or_path:e}=e:console.warn("using deprecated parameters for the initialization function; pass a single object instead")),e===void 0&&(e=new URL(""+new URL("qr_anchor_wasm_bg-D_ePkCCu.wasm",import.meta.url).href,import.meta.url));const t=$();(typeof e=="string"||typeof Request=="function"&&e instanceof Request||typeof URL=="function"&&e instanceof URL)&&(e=fetch(e));const{instance:r,module:n}=await N(await e,t);return B(r)}const v=1,D=28,m=self;let p=null;const w={};let O=!1;const U=e=>{var r,n;const t=JSON.stringify(e);(r=w.seeking)==null||r.set_targets(t),(n=w.holding)==null||n.set_targets(t)},V=async e=>{if(e.type==="init"){const q=performance.now(),A=await P({module_or_path:new URL(""+new URL("qr_anchor_wasm_bg-D_ePkCCu.wasm",import.meta.url).href,import.meta.url)});if(x()!==v||T()!==D)throw new Error(`its output is layout ${x()} × ${T()}, this worker reads ${v} × ${D}`);p=A.memory,w.seeking=new y(JSON.stringify(e.config.seeking)),w.holding=new y(JSON.stringify(e.config.holding)),U(e.targets),m.postMessage({type:"ready",version:C(),ms:performance.now()-q});return}if(e.type==="targets"){U(e.targets);return}const t=w[e.phase];if(!t||!p)throw new Error("a frame before the detector was made");const{seq:r,width:n,height:o}=e,c=performance.now(),i=t.frame_ptr(n,o);let _=0;if(i){new Uint8Array(p.buffer,i,n*o).set(new Uint8Array(e.gray,0,n*o));for(const[q,A,F,I,L]of e.hints)t.set_hint(q,A,F,I,L);_=t.process_gl(e.p0,e.p5,e.p8,e.p9)}const a=_>0?new Float64Array(p.buffer,t.output_ptr(),t.output_len()).slice():new Float64Array(0);m.postMessage({type:"result",seq:r,observations:a,processMs:performance.now()-c},[a.buffer])};let W=Promise.resolve();m.onmessage=e=>{const t=e.data;W=W.then(()=>{if(!O)return V(t).catch(r=>{O=!0,m.postMessage({type:"failed",message:r instanceof Error?r.message:String(r)})})})};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var tt=Object.defineProperty;var nt=(e,t,n)=>t in e?tt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var ue=(e,t,n)=>nt(e,typeof t!="symbol"?t+"":t,n);const rt="modulepreload",ot=function(e,t){return new URL(e,t).href},Ie={},st=function(t,n,r){let s=Promise.resolve();if(n&&n.length>0){let f=function(d){return Promise.all(d.map(p=>Promise.resolve(p).then(v=>({status:"fulfilled",value:v}),v=>({status:"rejected",reason:v}))))};const h=document.getElementsByTagName("link"),c=document.querySelector("meta[property=csp-nonce]"),m=(c==null?void 0:c.nonce)||(c==null?void 0:c.getAttribute("nonce"));s=f(n.map(d=>{if(d=ot(d,r),d in Ie)return;Ie[d]=!0;const p=d.endsWith(".css"),v=p?'[rel="stylesheet"]':"";if(r)for(let b=h.length-1;b>=0;b--){const x=h[b];if(x.href===d&&(!p||x.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${d}"]${v}`))return;const g=document.createElement("link");if(g.rel=p?"stylesheet":rt,p||(g.as="script"),g.crossOrigin="",g.href=d,m&&g.setAttribute("nonce",m),document.head.appendChild(g),p)return new Promise((b,x)=>{g.addEventListener("load",b),g.addEventListener("error",()=>x(Error(`Unable to preload CSS for ${d}`)))})}))}function i(f){const h=new Event("vite:preloadError",{cancelable:!0});if(h.payload=f,window.dispatchEvent(h),!h.defaultPrevented)throw f}return s.then(f=>{for(const h of f||[])h.status==="rejected"&&i(h.reason);return t().catch(i)})},it=(e,t)=>{const n=t.data instanceof Uint8Array?t.data.slice().buffer:t.data;return{systemId:e.putBuffer(n),name:t.name,size:n.byteLength}},S={present:e=>e!=null,f64:e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,i32:e=>typeof e=="number"&&Number.isInteger(e)&&e>=-2147483648&&e<=2147483647?e:void 0,bool:e=>typeof e=="boolean"?e:void 0,string:e=>typeof e=="string"?e:void 0,json:e=>e,bytes:e=>Object.prototype.toString.call(e)==="[object Uint8Array]"?e.slice():void 0,object:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0,array:(e,t)=>Array.isArray(e)&&(t===void 0||e.length===t)?e:void 0,list:(e,t)=>{if(!Array.isArray(e))return;const n=[];for(const r of e){const s=t(r);if(s===void 0)return;n.push(s)}return n},map:(e,t)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;const n={};for(const[r,s]of Object.entries(e)){const i=t(s);if(i===void 0)return;n[r]=i}return n}};let xe;const at=e=>{const t=S.object(e),n=t?S.i32(t.systemId):void 0,r=n===void 0||xe==null?void 0:xe(n);return t&&r?{data:r,name:S.string(t.name)??""}:void 0},he=e=>typeof e=="number"&&Number.isInteger(e)&&e>=0&&e<=4294967295?e:void 0,ct=e=>{const t=S.object(e);if(!t)return;let n;if(S.present(t.scene)){const i=he(t.scene);if(i===void 0)return;n=i}let r;if(S.present(t.camera)){const i=he(t.camera);if(i===void 0)return;r=i}const s={};return n!==void 0&&(s.scene=n),r!==void 0&&(s.camera=r),s},lt=e=>e==="world"||e==="images"?e:void 0,dt=e=>e==="horizontal"||e==="vertical"||e==="both"||e==="none"?e:void 0,Qe=e=>{const t=S.object(e);if(!t)return;const n=at(t.image);if(n===void 0)return;const r=S.f64(t.physicalWidth);if(r===void 0)return;const s=he(t.entity);return s===void 0?void 0:{image:n,physicalWidth:r,entity:s}},Ye=e=>{const t=S.object(e);if(!t)return;const n=S.string(t.text);if(n===void 0)return;const r=S.f64(t.size);if(r===void 0)return;const s=he(t.entity);return s===void 0?void 0:{text:n,size:r,entity:s}},ut=e=>e==="gravity"||e==="gravityAndHeading"?e:void 0,ft=e=>{const t=S.object(e);if(!t)return;let n;if(S.present(t.mode)){const d=lt(t.mode);if(d===void 0)return;n=d}let r;if(S.present(t.planes)){const d=dt(t.planes);if(d===void 0)return;r=d}let s;if(S.present(t.images)){const d=S.list(t.images,p=>Qe(p));if(d===void 0)return;s=d}let i;if(S.present(t.codes)){const d=S.list(t.codes,p=>Ye(p));if(d===void 0)return;i=d}let f;if(S.present(t.worldAlignment)){const d=ut(t.worldAlignment);if(d===void 0)return;f=d}let h;if(S.present(t.lightEstimation)){const d=S.bool(t.lightEstimation);if(d===void 0)return;h=d}let c;if(S.present(t.autoFocus)){const d=S.bool(t.autoFocus);if(d===void 0)return;c=d}const m={};return n!==void 0&&(m.mode=n),r!==void 0&&(m.planes=r),s!==void 0&&(m.images=s),i!==void 0&&(m.codes=i),f!==void 0&&(m.worldAlignment=f),h!==void 0&&(m.lightEstimation=h),c!==void 0&&(m.autoFocus=c),m},ht=e=>e==="planes"||e==="estimated"||e==="any"?e:void 0,mt=e=>{const t=S.object(e);if(!t)return;const n=S.f64(t.x);if(n===void 0)return;const r=S.f64(t.y);if(r===void 0)return;const s=S.f64(t.z);return s===void 0?void 0:{x:n,y:r,z:s}},wt=e=>{const t=S.object(e);if(!t)return;const n=S.f64(t.x);if(n===void 0)return;const r=S.f64(t.y);if(r===void 0)return;const s=S.f64(t.z);if(s===void 0)return;const i=S.f64(t.w);return i===void 0?void 0:{x:n,y:r,z:s,w:i}},gt=e=>{const t=S.object(e);if(!t)return;const n=mt(t.position);if(n===void 0)return;const r=wt(t.rotation);return r===void 0?void 0:{position:n,rotation:r}};class z extends Error{constructor(t){super(t),this.code=t}}const Le={name:"ar",version:2},yt=(e,t)=>{xe=n=>e.getBuffer(n),e.registerService(Le.name,(n,r)=>{const s={tracking:h=>r.emit("tracking",h),plane:h=>r.emit("plane",h),image:h=>r.emit("image",h),anchor:h=>r.emit("anchor",h),light:h=>r.emit("light",h),error:h=>r.emit("error",h),ended:h=>r.emit("ended",h)},i=t(ct(n)??{},s);return{call:async(h,c)=>{switch(h){case"start":{let m;if(S.present(c[0])){const d=ft(c[0]);if(d===void 0)throw new Error("bad arguments — AR.start: options");m=d}await i.start(m);return}case"stop":{await i.stop();return}case"setImages":{const m=S.list(c[0],d=>Qe(d));if(m===void 0)throw new Error("bad arguments — AR.setImages: images");await i.setImages(m);return}case"setCodes":{const m=S.list(c[0],d=>Ye(d));if(m===void 0)throw new Error("bad arguments — AR.setCodes: codes");await i.setCodes(m);return}case"hitTest":{const m=S.f64(c[0]);if(m===void 0)throw new Error("bad arguments — AR.hitTest: x");const d=S.f64(c[1]);if(d===void 0)throw new Error("bad arguments — AR.hitTest: y");let p;if(S.present(c[2])){const v=ht(c[2]);if(v===void 0)throw new Error("bad arguments — AR.hitTest: target");p=v}return await i.hitTest(m,d,p)}case"anchor":{const m=he(c[0]);if(m===void 0)throw new Error("bad arguments — AR.anchor: entity");const d=gt(c[1]);if(d===void 0)throw new Error("bad arguments — AR.anchor: pose");await i.anchor(m,d);return}case"release":{const m=he(c[0]);if(m===void 0)throw new Error("bad arguments — AR.release: entity");await i.release(m);return}case"captureFrame":{const m=await i.captureFrame();return it(e,m)}default:throw new Error(`Unknown method: ${h}`)}},close:()=>{var h;return(h=i.close)==null?void 0:h.call(i)}}},Le.version)},ce=Math.PI/180,ge=(e,t)=>[e[3]*t[0]+e[0]*t[3]+e[1]*t[2]-e[2]*t[1],e[3]*t[1]-e[0]*t[2]+e[1]*t[3]+e[2]*t[0],e[3]*t[2]+e[0]*t[1]-e[1]*t[0]+e[2]*t[3],e[3]*t[3]-e[0]*t[0]-e[1]*t[1]-e[2]*t[2]],Re=e=>{const t=Math.hypot(e[0],e[1],e[2],e[3])||1;return[e[0]/t,e[1]/t,e[2]/t,e[3]/t]},fe=(e,t)=>{const n=Math.sin(t/2);return[e[0]*n,e[1]*n,e[2]*n,Math.cos(t/2)]},Pe=(e,t)=>{const[n,r,s,i]=e,f=2*(r*t[2]-s*t[1]),h=2*(s*t[0]-n*t[2]),c=2*(n*t[1]-r*t[0]);return[t[0]+i*f+(r*c-s*h),t[1]+i*h+(s*f-n*c),t[2]+i*c+(n*h-r*f)]},_e=(e,t,n)=>ge(ge(fe([0,0,1],e*ce),fe([1,0,0],t*ce)),fe([0,1,0],n*ce)),Ze=e=>{const[t,n,r,s]=e;return[2*(t*r-s*n),2*(s*t+n*r),s*s-t*t-n*n+r*r]},pt=(e,t)=>{const n=e[0]*t[0]+e[1]*t[1]+e[2]*t[2];if(n<-.999999)return[1,0,0,0];const r=[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]];return Re([r[0],r[1],r[2],1+n])};class Oe{constructor(t=1,n=.05){ue(this,"q",[0,0,0,1]);ue(this,"seeded",!1);ue(this,"ix",0);ue(this,"iy",0);ue(this,"iz",0);this.kp=t,this.ki=n}get ready(){return this.seeded}seed(t){this.q=Re(t),this.seeded=!0}update(t,n,r){const s=Math.hypot(n[0],n[1],n[2]);if(!this.seeded){if(s===0)return;this.seed(pt([n[0]/s,n[1]/s,n[2]/s],[0,0,1]));return}let i=t[0],f=t[1],h=t[2];if(s>0){const x=n[0]/s,R=n[1]/s,$=n[2]/s,M=Ze(this.q),_=R*M[2]-$*M[1],j=$*M[0]-x*M[2],N=x*M[1]-R*M[0];this.ki>0&&(this.ix+=this.ki*_*r,this.iy+=this.ki*j*r,this.iz+=this.ki*N*r,i+=this.ix,f+=this.iy,h+=this.iz),i+=this.kp*_,f+=this.kp*j,h+=this.kp*N}const c=.5*r*i,m=.5*r*f,d=.5*r*h,[p,v,g,b]=this.q;this.q=Re([p+b*c+v*d-g*m,v+b*m-p*d+g*c,g+b*d+p*m-v*c,b-p*c-v*m-g*d])}}const xt=fe([1,0,0],-Math.PI/2),vt=(e,t)=>ge(ge(xt,e),fe([0,0,1],-t*ce)),Et=e=>{let[t,,n]=Pe(e,[0,0,-1]);return Math.hypot(t,n)<.3&&([t,,n]=Pe(e,[0,1,0])),fe([0,1,0],-Math.atan2(-t,-n))},kt=e=>{const[t,n,r,s]=e;return[1-2*(n*n+r*r),2*(t*n+s*r),2*(t*r-s*n),0,2*(t*n-s*r),1-2*(t*t+r*r),2*(n*r+s*t),0,2*(t*r+s*n),2*(n*r-s*t),1-2*(t*t+n*n),0,0,0,0,1]},bt=(e,t,n,r,s)=>{const i=Math.max(e,t)/2/Math.tan(s*ce/2),f=Math.max(n/e,r/t);return 2*Math.atan(r/f/2/i)/ce},At=(e,t,n,r)=>{const s=1/Math.tan(e*ce/2);return[s/t,0,0,0,0,s,0,0,0,0,(r+n)/(n-r),-1,0,0,2*r*n/(n-r),0]},Rt=(e,t,n,r)=>{const s=Math.max(n/e,r/t),i=n/(e*s),f=r/(t*s);return[i,0,(1-i)/2,0,f,(1-f)/2]},Mt=68,Te=.01,je=1e3,Ee=Math.PI/180,qe=30,zt=(e,t,n)=>{const r=t.scene;let s=0,i=!1,f=!1,h=null,c=null,m=null,d="",p=new Oe,v=!1,g=null,b=null,x=0,R=0,$=0,M=null,_=null;const j=k=>{k.alpha===null||k.beta===null||k.gamma===null||k.alpha===0&&k.beta===0&&k.gamma===0||(b=[k.beta,k.gamma],g=_e(k.alpha,k.beta,k.gamma))},N=k=>{const I=k.rotationRate,F=k.accelerationIncludingGravity;if(!I||!F||I.alpha===null||I.beta===null||I.gamma===null||F.x===null||F.y===null||F.z===null)return;const L=k.timeStamp;if(R||(R=L),$===0)if(b){const K=Ze(_e(0,b[0],b[1])),te=Math.hypot(F.x,F.y,F.z)||1;$=(K[0]*F.x+K[1]*F.y+K[2]*F.z)/te<0?-1:1}else if(L-R>500)$=1;else return;const G=x?Math.min(.1,Math.max(0,(L-x)/1e3)):0;x=L,v||(v=!0,g&&p.seed(g)),p.update([I.beta*Ee,I.gamma*Ee,I.alpha*Ee],[F.x*$,F.y*$,F.z*$],G)},V=()=>{if(!c||r===void 0)return;const k=e.viewport(r),{width:I,height:F}=k,L=`${c.videoWidth}x${c.videoHeight}:${I}x${F}`;if(L!==d&&c.videoWidth>0&&I>0&&F>0){d=L;const te=bt(c.videoWidth,c.videoHeight,I,F,Mt);e.setProjection(r,At(te,I/F,Te,je),Te,je),e.setBackground(r,c,Rt(c.videoWidth,c.videoHeight,I,F))}const G=v?p.ready?p.q:null:g;if(!G)return;const K=vt(G,k.rotation);M??(M=Et(K)),e.setCameraMatrix(r,kt(ge(M,K)))},H=async()=>{const k=[window.DeviceMotionEvent,window.DeviceOrientationEvent];if(!k[1])return!1;let I=!0;for(const F of k)if(typeof(F==null?void 0:F.requestPermission)=="function")try{I=await F.requestPermission()==="granted"&&I}catch{I=!1}return I},Y=()=>{_&&window.removeEventListener("pointerup",_,!0),_=null},X=()=>{_||(_=()=>{Y(),H().then(k=>{k||console.warn("[ar] the page was not given the device's sensors: the camera of the scene stays as the app turned it")})},window.addEventListener("pointerup",_,!0))},J=()=>{s++,i=!1,f=!1,m==null||m(),m=null;for(const k of(h==null?void 0:h.getTracks())??[])k.stop();h=null,c&&(c.srcObject=null,r!==void 0&&(e.setBackground(r,null),e.clearProjection(r)),c.remove()),c=null,d="",Y(),window.removeEventListener("deviceorientation",j),window.removeEventListener("devicemotion",N),p=new Oe,v=!1,g=null,b=null,x=0,R=0,$=0,M=null};return{async start(k){var K,te;if(i||f)throw new z("busy");if(r===void 0||t.camera===void 0)throw console.warn("[ar] start: the session has no scene or camera to drive (ARScene opens it with both)"),new z("failed");((k==null?void 0:k.mode)==="images"||(K=k==null?void 0:k.images)!=null&&K.length)&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked"),(te=k==null?void 0:k.codes)!=null&&te.length&&console.warn("[ar] the web finds no codes in the camera's picture yet: their anchors are never tracked"),f=!0;const I=++s,F=H();let L;try{L=await navigator.mediaDevices.getUserMedia({audio:!1,video:{facingMode:{ideal:"environment"},width:{ideal:1280},height:{ideal:720}}})}catch(Z){I===s&&(f=!1);const w=Z instanceof DOMException?Z.name:"";throw console.warn(`[ar] no camera: ${Z instanceof Error?Z.message:Z}`),new z(w==="NotAllowedError"||w==="SecurityError"?"denied":w==="NotFoundError"||w==="OverconstrainedError"?"unavailable":"failed")}if(I!==s){for(const Z of L.getTracks())Z.stop();throw new z("failed")}h=L;const G=document.createElement("video");if(G.muted=!0,G.playsInline=!0,G.autoplay=!0,G.srcObject=L,c=G,await new Promise(Z=>{if(G.readyState>=2)return Z();G.addEventListener("loadeddata",()=>Z(),{once:!0})}),await G.play().catch(()=>{}),I!==s)throw new z("failed");window.addEventListener("deviceorientation",j),window.addEventListener("devicemotion",N),m=e.onFrame(V),V(),f=!1,i=!0,n.tracking({state:"normal"}),F.then(Z=>{!Z&&I===s&&X()})},stop(){J()},setImages(k){if(!i)throw new z("notRunning");k.length&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked")},setCodes(k){if(!i)throw new z("notRunning");k.length&&console.warn("[ar] the web finds no codes in the camera's picture yet: their anchors are never tracked")},hitTest(){if(!i)throw new z("notRunning");return[]},anchor(){if(!i)throw new z("notRunning")},release(){},async captureFrame(){const k=c;if(!i||!k)throw new z("notRunning");const I=s;for(let K=0;k.readyState<2||k.videoWidth===0;K++){if(K>=qe)throw console.warn(`[ar] captureFrame: the camera gave no picture in ${qe} frames`),new z("failed");if(await new Promise(te=>requestAnimationFrame(te)),I!==s)throw new z("notRunning")}const F=document.createElement("canvas");F.width=k.videoWidth,F.height=k.videoHeight;const L=F.getContext("2d");if(!L)throw new z("failed");L.drawImage(k,0,0);const G=await new Promise(K=>F.toBlob(K,"image/jpeg",.9));if(!G)throw new z("failed");return{data:await G.arrayBuffer(),name:"frame.jpg"}},close(){J()}}},$t=180/Math.PI,Me=({position:e,rotation:t})=>{const{x:n,y:r,z:s,w:i}=t;return[1-2*(r*r+s*s),2*(n*r+i*s),2*(n*s-i*r),0,2*(n*r-i*s),1-2*(n*n+s*s),2*(r*s+i*n),0,2*(n*s+i*r),2*(r*s-i*n),1-2*(n*n+r*r),0,e.x,e.y,e.z,1]},ze=e=>{const[t,n,r,s,i,f,h,c,m,d,p,v,g,b,x,R]=Array.from(e),$=t*f-n*i,M=t*h-r*i,_=t*c-s*i,j=n*h-r*f,N=n*c-s*f,V=r*c-s*h,H=m*b-d*g,Y=m*x-p*g,X=m*R-v*g,J=d*x-p*b,k=d*R-v*b,I=p*R-v*x,F=$*I-M*k+_*J+j*X-N*Y+V*H;if(!F)return null;const L=1/F;return[(f*I-h*k+c*J)*L,(r*k-n*I-s*J)*L,(b*V-x*N+R*j)*L,(p*N-d*V-v*j)*L,(h*X-i*I-c*Y)*L,(t*I-r*X+s*Y)*L,(x*_-g*V-R*M)*L,(m*V-p*_+v*M)*L,(i*k-f*X+c*H)*L,(n*X-t*k-s*H)*L,(g*N-b*_+R*$)*L,(d*_-m*N-v*$)*L,(f*Y-i*J-h*H)*L,(t*J-n*Y+r*H)*L,(b*M-g*j-x*$)*L,(m*j-d*M+p*$)*L]},Ft=(e,t)=>{const n=new Array(16);for(let r=0;r<4;r++)for(let s=0;s<4;s++)n[r*4+s]=e[s]*t[r*4]+e[4+s]*t[r*4+1]+e[8+s]*t[r*4+2]+e[12+s]*t[r*4+3];return n},St=e=>{const t=e[0],n=e[1],r=e[2],s=e[4],i=e[5],f=e[6],h=e[8],c=e[9],m=e[10],d=t+i+m;let p,v,g,b;if(d>0){const R=Math.sqrt(d+1)*2;b=R/4,p=(f-c)/R,v=(h-r)/R,g=(n-s)/R}else if(t>i&&t>m){const R=Math.sqrt(1+t-i-m)*2;b=(f-c)/R,p=R/4,v=(s+n)/R,g=(h+r)/R}else if(i>m){const R=Math.sqrt(1+i-t-m)*2;b=(h-r)/R,p=(s+n)/R,v=R/4,g=(c+f)/R}else{const R=Math.sqrt(1+m-t-i)*2;b=(n-s)/R,p=(h+r)/R,v=(c+f)/R,g=R/4}const x=Math.hypot(p,v,g,b)||1;return{x:p/x,y:v/x,z:g/x,w:b/x}},It=e=>({position:{x:e[12],y:e[13],z:e[14]},rotation:St(e)}),Ce=(e,t)=>Math.hypot(e.x-t.x,e.y-t.y,e.z-t.z),De=(e,t)=>{const n=Math.min(1,Math.abs(e.x*t.x+e.y*t.y+e.z*t.z+e.w*t.w));return 2*Math.acos(n)*$t},Lt=(e,t,n)=>{let r=e.x*t.x+e.y*t.y+e.z*t.z+e.w*t.w;const s=r<0?-1:1;r*=s;let i,f;if(r>.9995)i=1-n,f=n*s;else{const v=Math.acos(r),g=Math.sin(v);i=Math.sin((1-n)*v)/g,f=Math.sin(n*v)/g*s}const h=i*e.x+f*t.x,c=i*e.y+f*t.y,m=i*e.z+f*t.z,d=i*e.w+f*t.w,p=Math.hypot(h,c,m,d)||1;return{x:h/p,y:c/p,z:m/p,w:d/p}},Pt=(e,t,n)=>({position:{x:e.position.x+(t.position.x-e.position.x)*n,y:e.position.y+(t.position.y-e.position.y)*n,z:e.position.z+(t.position.z-e.position.z)*n},rotation:Lt(e.rotation,t.rotation,n)}),_t=(e,t,n,r,s)=>{const i=n,f=t,h=i[0]*e.x+i[4]*e.y+i[8]*e.z+i[12],c=i[1]*e.x+i[5]*e.y+i[9]*e.z+i[13],m=i[2]*e.x+i[6]*e.y+i[10]*e.z+i[14],d=i[3]*e.x+i[7]*e.y+i[11]*e.z+i[15],p=f[0]*h+f[4]*c+f[8]*m+f[12]*d,v=f[1]*h+f[5]*c+f[9]*m+f[13]*d,g=f[3]*h+f[7]*c+f[11]*m+f[15]*d;return g>0?[(p/g+1)*r/2,(1-v/g)*s/2]:null},Ot=(e,t,n)=>{const r=n,s=t,i=r[0]*e.x+r[4]*e.y+r[8]*e.z+r[12],f=r[1]*e.x+r[5]*e.y+r[9]*e.z+r[13],h=r[2]*e.x+r[6]*e.y+r[10]*e.z+r[14],c=r[3]*e.x+r[7]*e.y+r[11]*e.z+r[15],m=s[0]*i+s[4]*f+s[8]*h+s[12]*c,d=s[1]*i+s[5]*f+s[9]*h+s[13]*c,p=s[3]*i+s[7]*f+s[11]*h+s[15]*c;return p>0&&Math.abs(m)<=p&&Math.abs(d)<=p},Se={window:24,minObservations:6,minBaseline:.04,minSpread:.01,min:.8,max:1.25,maxResidual:.008,step:.25},Tt=(e,t)=>({p:{x:e[12],y:e[13],z:e[14]},u:{x:e[0]*t.x+e[4]*t.y+e[8]*t.z,y:e[1]*t.x+e[5]*t.y+e[9]*t.z,z:e[2]*t.x+e[6]*t.y+e[10]*t.z}}),jt=(e,t=Se)=>{const n=e.length;if(n<Math.max(2,t.minObservations))return null;let r=0;for(let $=0;$<n;$++)for(let M=$+1;M<n;M++){const _=e[$].p,j=e[M].p;r=Math.max(r,Math.hypot(_.x-j.x,_.y-j.y,_.z-j.z))}if(!(r>=t.minBaseline))return null;let s=0,i=0,f=0,h=0,c=0,m=0;for(const{p:$,u:M}of e)s+=$.x,i+=$.y,f+=$.z,h+=M.x,c+=M.y,m+=M.z;s/=n,i/=n,f/=n,h/=n,c/=n,m/=n;let d=0,p=0;for(const{p:$,u:M}of e){const _=M.x-h,j=M.y-c,N=M.z-m;d+=_*($.x-s)+j*($.y-i)+N*($.z-f),p+=_*_+j*j+N*N}const v=Math.sqrt(p/n);if(!(v>=t.minSpread))return null;const g=-d/p,b=Math.min(t.max,Math.max(t.min,g)),x={x:s+b*h,y:i+b*c,z:f+b*m};let R=0;for(const{p:$,u:M}of e){const _=b*M.x+$.x-x.x,j=b*M.y+$.y-x.y,N=b*M.z+$.z-x.z;R+=_*_+j*j+N*N}return{scale:b,unclamped:g,point:x,residual:Math.sqrt(R/n),baseline:r,spread:v,n}},qt=(e=Se)=>{const t=[];let n=1,r=null;return{get scale(){return n},get estimate(){return r},add(s){t.push(s),t.length>e.window&&t.splice(0,t.length-e.window);const i=jt(t,e);return!i||!(i.residual<=e.maxResidual)?{fit:i,taken:!1}:(n+=(r===null?1:e.step)*(i.scale-n),r=i.scale,{fit:i,taken:!0})},reset(){t.length=0,n=1,r=null}}},ve={min_init_side_px:40,min_reacquire_side_px:20,max_rms_px:.6},Ct={seeking:{...ve,search_first:!1},holding:{...ve,search_first:!0,full_detect_interval:1e6}},We=480,ke=960,be=6,Ne=16,Dt=24,Wt=2,Nt=15,Ht=5,Ut=.02,Gt=2,Bt=.01,Vt=1,Xt=.03,Kt=5,$e=.8,He=10,Ue=30,Qt=.5,Yt=500,Ae=2e3,Zt=5e3,Jt=.005,pe=28,en=(e,t,n=ve.max_rms_px)=>e.rms<=n?e.metres>Xt||e.degrees>Kt?e.inliers!==void 0&&(e.inliers<Dt||e.side<$e*t)?"kept":"move":e.metres<=Bt&&e.degrees<=Vt?"near":e.side>=$e*t?"replace":"kept":"bad",Ge=e=>({position:{x:e.position.x,y:e.position.y,z:e.position.z},rotation:{x:e.orientation.x,y:e.orientation.y,z:e.orientation.z,w:e.orientation.w}});let Fe=!1;const ae=e=>{if(!Fe)try{e==null||e.delete()}catch{}},Be=e=>e instanceof Error?e.message:String(e),tn=e=>{const t=new URL(e,location.href);if(t.origin===location.origin)return{worker:new Worker(t,{type:"module"}),blob:null};const n=URL.createObjectURL(new Blob([`import ${JSON.stringify(t.href)}
|
|
2
|
+
`],{type:"text/javascript"}));try{return{worker:new Worker(n,{type:"module"}),blob:n}}catch(r){throw URL.revokeObjectURL(n),r}},nn=e=>{let t=[],n=0,r=null,s=null,i=!1,f=!1,h=!1,c=null,m=0,d=!1,p=0,v=0,g=null;const b=new Set,x={phase:"seeking",since:0,captures:0,found:0,workerMs:0,captureFrames:0,resultFrames:0},R=o=>{x.since=o,x.captures=x.found=x.workerMs=x.captureFrames=x.resultFrames=0},$=o=>[o.target.size,o.target.size],M=(o,l,a)=>{o.tracked!==l&&(o.tracked=l,console.log(`[ar] qr ${o.target.text}: ${a}`),e.image({entity:o.target.entity,tracked:l,extent:$(o)}))},_=()=>t.map(o=>({text:o.target.text,size:o.target.size})),j=(o,l=[])=>r==null?void 0:r.postMessage(o,l),N=()=>{s&&URL.revokeObjectURL(s),s=null},V=()=>{r==null||r.terminate(),r=null,N(),i=f=d=!1,c=null},H=o=>{h||(h=!0,V(),console.warn(`[ar] the QR detector failed (${o}): QR codes are not tracked`))},Y=()=>{if(r||f||h||t.length===0)return;if(typeof Worker>"u"){H("this browser has no workers");return}f=!0;const o=n;e.workerUrl().then(l=>{if(o!==n)return;if(f=!1,!l)throw new Error("the half was built without its worker (the manifest's qrAnchorWorker)");const a=tn(l),u=a.worker;r=u,s=a.blob,u.onmessage=y=>{r===u&&te(y.data)},u.onerror=y=>{y.preventDefault(),r===u&&H(y.message||"its module did not load")},j({type:"init",config:Ct,targets:_()})}).catch(l=>{o===n&&H(Be(l))})},X=o=>({target:o,anchor:null,anchorPose:null,anchorTracked:!1,asked:null,next:null,nextFrames:0,shown:null,tracked:!1,slew:null,corners:null,cornersIn:[0,0],side:0,misses:0,bestSide:0,loggedAt:-1/0,keptLoggedAt:-1/0,depth:qt(),depthLogged:null,depthRefusedAt:-1/0}),J=(o,l)=>{ae(o.anchor),ae(o.next),o.anchor=o.next=null,o.asked=null,o.depth.reset(),o.depthLogged=null,l&&M(o,!1,"no longer a target")},k=(o,l,a,u)=>{const{fit:y,taken:E}=o.depth.add(Tt(l,a));if(!y)return;const P=y.scale!==y.unclamped?`, clamped from ${y.unclamped.toFixed(3)}`:"",q=`baseline ${(y.baseline*100).toFixed(1)} cm, residual ${(y.residual*1e3).toFixed(1)} mm, ${y.n} obs${P}`;if(E){if(o.depthLogged!==null&&Math.abs(y.scale-o.depthLogged)<=Jt*o.depthLogged)return;o.depthLogged=y.scale,console.log(`[ar] qr ${o.target.text}: depth scale ${o.depth.scale.toFixed(3)} (estimate ${y.scale.toFixed(3)}, ${q})`)}else u-o.depthRefusedAt>=Ae&&(o.depthRefusedAt=u,console.log(`[ar] qr ${o.target.text}: depth scale estimate ${y.scale.toFixed(3)} refused (${q}; the residual over ${(Se.maxResidual*1e3).toFixed(0)} mm), the scale stays ${o.depth.scale.toFixed(3)}`))},I=o=>o.asked||o.next?"waiting":o.anchor&&o.anchorTracked?"holding":"seeking",F=(o,l)=>{const a=n,u={pose:l,wanted:()=>a===n&&o.asked===u&&t.includes(o),settle:y=>{if(o.asked!==u){ae(y);return}o.asked=null,y&&(ae(o.next),o.next=y,o.nextFrames=0)}};o.asked=u,e.anchor(u)},L=(o,l,a)=>{try{if(o.trackedAnchors&&!o.trackedAnchors.has(a))return null;const u=o.getPose(a.anchorSpace,l);return u?Ge(u.transform):null}catch{return null}},G=(o,l)=>{for(const a of t){if(a.next)if(L(o,l,a.next)){const E=a.anchor;a.anchor=a.next,a.next=null,a.anchorTracked=!1,ae(E),a.slew=a.shown?{from:a.shown,frame:0}:null}else++a.nextFrames>Ue&&(console.warn(`[ar] qr ${a.target.text}: the new anchor was not tracked in ${Ue} frames: given up`),ae(a.next),a.next=null);if(!a.anchor)continue;const u=L(o,l,a.anchor);if(!u){a.anchorTracked&&(a.anchorTracked=!1,M(a,!1,"lost (its anchor is not tracked)"));continue}a.anchorTracked=!0,a.anchorPose=u;let y=u;a.slew&&(a.slew.frame++,y=Pt(a.slew.from,u,Math.min(1,a.slew.frame/He)),a.slew.frame>=He&&(a.slew=null)),a.shown=y,e.setMatrix(a.target.entity,Me(y)),M(a,!0,"regained (its anchor is tracked again)")}},K=(o,l,a,u,y)=>{const{picture:E}=a,P=l[1],q=l[2],U=l[3],B=l.subarray(4,20),ee=performance.now();if(o.misses=0,U<Ne){ee-o.keptLoggedAt>=Ae&&(o.keptLoggedAt=ee,console.log(`[ar] qr ${o.target.text}: ignored (${U} points < ${Ne}; side ${P.toFixed(0)} px of ${E.width}, rms ${q.toFixed(2)} px)`));return}q<=ve.max_rms_px&&k(o,E.viewMatrix,{x:B[12],y:B[13],z:B[14]},ee);const O=o.depth.scale,D=Array.from(B);D[12]=B[12]*O,D[13]=B[13]*O,D[14]=B[14]*O;const C=Ft(E.viewMatrix,D),T=It(C);o.corners=Array.from(l.subarray(20,28)),o.cornersIn=[E.width,E.height],o.side=P;const W=P*ke/E.width;ee-o.loggedAt>=Yt&&(o.loggedAt=ee,console.log(`[ar] qr ${o.target.text}: side ${P.toFixed(0)} px of ${E.width}, rms ${q.toFixed(2)} px, inliers ${U}, t=(${B[12].toFixed(3)} ${B[13].toFixed(3)} ${B[14].toFixed(3)}) m, the picture after ${a.capturedAt-a.askedAt} frames, the result after ${y}, worker ${u.toFixed(1)} ms`));const re=I(o);if(re==="waiting")return;if(re==="seeking"){o.anchor&&(ae(o.anchor),o.anchor=null,o.anchorPose=null),o.slew=null,o.shown=T,o.bestSide=W,e.setMatrix(o.target.entity,C),M(o,!0,`found at (${T.position.x.toFixed(3)} ${T.position.y.toFixed(3)} ${T.position.z.toFixed(3)}) m`),e.anchors()&&F(o,T);return}const le=o.anchorPose,me=Ce(T.position,le.position),ie=De(T.rotation,le.rotation),Q=en({metres:me,degrees:ie,side:W,rms:q,inliers:U},o.bestSide),ne=`Δ ${(me*1e3).toFixed(1)} mm, ${ie.toFixed(2)}°`;Q==="near"?o.bestSide=Math.max(o.bestSide,W):Q==="kept"?ee-o.keptLoggedAt>=Ae&&(o.keptLoggedAt=ee,console.log(`[ar] qr ${o.target.text}: kept (side ${W.toFixed(0)} < ${$e} × ${o.bestSide.toFixed(0)} px), ${ne} (rms ${q.toFixed(2)} px)`)):(Q==="replace"||Q==="move")&&(console.log(`[ar] qr ${o.target.text}: anchor replaced${Q==="move"?" (a large Δ)":""}, ${ne} (side ${W.toFixed(0)} px, best ${o.bestSide.toFixed(0)}, rms ${q.toFixed(2)} px)`),o.bestSide=W,F(o,T))},te=o=>{if(N(),o.type==="ready"){i=!0,console.log(`[ar] qr: qr-anchor ${o.version} loaded in its worker in ${o.ms.toFixed(0)} ms`);return}if(o.type==="failed"){H(o.message);return}const l=c;if(!l||o.seq!==l.seq)return;c=null,d=!1;const a=o.observations,u=Math.floor(a.length/pe),y=p-l.askedAt;if(x.since&&l.phase===x.phase&&(x.captures++,x.workerMs+=o.processMs,x.captureFrames+=l.capturedAt-l.askedAt,x.resultFrames+=y,u>0&&x.found++),!l.stale){if(l.phase==="holding"){const E=new Set;for(let P=0;P<u;P++)E.add(a[P*pe]);t.forEach((P,q)=>{!E.has(q)&&I(P)==="holding"&&P.misses++})}for(let E=0;E<u;E++){const P=a.subarray(E*pe,(E+1)*pe),q=t[P[0]];q&&K(q,P,l,o.processMs,y)}}},Z=(o,l,a)=>{if(!r||!i){d=!1;return}const{width:u,height:y}=o,E=o.projection;if(!b.has(u)){b.add(u);const O=E[0]*u/2,D=E[5]*y/2,C=(1-E[8])*u/2-.5,T=(1+E[9])*y/2-.5;console.log(`[ar] qr: the capture is ${u}×${y} gray (seeking ${We} px / holding ${ke} px, worker), fx ${O.toFixed(1)} fy ${D.toFixed(1)} cx ${C.toFixed(1)} cy ${T.toFixed(1)} px`)}const P=[],q=ze(o.viewMatrix);t.forEach((O,D)=>{if(I(O)!=="holding")return;let C=1/0,T=1/0,W=-1/0,re=-1/0,le=0;const me=O.anchorPose;if(me&&q){const Q=Me(me),ne=O.target.size/2;for(const[se,de]of[[-ne,-ne],[ne,-ne],[ne,ne],[-ne,ne]]){const ye=Q[0]*se+Q[8]*de+Q[12],Je=Q[1]*se+Q[9]*de+Q[13],et=Q[2]*se+Q[10]*de+Q[14],we=_t({x:ye,y:Je,z:et},E,q,u,y);if(!we){C=1/0;break}C=Math.min(C,we[0]),W=Math.max(W,we[0]),T=Math.min(T,we[1]),re=Math.max(re,we[1])}le=Math.max(W-C,re-T)}if(!(C<1/0)&&O.corners){const Q=u/O.cornersIn[0],ne=y/O.cornersIn[1];C=1/0,T=1/0,W=-1/0,re=-1/0;for(let se=0;se<4;se++){const de=O.corners[2*se]*Q,ye=O.corners[2*se+1]*ne;C=Math.min(C,de),W=Math.max(W,de),T=Math.min(T,ye),re=Math.max(re,ye)}le=O.side*Math.max(Q,ne)}if(!(C<1/0))return;const ie=Qt*le;P.push([D,Math.floor(C-ie),Math.floor(T-ie),Math.ceil(W+ie),Math.ceil(re+ie)])});const U=o.data,B=U.byteOffset===0&&U.byteLength===U.buffer.byteLength?U.buffer:U.slice().buffer,ee=++m;c={seq:ee,phase:l,picture:o,askedAt:a,capturedAt:p,stale:!1},j({type:"frame",seq:ee,phase:l,gray:B,width:u,height:y,p0:E[0],p5:E[5],p8:E[8],p9:E[9],hints:P},[B])},w=o=>{const l=g;return g=o,!!l&&Ce(o.position,l.position)<Ut&&De(o.rotation,l.rotation)<Gt},A=(o,l,a)=>(l?o/l:0).toFixed(a);return{setCodes(o){const l=o.map(u=>t.find(y=>y.target.entity===u.entity&&y.target.text===u.text&&y.target.size===u.size)??X(u));for(const u of t)l.includes(u)||J(u,!0);const a=l.length!==t.length||l.some((u,y)=>u!==t[y]);t=l,a&&(c&&(c.stale=!0),j({type:"targets",targets:_()})),Y()},frame(o,l,a){if(p++,t.length===0||!l)return;G(o,l);const u=a==null?void 0:a.views[0];if(!a||!u||a.emulatedPosition){g=null;return}const y=w(Ge(a.transform));v++;const E=t.map(I),P=t.some((O,D)=>E[D]==="seeking"||E[D]==="holding"&&O.misses>=be);if(P||E.includes("holding")){const O=performance.now(),D=P?"seeking":"holding";if(!x.since||x.phase!==D)x.phase=D,R(O);else if(O-x.since>=Zt){const C=x.captures,T=t.map(W=>t.length>1?`${W.target.text} ${W.depth.scale.toFixed(3)}`:W.depth.scale.toFixed(3)).join(", ");console.log(`[ar] qr: ${D} — ${C} captures in ${((O-x.since)/1e3).toFixed(0)} s, ${x.found} with a code, the picture after ${A(x.captureFrames,C,1)} frames, the result after ${A(x.resultFrames,C,1)}, worker ${A(x.workerMs,C,1)} ms mean, depth scale ${T}${t.some(W=>W.misses>=be)?`, sought again after ${be} misses`:""}`),R(O)}}else x.since=0;if(d||!i)return;let q;if(P)q=v>=Wt;else{const O=t.some((C,T)=>E[T]==="holding"&&C.depth.estimate===null)?Ht:Nt,D=v>=O&&y?ze(u.transform.matrix):null;q=!!D&&t.some((C,T)=>E[T]==="holding"&&C.anchorPose&&Ot(C.anchorPose.position,u.projectionMatrix,D))}if(!q)return;v=0,d=!0;const U=n,B=p,ee=P?"seeking":"holding";e.capture(P?We:ke).then(O=>{U===n&&(O?Z(O,ee,B):d=!1)},O=>{U===n&&(d=!1,console.warn(`[ar] qr: the capture failed (${Be(O)})`))})},clear(o=!1){n++,Fe=o;try{for(const l of t)J(l,!1)}finally{Fe=!1}t=[],V(),h=!1,v=0,g=null,b.clear(),R(0)}}},Ve=["hit-test","anchors","dom-overlay","camera-access","light-estimation"],rn=4,on=100,Xe=new Set,oe=(e,t)=>{Xe.has(e)||(Xe.add(e),console.warn(`[ar] ${t}`))},sn=async e=>{const t=typeof navigator>"u"?void 0:navigator.xr;if(!t||typeof e.presentXR!="function")return!1;try{return await t.isSessionSupported("immersive-ar")}catch{return!1}},Ke=(e,t,n,r)=>{const s=e[3]*t+e[7]*n+e[11]*r+e[15];return[(e[0]*t+e[4]*n+e[8]*r+e[12])/s,(e[1]*t+e[5]*n+e[9]*r+e[13])/s,(e[2]*t+e[6]*n+e[10]*r+e[14])/s]},an=(e,t,n,r)=>{const s=t.scene;let i=0,f=!1,h=!1,c=null,m=null,d=null,p=null,v=null,g=new Set,b="",x=!1,R=null,$=[0,0,0],M=null,_=-1/0,j=-1,N=!0,V=[],H=[];const Y=new Map,X=new Map,J=nn({setMatrix:(w,A)=>e.setMatrix(w,A),image:w=>n.image(w),capture:w=>p?p.captureCamera({gray:!0,maxWidth:w}):Promise.resolve(null),anchor:w=>{H.push(w)},anchors:()=>g.has("anchors"),workerUrl:async()=>{var w;return(w=await(r==null?void 0:r()))==null?void 0:w.qrAnchorWorker}}),k=w=>w.preventDefault(),I=w=>{let A="normal",o;(!w||w.emulatedPosition)&&(A="limited",o=x?"relocalizing":"initializing"),w&&!w.emulatedPosition&&(x=!0);const l=`${A}:${o??""}`;l!==b&&(b=l,n.tracking(o?{state:A,reason:o}:{state:A}))},F=(w,A)=>{const o=[];for(const l of w.getHitTestResults(A)){const a=m?l.getPose(m):void 0;if(!a)continue;const u=a.transform.position,y=a.transform.orientation;o.push({type:"estimated",pose:{position:{x:u.x,y:u.y,z:u.z},rotation:{x:y.x,y:y.y,z:y.z,w:y.w}},distance:Math.hypot(u.x-$[0],u.y-$[1],u.z-$[2])})}return o.sort((l,a)=>l.distance-a.distance)},L=(w,A)=>{var y;if(!A.wanted()){A.settle(null,!1);return}const o=i,{position:l,rotation:a}=A.pose;let u;try{u=m?(y=w.createAnchor)==null?void 0:y.call(w,new XRRigidTransform({x:l.x,y:l.y,z:l.z,w:1},{x:a.x,y:a.y,z:a.z,w:a.w}),m):void 0}catch(E){u=Promise.reject(E)}if(!u){A.settle(null,!1);return}u.then(E=>{if(o!==i||!A.wanted()){E.delete(),A.settle(null,o!==i);return}A.settle(E,!1)},E=>{oe("anchor",`the session made no anchor (${E instanceof Error?E.message:E}): an anchored node stays where it was put`),A.settle(null,o!==i)})},G=(w,A)=>{var u;if(!M||w-_<on)return;const o=(u=A.getLightEstimate)==null?void 0:u.call(A,M);if(!o)return;_=w;const l=o.primaryLightIntensity,a=Math.max(l.x,l.y,l.z)*1e3;Math.abs(a-j)<1||(j=a,n.light({intensity:a,temperature:6500}))},K=(w,A,o)=>{if(A.session!==c)return;I(o);const l=o==null?void 0:o.views[0];if(o&&l){R=ze(l.projectionMatrix);const a=o.transform.position;$=[a.x,a.y,a.z]}for(const[a,u]of X){const E=(A.trackedAnchors?A.trackedAnchors.has(u.anchor):!0)&&m?A.getPose(u.anchor.anchorSpace,m):void 0;E&&e.setMatrix(a,E.transform.matrix);const P=!!E;P&&(u.seen=!0),P!==u.tracked&&u.seen&&(u.tracked=P,n.anchor({entity:a,tracked:P}))}if(J.frame(A,m,o),H.length){const a=H;H=[];for(const u of a)L(A,u)}V.length&&(V=V.filter(a=>{const u=F(A,a.source);return u.length===0&&++a.frames<rn?!0:(a.source.cancel(),a.resolve(u),!1)})),G(w,A),v==null||v()},te=(w=!1)=>{var l;i++,f=!1,h=!1,v==null||v(new z("failed")),p==null||p.end(),p=null;const A=c;c=null,A&&(A.removeEventListener("end",Z),w||A.end().catch(()=>{}),s!==void 0&&e.clearProjection(s)),(l=document.body)==null||l.removeEventListener("beforexrselect",k);for(const a of V){if(!w)try{a.source.cancel()}catch{}a.reject(new z("notRunning"))}V=[];const o=H;H=[];for(const a of o)a.settle(null,!0);if(Y.clear(),!w)for(const a of X.values())try{a.anchor.delete()}catch{}X.clear(),J.clear(w),m=null,d=null,g=new Set,b="",x=!1,R=null,$=[0,0,0],M=null,_=-1/0,j=-1},Z=()=>{const w=f;te(!0),w&&n.ended({reason:"user"})};return{async start(w){var u;if(f||h)throw new z("busy");if(s===void 0||t.camera===void 0)throw console.warn("[ar] start: the session has no scene or camera to drive (ARScene opens it with both)"),new z("failed");const A=navigator.xr,o=e.presentXR;if(!A||!o)throw new z("unavailable");((w==null?void 0:w.mode)==="images"||(u=w==null?void 0:w.images)!=null&&u.length)&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked"),(w==null?void 0:w.worldAlignment)==="gravityAndHeading"&&oe("heading","WebXR has no compass: the world is aligned with gravity alone, −Z where the session began"),h=!0;const l=++i;J.setCodes((w==null?void 0:w.codes)??[]);let a;try{a=await A.requestSession("immersive-ar",{requiredFeatures:["local"],optionalFeatures:Ve,domOverlay:{root:document.body}})}catch(y){l===i&&(h=!1);const E=y instanceof Error?y.name:"";throw console.warn(`[ar] no AR session: ${y instanceof Error?y.message:y}`),new z(E==="NotAllowedError"||E==="SecurityError"?"denied":E==="NotSupportedError"?"unavailable":"failed")}if(l!==i)throw a.end().catch(()=>{}),new z("failed");c=a,a.addEventListener("end",Z),document.body.addEventListener("beforexrselect",k),g=new Set(a.enabledFeatures??["local",...Ve]);try{const y=await a.requestReferenceSpace("local");if(m=y,d=await a.requestReferenceSpace("viewer"),w!=null&&w.lightEstimation){const q=a.requestLightProbe;g.has("light-estimation")&&q?M=await q.call(a).catch(U=>(oe("light",`no light estimate: ${U instanceof Error?U.message:U}`),null)):oe("light","the AR session has no light estimate: no light event comes")}if(l!==i)throw new z("failed");const E=new Promise((q,U)=>{v=B=>{v=null,B?U(B):q()}}),P=await o.call(e,s,a,y,K);if(l!==i)throw P.end(),new z("failed");p=P,await E}catch(y){throw y instanceof z||console.warn(`[ar] the AR session did not start: ${y instanceof Error?y.message:y}`),l===i&&te(),y instanceof z?y:new z("failed")}if(l!==i)throw new z("failed");h=!1,f=!0},stop(){te()},setImages(w){if(!f)throw new z("notRunning");w.length&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked")},setCodes(w){if(!f)throw new z("notRunning");J.setCodes(w)},async hitTest(w,A,o){const l=c;if(!f||!l||!d||s===void 0)throw new z("notRunning");if(!g.has("hit-test")||!l.requestHitTestSource)return oe("hit-test","the AR session has no hit test: hitTest finds nothing"),[];const a=e.viewport(s),u=R;if(!u||!a.width||!a.height)return[];const y=w/a.width*2-1,E=1-A/a.height*2,P=Ke(u,y,E,-1),q=Ke(u,y,E,1),U={x:q[0]-P[0],y:q[1]-P[1],z:q[2]-P[2],w:0},B=new XRRay({x:0,y:0,z:0,w:1},U),ee=i,O=async T=>l.requestHitTestSource({space:d,offsetRay:B,entityTypes:T});let D;try{D=await O(o==="planes"||!N?["plane"]:["plane","point"])}catch(T){if(o==="planes"||!N||ee!==i){if(oe("hit-source",`the session made no hit test: ${T instanceof Error?T.message:T}`),ee!==i)throw new z("notRunning");return[]}N=!1;try{D=await O(["plane"])}catch(W){oe("hit-source",`the session made no hit test: ${W instanceof Error?W.message:W}`)}}if(ee!==i)throw D==null||D.cancel(),new z("notRunning");if(!D)return[];const C=D;return new Promise((T,W)=>{V.push({source:C,frames:0,resolve:T,reject:W})})},anchor(w,A){if(!f||!c)throw new z("notRunning");if(e.setMatrix(w,Me(A)),!g.has("anchors")){oe("anchors","the AR session has no anchors: an anchored node stays where it was put (the world map does not refine it)");return}const o=X.get(w);if(o){X.delete(w);try{o.anchor.delete()}catch{}}return new Promise((l,a)=>{const u={pose:A,wanted:()=>Y.get(w)===u,settle:(y,E)=>{Y.get(w)===u&&Y.delete(w),y&&X.set(w,{anchor:y,tracked:!0,seen:!1}),E?a(new z("notRunning")):l()}};Y.set(w,u),H.push(u)})},release(w){Y.delete(w);const A=X.get(w);if(A){X.delete(w);try{A.anchor.delete()}catch{}}},async captureFrame(){const w=p;if(!f||!w)throw new z("notRunning");const A=i,o=await w.captureCamera();if(A!==i)throw new z("notRunning");if(!o)throw oe("capture","captureFrame: the AR session gives no camera picture (WebXR's camera-access: Chrome on Android, the camera allowed)"),new z("failed");const l=document.createElement("canvas");l.width=o.width,l.height=o.height;const a=l.getContext("2d");if(!a)throw new z("failed");a.putImageData(new ImageData(new Uint8ClampedArray(o.rgba.buffer,o.rgba.byteOffset,o.rgba.byteLength),o.width,o.height),0,0);const u=await new Promise(y=>l.toBlob(y,"image/jpeg",.9));if(!u)throw new z("failed");return{data:await u.arrayBuffer(),name:"frame.jpg"}},close(){te()}}},cn=(e,t)=>{var r;if(typeof navigator>"u"||!((r=navigator.mediaDevices)!=null&&r.getUserMedia))return;const n=e.gl;yt(e,(s,i)=>{let f=null,h=null,c=null,m=!1,d=0;const p=()=>{if(!c)throw new z("notRunning");return c},v=()=>{d++;const g=c;return c=null,g};return{async start(g){if(m)throw new z("busy");if(c)return c.start(g);m=!0;const b=d;let x;try{x=await sn(n)?h??(h=an(n,s,i,t)):f??(f=zt(n,s,i))}finally{m=!1}if(b!==d)throw new z("failed");c=x;try{await x.start(g)}catch(R){throw c===x&&(c=null),R}},stop(){var g;return(g=v())==null?void 0:g.stop()},setImages(g){return p().setImages(g)},setCodes(g){return p().setCodes(g)},hitTest(g,b,x){return p().hitTest(g,b,x)},anchor(g,b){return p().anchor(g,b)},release(g){return c==null?void 0:c.release(g)},captureFrame(){return p().captureFrame()},close(){var g,b;(b=(g=v())==null?void 0:g.close)==null||b.call(g)}}})},dn=e=>cn(e,()=>st(()=>import("./_virtual_lecodes-half-resources-CNYmVR88.js"),[],import.meta.url).then(t=>t.resources));export{dn as register};
|
|
@@ -11,16 +11,17 @@ import {
|
|
|
11
11
|
type AppConfig, ANDROID_SDK_VARIANTS, type AndroidSdkVariant, VERSION_RE, androidSdkArtifact, androidVariantEngines,
|
|
12
12
|
childEnv, cmpVersion, ensureShellIgnored, fetchLatestSdkVersion,
|
|
13
13
|
projectRoot, readAppConfigAt, readJson, resolvePlugin, resolveSdkVersion, resolveVariant, runTool, sdkSatisfies, slugify,
|
|
14
|
-
writeJson, type LockEntry, resolveUpdateUrl, urlSchemeOf,
|
|
14
|
+
writeJson, type LockEntry, resolveUpdateUrl, urlSchemeOf, appLinksOf, appLinksSiteNote, type AppLink,
|
|
15
15
|
} from "./shared"
|
|
16
16
|
import { loadIconMaster, restampManifestIcon, writeAndroidIcons } from "./icon"
|
|
17
|
+
import { androidSplashThemeHint, loadSplashMaster, writeAndroidSplash } from "./splash"
|
|
17
18
|
import {
|
|
18
19
|
ANDROID_GITIGNORE, ANDROID_README, APP_PROGUARD_RULES, COLORS_XML, GRADLE_PROPERTIES, GRADLE_VERSION,
|
|
19
20
|
GRADLE_PLUGIN_VERSIONS, IC_LAUNCHER_XML, aarWrapperBuildGradle, appBuildGradleTemplate,
|
|
20
21
|
appManifestTemplate, appViewModelTemplate, gradleWrapperProperties, mainActivityTemplate,
|
|
21
22
|
pluginBuildGradleTemplate, rootBuildGradleTemplate, runtimeBuildGradleTemplate,
|
|
22
|
-
runtimeKotlinTemplate, settingsGradleTemplate, stringsXmlTemplate, themesXmlTemplate,
|
|
23
|
-
type AndroidRuntimePlugin, type AndroidShellIdentity, runtimeManifestTemplate, urlSchemeFilter,
|
|
23
|
+
runtimeKotlinTemplate, runtimeThemeTemplate, settingsGradleTemplate, stringsXmlTemplate, themesXmlTemplate,
|
|
24
|
+
type AndroidRuntimePlugin, type AndroidShellIdentity, runtimeManifestTemplate, urlSchemeFilter, appLinksFilter,
|
|
24
25
|
} from "./templates/android"
|
|
25
26
|
import { GRADLEW_BAT, GRADLEW_SH } from "./templates/gradlew"
|
|
26
27
|
|
|
@@ -38,6 +39,10 @@ export const ANDROID_SDK_REPO = "letary/lecodes-android-sdk"
|
|
|
38
39
|
* :lecodes-ui). A 2.x bundle is refused by a 1.x runtime (`_creatorApp.run()` gates on the major),
|
|
39
40
|
* so a shell pinned below this would install and show nothing; `lecodes app update` moves a 1.x
|
|
40
41
|
* shell here. (1.2.0 was the 1.x floor: system back moved into engine.attach().) */
|
|
42
|
+
// TODO(docs/plans/ota-splash-plan.md step 1): raise to the first lecodes-android-sdk release with
|
|
43
|
+
// `LeCodesShell.boot(engine, activity, …)` (the first-launch wait + the splash hold), which the
|
|
44
|
+
// AppViewModel template now calls — the owner fills the version in at release time. (Against an
|
|
45
|
+
// older AAR the call still compiles: it lands on the context form, which never waits.)
|
|
41
46
|
export const ANDROID_SDK_MIN_VERSION = "2.0.13"
|
|
42
47
|
|
|
43
48
|
/** First lecodes-android-sdk release that publishes the variant matrix (lecodes-sdk-3d / -2d /
|
|
@@ -171,7 +176,7 @@ export const initAndroid = async (opts: AndroidInitOptions) => {
|
|
|
171
176
|
|
|
172
177
|
write("app/build.gradle.kts", appBuildGradleTemplate(identity, config.android!.minify ?? true))
|
|
173
178
|
write("app/proguard-rules.pro", APP_PROGUARD_RULES)
|
|
174
|
-
write("app/src/main/AndroidManifest.xml", appManifestTemplate(urlSchemeOf(config)))
|
|
179
|
+
write("app/src/main/AndroidManifest.xml", appManifestTemplate(urlSchemeOf(config), appLinksOf(config)))
|
|
175
180
|
write(`app/src/main/java/${pkgPath}/MainActivity.kt`, mainActivityTemplate(identity.applicationId))
|
|
176
181
|
write(`app/src/main/java/${pkgPath}/AppViewModel.kt`, appViewModelTemplate(identity.applicationId))
|
|
177
182
|
write("app/src/main/res/values/themes.xml", themesXmlTemplate())
|
|
@@ -184,6 +189,8 @@ export const initAndroid = async (opts: AndroidInitOptions) => {
|
|
|
184
189
|
const initArtifact = cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) >= 0 ? androidSdkArtifact(initVariant, sdkVersion) : "lecodes-sdk"
|
|
185
190
|
write("lecodes-runtime/build.gradle.kts", runtimeBuildGradleTemplate(sdkVersion, [], initArtifact))
|
|
186
191
|
write("lecodes-runtime/src/main/kotlin/io/letary/lecodes/runtime/LeCodesRuntime.kt", runtimeKotlinTemplate([]))
|
|
192
|
+
// The parent of app/'s Theme.LeCodesApp (black until a sync renders app.json "splash" into it).
|
|
193
|
+
write("lecodes-runtime/src/main/res/values/lecodes_splash.xml", runtimeThemeTemplate())
|
|
187
194
|
|
|
188
195
|
log("")
|
|
189
196
|
log(`${c.green("sdk")} lecodes-android-sdk ${sdkVersion}${sdk === "latest" ? c.dim(" (android.sdk: latest — re-resolved on every sync)") : c.dim(" (pinned in app.json android.sdk — `lecodes app update` moves it)")}`)
|
|
@@ -191,6 +198,7 @@ export const initAndroid = async (opts: AndroidInitOptions) => {
|
|
|
191
198
|
log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`)
|
|
192
199
|
log(` ${c.bold("lecodes app open android")} then run on your device from Android Studio`)
|
|
193
200
|
log(` App icon: set app.json "icon" to a square ≥1024px image — sync renders every size (or use Android Studio → Image Asset)`)
|
|
201
|
+
log(` Splash: set app.json "splash": {"background": "#RRGGBB", "image": "./splash.png"} — sync renders the splash (the image centred at 160 dp, "size" overrides; Android 12+'s system splash included)`)
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
/* ---------------------------------------- sync ---------------------------------------- */
|
|
@@ -378,7 +386,14 @@ export const syncAndroid = async (root: string, config: AppConfig, prior: SyncAr
|
|
|
378
386
|
// LeCodesShell.boot reads it — app.json "update" resolved like the iOS plist key ----
|
|
379
387
|
const updateUrl = resolveUpdateUrl(config.update, root, sdkVersion)
|
|
380
388
|
writeFileSync(join(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml"), runtimeManifestTemplate(updateUrl))
|
|
381
|
-
if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch (LeCodesShell.boot), new code
|
|
389
|
+
if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch (LeCodesShell.boot): the first launch of a build waits (≤ 5 s) for the newest bundle, later launches apply new code next launch`)
|
|
390
|
+
// ---- splash: Theme.LeCodesRuntime (the parent of the app's theme) in the runtime module's res —
|
|
391
|
+
// black without app.json "splash", the splash colour / image / Android 12+ system splash with it ----
|
|
392
|
+
await writeAndroidSplash(androidDir, await loadSplashMaster(root, config))
|
|
393
|
+
if (config.splash !== undefined) {
|
|
394
|
+
const themeHint = androidSplashThemeHint(androidDir)
|
|
395
|
+
if (themeHint) warn(`splash: ${themeHint}`)
|
|
396
|
+
}
|
|
382
397
|
|
|
383
398
|
if (pluginSpecs.length > 0 || existsSync(lockPath)) writeJson(lockPath, { plugins: newLock })
|
|
384
399
|
|
|
@@ -393,38 +408,59 @@ export const syncAndroid = async (root: string, config: AppConfig, prior: SyncAr
|
|
|
393
408
|
restampManifestIcon(androidDir)
|
|
394
409
|
}
|
|
395
410
|
restampManifestUrlScheme(androidDir, urlSchemeOf(config))
|
|
411
|
+
const appLinks = appLinksOf(config)
|
|
412
|
+
restampManifestAppLinks(androidDir, appLinks)
|
|
413
|
+
if (appLinks.length) {
|
|
414
|
+
log(`${c.green("links")} ${appLinks.map(l => l.url).join(", ")} — verified by the site's proof:`)
|
|
415
|
+
for (const line of appLinksSiteNote(appLinks, { platform: "android", applicationId: config.android?.applicationId })) log(` ${line}`)
|
|
416
|
+
}
|
|
396
417
|
|
|
397
418
|
log("")
|
|
398
419
|
success(`Android shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, v${config.version ?? "1.0.0"} (${config.android?.versionCode ?? 1}).`)
|
|
399
420
|
note("Build & run from Android Studio (`lecodes app open android`) — user files in android/app/ were not touched.")
|
|
400
421
|
}
|
|
401
422
|
|
|
402
|
-
/**
|
|
403
|
-
* activity when
|
|
423
|
+
/** A `lecodes:<tag>` block of the USER-owned manifest follows app.json: written into the
|
|
424
|
+
* activity when its key appears, rewritten when it changes, removed when it goes. A manifest
|
|
404
425
|
* without the tag and without a MainActivity to put it in is left alone, with a warning. */
|
|
405
|
-
|
|
426
|
+
const restampManifestBlock = (androidDir: string, tag: string, block: string, what: string, missing: string, after: RegExp[] = []): void => {
|
|
406
427
|
const path = join(androidDir, "app", "src", "main", "AndroidManifest.xml")
|
|
407
428
|
if (!existsSync(path)) return
|
|
408
429
|
const content = readFileSync(path, "utf8")
|
|
409
|
-
const tagged =
|
|
410
|
-
const block = urlSchemeFilter(urlScheme)
|
|
430
|
+
const tagged = new RegExp(`\\n[ \\t]*<!-- lecodes:${tag}[\\s\\S]*?<!-- \\/lecodes:${tag} -->`)
|
|
411
431
|
let next: string
|
|
412
|
-
if (tagged.test(content)) next = content.replace(tagged, block)
|
|
413
|
-
else if (!
|
|
432
|
+
if (tagged.test(content)) next = content.replace(tagged, () => block)
|
|
433
|
+
else if (!block) return
|
|
414
434
|
else {
|
|
415
|
-
// Inside the MainActivity element:
|
|
435
|
+
// Inside the MainActivity element: after the block it follows in the template when that one
|
|
436
|
+
// is there, else right after the LAUNCHER intent-filter — so a restamped manifest reads as a
|
|
437
|
+
// fresh init's.
|
|
416
438
|
const launcher = /(<category android:name="android\.intent\.category\.LAUNCHER" \/>\s*<\/intent-filter>)/
|
|
417
|
-
|
|
418
|
-
|
|
439
|
+
const anchor = [...after, launcher].find(re => re.test(content))
|
|
440
|
+
if (!anchor) {
|
|
441
|
+
warn(`couldn't find the launcher activity in app/src/main/AndroidManifest.xml — ${missing}`)
|
|
419
442
|
return
|
|
420
443
|
}
|
|
421
|
-
next = content.replace(
|
|
444
|
+
next = content.replace(anchor, (m) => `${m}${block}`)
|
|
422
445
|
}
|
|
423
446
|
if (next === content) return
|
|
424
447
|
writeFileSync(path, next)
|
|
425
|
-
log(`${c.green("updated")} android/app/src/main/AndroidManifest.xml:
|
|
448
|
+
log(`${c.green("updated")} android/app/src/main/AndroidManifest.xml: ${what}`)
|
|
426
449
|
}
|
|
427
450
|
|
|
451
|
+
/** app.json "urlScheme" → the tagged BROWSABLE intent-filter (`urlSchemeFilter`). */
|
|
452
|
+
export const restampManifestUrlScheme = (androidDir: string, urlScheme: string | undefined): void =>
|
|
453
|
+
restampManifestBlock(androidDir, "urlScheme", urlSchemeFilter(urlScheme),
|
|
454
|
+
`urlScheme ${urlScheme ? `→ ${urlScheme}://` : "removed"}`,
|
|
455
|
+
`add an intent-filter for the "${urlScheme}" scheme yourself.`)
|
|
456
|
+
|
|
457
|
+
/** app.json "appLinks" → the tagged autoVerify intent-filters (`appLinksFilter`). */
|
|
458
|
+
export const restampManifestAppLinks = (androidDir: string, links: AppLink[]): void =>
|
|
459
|
+
restampManifestBlock(androidDir, "appLinks", appLinksFilter(links),
|
|
460
|
+
`appLinks ${links.length ? `→ ${links.map(l => l.url).join(", ")}` : "removed"}`,
|
|
461
|
+
`add the autoVerify intent-filters for ${links.map(l => l.url).join(", ")} yourself.`,
|
|
462
|
+
[/<!-- \/lecodes:urlScheme -->/])
|
|
463
|
+
|
|
428
464
|
/** Values-only restamps + tagged-line management in the USER-owned app/build.gradle.kts —
|
|
429
465
|
* the Android analog of the pbxproj MARKETING_VERSION restamp + CODE_SIGN_ENTITLEMENTS
|
|
430
466
|
* injection: warn-not-fail when the user restructured, and the ONLY lines sync ever removes
|
|
@@ -1025,9 +1061,22 @@ export const doctorAndroidChecks = async (
|
|
|
1025
1061
|
check(existsSync(manifest) && readFileSync(manifest, "utf8").includes("io.letary.lecodes.UPDATE_URL"), "OTA update URL in the runtime manifest", "run `lecodes app sync`", false)
|
|
1026
1062
|
const appSrc = join(androidDir, "app", "src", "main", "java")
|
|
1027
1063
|
const kt = existsSync(appSrc) ? readdirSync(appSrc, { recursive: true }).map(String).filter((f) => f.endsWith(".kt")) : []
|
|
1028
|
-
const
|
|
1064
|
+
const sources = kt.map((f) => readFileSync(join(appSrc, f), "utf8"))
|
|
1065
|
+
const boots = sources.some((src) => src.includes("LeCodesShell.boot"))
|
|
1029
1066
|
check(boots, "App code launches via the updater (LeCodesShell.boot)",
|
|
1030
|
-
"no app/src/main/java/**/*.kt calls LeCodesShell.boot(engine,
|
|
1067
|
+
"no app/src/main/java/**/*.kt calls LeCodesShell.boot(engine, activity) — downloaded updates would never run; see \"Over-the-air updates\" in android/README.md for the AppViewModel snippet", false)
|
|
1068
|
+
// The context form (the pre-wait shape, `boot(engine, getApplication())`) still runs downloads,
|
|
1069
|
+
// but only from the NEXT launch: no view to hold, so the first launch of a build never waits.
|
|
1070
|
+
const waits = sources.some((src) => /LeCodesShell\.boot\(\s*engine\s*,\s*activity\b/.test(src))
|
|
1071
|
+
if (boots) check(waits, "The first launch waits for the newest bundle (LeCodesShell.boot(engine, activity))",
|
|
1072
|
+
"AppViewModel calls the context form of LeCodesShell.boot — pass the activity instead (onActivityCreate's parameter) so a fresh install runs the newest bundle at once; see \"Over-the-air updates\" in android/README.md", false)
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (config.splash !== undefined) {
|
|
1076
|
+
// The splash lives in Theme.LeCodesRuntime (the runtime module); the user-owned app theme must
|
|
1077
|
+
// inherit it — a shell generated before the key existed is parented on AppCompat directly.
|
|
1078
|
+
const themeHint = androidSplashThemeHint(androidDir)
|
|
1079
|
+
check(!themeHint, "The app theme inherits the splash (Theme.LeCodesRuntime)", themeHint ?? "", false)
|
|
1031
1080
|
}
|
|
1032
1081
|
|
|
1033
1082
|
const signing = resolveAndroidSigning(androidDir)
|
|
@@ -3,25 +3,36 @@ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statS
|
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path"
|
|
5
5
|
import { readManifest } from "../../project/manifest"
|
|
6
|
+
import { binaryName } from "../../plugin/manifest"
|
|
6
7
|
import { compileShell, type BundleResource } from "../compile"
|
|
7
8
|
import { CliError, UsageError, bool, c, defineCommand, log, note, str, success, warn } from "../../cli"
|
|
8
9
|
import {
|
|
9
10
|
ACCENT_COLOR_JSON, APP_DELEGATE, APP_ICON_JSON, ASSETS_ROOT_JSON, IOS_README, LAUNCH_STORYBOARD,
|
|
10
11
|
SCENE_DELEGATE, SCHEME_XML, VIEW_CONTROLLER, entitlementsTemplate, infoPlistTemplate,
|
|
11
|
-
pbxprojTemplate, runtimePackageTemplate, runtimeSwiftTemplate,
|
|
12
|
+
pbxprojTemplate, runtimeBinaryPath, runtimePackageTemplate, runtimeSwiftTemplate,
|
|
12
13
|
type RuntimePlugin, type SdkVariant, type ShellIdentity,
|
|
13
14
|
} from "./templates/ios"
|
|
14
15
|
import {
|
|
15
16
|
FIRST_PARTY_PLUGINS, SDK_VARIANTS, cmpVersion, ensureShellIgnored,
|
|
16
|
-
fetchLatestSdkVersion,
|
|
17
|
+
fetchLatestSdkVersion, latestPluginTag, localPlugin, localPluginKey, pluginSpecsFor, projectRoot, readAppConfigAt, readJson, resolvePlugin, resolveSdkVersion, sdkSatisfies,
|
|
17
18
|
resolveVariant, runTool, slugify, writeJson, VERSION_RE, type AppConfig, type LockEntry, resolveUpdateUrl, urlSchemeOf,
|
|
19
|
+
appLinksOf, appLinksSiteNote, associatedDomainsOf,
|
|
18
20
|
} from "./shared"
|
|
21
|
+
import { isLocalPluginSpec, type PluginEntry } from "lecodes-sdk/compile"
|
|
19
22
|
import {
|
|
20
23
|
ANDROID_SDK_MIN_VERSION, ANDROID_SDK_REPO, androidBuild, doctorAndroidChecks, initAndroid,
|
|
21
24
|
keystore, openAndroid, shellSdkVersionAndroid, syncAndroid, type SyncArtifacts,
|
|
22
25
|
} from "./android"
|
|
23
26
|
import { desktopBuild, initDesktop } from "./desktop"
|
|
24
27
|
import { loadIconMaster, writeIosAppIcon } from "./icon"
|
|
28
|
+
import { loadSplashMaster, writeIosSplash } from "./splash"
|
|
29
|
+
|
|
30
|
+
/** The entitlements of app.json alone (init, before any plugin): the associated domains of
|
|
31
|
+
* "appLinks". Sync merges the plugins' values on top of these. */
|
|
32
|
+
const iosEntitlements = (base: Record<string, unknown>, config: AppConfig): Record<string, unknown> => {
|
|
33
|
+
const domains = associatedDomainsOf(appLinksOf(config))
|
|
34
|
+
return domains.length ? { ...base, "com.apple.developer.associated-domains": domains } : base
|
|
35
|
+
}
|
|
25
36
|
|
|
26
37
|
/*
|
|
27
38
|
* `lecodes app` — native app shells for a LeCodes project (docs/xcode-app-plan.md). This file is
|
|
@@ -67,6 +78,10 @@ const SDK_REPO = "letary/lecodes-ios-sdk"
|
|
|
67
78
|
// it); this floor is only the last-resort fallback when the network is unavailable at init. A 1.x
|
|
68
79
|
// shell is moved here by `lecodes app update` (its CLI-owned files are regenerated; its
|
|
69
80
|
// App/ViewController.swift keeps compiling: LeCodesViewController and LeCodesUpdater exist in 2.x).
|
|
81
|
+
// TODO(docs/plans/ota-splash-plan.md step 1): raise to the first lecodes-ios-sdk release with the
|
|
82
|
+
// first-launch wait (LeCodesShell.boot's `firstLaunchTimeout` + the launch-screen hold of
|
|
83
|
+
// LeCodesShellViewController) — the owner fills the version in at release time. (An older SDK
|
|
84
|
+
// still builds the generated shell: it just never waits.)
|
|
70
85
|
const SDK_MIN_VERSION = "2.0.7"
|
|
71
86
|
|
|
72
87
|
// First lecodes-ios-sdk release with `LeCodesEngine.bootProjectUuid` — the shell's world
|
|
@@ -166,7 +181,7 @@ const initIos = async (opts: IosInitOptions): Promise<void> => {
|
|
|
166
181
|
write("App/SceneDelegate.swift", SCENE_DELEGATE)
|
|
167
182
|
write("App/ViewController.swift", VIEW_CONTROLLER)
|
|
168
183
|
write("App/Info.plist", infoPlistTemplate({}, undefined, urlSchemeOf(config)))
|
|
169
|
-
write("App/App.entitlements", entitlementsTemplate({}))
|
|
184
|
+
write("App/App.entitlements", entitlementsTemplate(iosEntitlements({}, config)))
|
|
170
185
|
write("App/Base.lproj/LaunchScreen.storyboard", LAUNCH_STORYBOARD)
|
|
171
186
|
write("App/Assets.xcassets/Contents.json", ASSETS_ROOT_JSON)
|
|
172
187
|
write("App/Assets.xcassets/AccentColor.colorset/Contents.json", ACCENT_COLOR_JSON)
|
|
@@ -182,6 +197,7 @@ const initIos = async (opts: IosInitOptions): Promise<void> => {
|
|
|
182
197
|
log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`)
|
|
183
198
|
log(` ${c.bold("open ios/App.xcodeproj")} then run on your device from Xcode`)
|
|
184
199
|
log(` App icon: set app.json "icon" to a square ≥1024px image — sync renders every size (or drop a PNG into App/Assets.xcassets/AppIcon.appiconset/)`)
|
|
200
|
+
log(` Splash: set app.json "splash": {"background": "#RRGGBB", "image": "./splash.png"} — sync writes the launch screen (the image centred at 160 pt, "size" overrides; or edit App/Base.lproj/LaunchScreen.storyboard yourself)`)
|
|
185
201
|
}
|
|
186
202
|
|
|
187
203
|
const init = defineCommand({
|
|
@@ -255,7 +271,9 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
|
|
|
255
271
|
const entitlementValues: Record<string, unknown[]> = {}
|
|
256
272
|
const newLock: Record<string, LockEntry> = {}
|
|
257
273
|
|
|
258
|
-
const
|
|
274
|
+
const runtimeDir = join(iosDir, "LeCodesRuntime")
|
|
275
|
+
const sourcesDir = join(runtimeDir, "Sources")
|
|
276
|
+
const binariesDir = join(runtimeDir, "Binaries")
|
|
259
277
|
for (const [id, spec] of pluginSpecs) {
|
|
260
278
|
const plugin = resolvePlugin(id, spec, oldLock[id], "ios")
|
|
261
279
|
const ios = plugin.manifest.ios!
|
|
@@ -268,6 +286,11 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
|
|
|
268
286
|
const dest = join(sourcesDir, module)
|
|
269
287
|
rmSync(dest, { recursive: true, force: true })
|
|
270
288
|
cpSync(src, dest, { recursive: true })
|
|
289
|
+
// Its prebuilt xcframeworks (manifest ios.binaries) beside the sources, replaced the same way:
|
|
290
|
+
// a binary target's path is inside the package (SwiftPM takes none outside it).
|
|
291
|
+
const binaries = (ios.binaries ?? []).map(path => ({ path, name: binaryName(path) }))
|
|
292
|
+
rmSync(join(binariesDir, id), { recursive: true, force: true })
|
|
293
|
+
for (const b of binaries) cpSync(join(plugin.dir, b.path), join(runtimeDir, runtimeBinaryPath(id, b.name)), { recursive: true, verbatimSymlinks: true })
|
|
271
294
|
|
|
272
295
|
// Hard error, not a warning: outside the plugin's `sdk` range the vendored sources reference
|
|
273
296
|
// an SDK API the linked release does not have (or another contract major), so the shell
|
|
@@ -286,9 +309,9 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
|
|
|
286
309
|
throw new CliError(`Plugin "${id}": manifest ios.packages entries need { url, exact | from, products: [...] } — got ${JSON.stringify(pkg)}`)
|
|
287
310
|
}
|
|
288
311
|
}
|
|
289
|
-
runtimePlugins.push({ id, module, register, packages: ios.packages })
|
|
312
|
+
runtimePlugins.push({ id, module, register, packages: ios.packages, binaries: binaries.map(b => b.name) })
|
|
290
313
|
newLock[id] = plugin.lock
|
|
291
|
-
log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`)
|
|
314
|
+
log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module}${binaries.length ? `, Binaries/${id}` : ""})`)
|
|
292
315
|
}
|
|
293
316
|
|
|
294
317
|
// Remove targets of uninstalled plugins (everything under Sources/ except the runtime itself).
|
|
@@ -299,6 +322,15 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
|
|
|
299
322
|
log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`)
|
|
300
323
|
}
|
|
301
324
|
}
|
|
325
|
+
// …and their binaries (Binaries/<id>/); the folder goes with the last of them.
|
|
326
|
+
const installed = new Set(runtimePlugins.map(p => p.id))
|
|
327
|
+
for (const entry of existsSync(binariesDir) ? readdirSync(binariesDir) : []) {
|
|
328
|
+
if (!installed.has(entry) && statSync(join(binariesDir, entry)).isDirectory()) {
|
|
329
|
+
rmSync(join(binariesDir, entry), { recursive: true })
|
|
330
|
+
log(`${c.yellow("removed")} ios/LeCodesRuntime/Binaries/${entry} (plugin uninstalled)`)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (existsSync(binariesDir) && readdirSync(binariesDir).length === 0) rmSync(binariesDir, { recursive: true })
|
|
302
334
|
|
|
303
335
|
// ---- bundle: compile self-contained into Resources/, then embed the assets it references ----
|
|
304
336
|
const remotePatterns = config.resources?.remote ?? []
|
|
@@ -374,7 +406,7 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
|
|
|
374
406
|
for (const [key, values] of Object.entries(infoValues)) mergedKeys[key] = [...new Set(values)].join(" ")
|
|
375
407
|
const updateUrl = resolveUpdateUrl(config.update, root, sdkVersion)
|
|
376
408
|
writeFileSync(join(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys, updateUrl, urlSchemeOf(config)))
|
|
377
|
-
if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch, new code
|
|
409
|
+
if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch: the first launch of a build waits (≤ 5 s) for the newest bundle, later launches apply new code next launch${config.update === undefined ? c.dim(' ("update": false in app.json turns it off)') : ""}`)
|
|
378
410
|
else if (config.update === false) log(`${c.green("update")} off (app.json "update": false)`)
|
|
379
411
|
else note(`no OTA: a local-only project has no bundle on the platform — push it, or set "update": {"url": "https://…/app.js"} for custom hosting`)
|
|
380
412
|
|
|
@@ -392,6 +424,13 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
|
|
|
392
424
|
// get the CODE_SIGN_ENTITLEMENTS reference injected the first time a plugin brings a
|
|
393
425
|
// capability — the ONE structural pbxproj edit sync ever makes, idempotent and line-shaped.
|
|
394
426
|
const mergedEntitlements: Record<string, unknown> = {}
|
|
427
|
+
// app.json "appLinks" → associated-domains, merged like a plugin's array value (a plugin may
|
|
428
|
+
// declare the key too, e.g. a web-credentials domain). The key gone = the domains gone: the
|
|
429
|
+
// file is regenerated whole.
|
|
430
|
+
const appLinks = appLinksOf(config)
|
|
431
|
+
if (appLinks.length) {
|
|
432
|
+
(entitlementValues["com.apple.developer.associated-domains"] ??= []).unshift(associatedDomainsOf(appLinks))
|
|
433
|
+
}
|
|
395
434
|
for (const [key, values] of Object.entries(entitlementValues)) {
|
|
396
435
|
if (values.every(v => Array.isArray(v))) {
|
|
397
436
|
const seen = new Set<string>()
|
|
@@ -404,6 +443,10 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
|
|
|
404
443
|
mergedEntitlements[key] = values[0]
|
|
405
444
|
}
|
|
406
445
|
}
|
|
446
|
+
if (appLinks.length) {
|
|
447
|
+
log(`${c.green("links")} ${appLinks.map(l => l.url).join(", ")} — verified by the site's proof:`)
|
|
448
|
+
for (const line of appLinksSiteNote(appLinks, { platform: "ios", teamId: config.ios?.teamId, bundleId: config.ios?.bundleId })) log(` ${line}`)
|
|
449
|
+
}
|
|
407
450
|
const hasEntitlements = Object.keys(mergedEntitlements).length > 0
|
|
408
451
|
if (pbx.includes("CODE_SIGN_ENTITLEMENTS") || hasEntitlements) {
|
|
409
452
|
writeFileSync(join(iosDir, "App", "App.entitlements"), entitlementsTemplate(mergedEntitlements))
|
|
@@ -425,6 +468,11 @@ const syncIos = async (root: string, config: AppConfig): Promise<SyncArtifacts>
|
|
|
425
468
|
if (config.icon) {
|
|
426
469
|
await writeIosAppIcon(iosDir, await loadIconMaster(root, config))
|
|
427
470
|
}
|
|
471
|
+
// ---- splash: the launch storyboard from app.json "splash" when set (it becomes CLI-owned with
|
|
472
|
+
// its colorset / imageset) — absent, the storyboard stays the person's ----
|
|
473
|
+
if (config.splash !== undefined) {
|
|
474
|
+
await writeIosSplash(iosDir, await loadSplashMaster(root, config))
|
|
475
|
+
}
|
|
428
476
|
|
|
429
477
|
log("")
|
|
430
478
|
success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`)
|
|
@@ -436,40 +484,55 @@ const sync = defineCommand({
|
|
|
436
484
|
name: "sync",
|
|
437
485
|
summary: "Make every existing shell match the project: bundle + assets + SDK variant + plugins + CLI-owned files",
|
|
438
486
|
usage: "[ios|android]",
|
|
439
|
-
description: "Compiles the bundle and embeds the assets it references (ios/App/Resources/ resp. the android assets/; offline by default, also for server projects — app.json \"resources\": {\"remote\": [globs]} keeps heavy assets streaming), picks the SDK variant from the bundle's engine use, vendors the app.json plugins (pinned in plugins.lock.json) and regenerates the CLI-owned shell files. User files (ios/App/*.swift, android/app/) are never touched.\n\nNative OTA JS updates at launch (both shells
|
|
487
|
+
description: "Compiles the bundle and embeds the assets it references (ios/App/Resources/ resp. the android assets/; offline by default, also for server projects — app.json \"resources\": {\"remote\": [globs]} keeps heavy assets streaming), picks the SDK variant from the bundle's engine use, vendors the app.json plugins (pinned in plugins.lock.json) and regenerates the CLI-owned shell files. User files (ios/App/*.swift, android/app/) are never touched.\n\nNative OTA JS updates at launch (both shells): the first launch of an installed build waits (≤ 5 s, the splash held) for the check and runs the newest bundle; every later launch runs the newest LOCAL bundle and a download applies on the next. On by default for a pushed project (its bundle on the platform, the launcher's own URL), app.json \"update\": {\"url\": …} for custom hosting, \"update\": false turns it off. A download runs only when it is of the shell's SDK major and not older than the bundle running; a store update keeps a downloaded bundle only when it is newer code than the one it ships. app.json \"icon\" (a square ≥1024px image) renders the app icons every sync (\"iconBackground\": the adaptive / flatten backdrop). app.json \"splash\" ({\"background\": \"#RRGGBB\", \"image\": \"./splash.png\", \"size\": 160}, all optional) renders the launch screen of both shells every sync — the colour with the image fitted centred into a size × size pt / dp square, 160 by default (iOS LaunchScreen.storyboard, Android the system splash of Theme.LeCodesRuntime, which the app's theme inherits). app.json \"urlScheme\" (\"myshop\") is the scheme the shells open on: iOS CFBundleURLTypes, Android a BROWSABLE intent-filter — a link arrives as app.launchUrl / the \"url\" event (the return of Browser.open with a returnUrl). app.json \"appLinks\" ([\"https://shop.example/pay/\"]) are the verified https links they open on — iOS universal links (associated-domains in App.entitlements), Android App Links (autoVerify intent-filters in the manifest); sync prints the apple-app-site-association / assetlinks.json each site must serve. No platform = every shell that exists; a dual-shell project compiles once.",
|
|
440
488
|
examples: ["lecodes app sync", "lecodes app sync android"],
|
|
441
489
|
run: ({ args }) => runSync(args[0]),
|
|
442
490
|
})
|
|
443
491
|
|
|
444
492
|
/* -------------------------------------- plugin -------------------------------------- */
|
|
445
493
|
|
|
446
|
-
const pluginConfig = (): { root: string, configPath: string, config: AppConfig & { plugins: Record<string,
|
|
494
|
+
const pluginConfig = (): { root: string, configPath: string, config: AppConfig & { plugins: Record<string, PluginEntry> } } => {
|
|
447
495
|
const root = projectRoot()
|
|
448
496
|
const configPath = join(root, "app.json")
|
|
449
497
|
const config = readAppConfigAt(root)
|
|
450
498
|
if (!config) throw new CliError("No app.json here — run `lecodes app init <ios|android>` first.")
|
|
451
499
|
config.plugins ??= {}
|
|
452
|
-
return { root, configPath, config: config as AppConfig & { plugins: Record<string,
|
|
500
|
+
return { root, configPath, config: config as AppConfig & { plugins: Record<string, PluginEntry> } }
|
|
453
501
|
}
|
|
454
502
|
|
|
503
|
+
/** An entry as app.json says it: one spec, or `ios: …, web: …`. */
|
|
504
|
+
const entryText = (entry: PluginEntry): string =>
|
|
505
|
+
typeof entry === "string" ? entry : Object.entries(entry).map(([p, s]) => `${p}: ${s}`).join(", ") || "*"
|
|
506
|
+
|
|
455
507
|
const pluginList = (): void => {
|
|
456
508
|
const { root, config } = pluginConfig()
|
|
457
509
|
const entries = Object.entries(config.plugins)
|
|
458
510
|
if (entries.length === 0) { log("No plugins installed."); return }
|
|
459
511
|
const lock = readJson<{ plugins?: Record<string, LockEntry> }>(join(root, "plugins.lock.json"))?.plugins ?? {}
|
|
460
|
-
for (const [id,
|
|
512
|
+
for (const [id, entry] of entries) {
|
|
461
513
|
const pin = lock[id] ? c.dim(` ${lock[id].repo} ${lock[id].tag} (${lock[id].commit.slice(0, 7)})`) : c.dim(" (not synced)")
|
|
462
|
-
log(`${c.bold(id)} ${
|
|
514
|
+
log(`${c.bold(id)} ${entryText(entry)}${pin}`)
|
|
463
515
|
}
|
|
464
516
|
}
|
|
465
517
|
|
|
466
518
|
const pluginAdd = (id: string | undefined): void => {
|
|
467
519
|
if (!id) throw new UsageError("Which plugin? `lecodes app plugin add <id | github:owner/repo[@tag] | ./plugins/<id>>`")
|
|
468
|
-
const { configPath, config } = pluginConfig()
|
|
520
|
+
const { root, configPath, config } = pluginConfig()
|
|
469
521
|
if (isLocalPluginSpec(id)) {
|
|
470
522
|
const bare = basename(id.replace(/\/+$/, ""))
|
|
471
|
-
|
|
472
|
-
|
|
523
|
+
// A half folder is named for the platforms it has a half of, under the plugin it implements:
|
|
524
|
+
// the first-party halves stay on the others (the entry's spec for them is kept).
|
|
525
|
+
const { manifest } = localPlugin(root, localPluginKey(root, id), id)
|
|
526
|
+
if (manifest.implements) {
|
|
527
|
+
const was = config.plugins[manifest.implements] ?? "*"
|
|
528
|
+
const kept: Record<string, string> = typeof was === "string" ? (was === "*" ? {} : { ios: was, android: was, web: was }) : { ...was }
|
|
529
|
+
for (const p of ["ios", "android", "web"] as const) if (manifest[p]) kept[p] = id
|
|
530
|
+
config.plugins[manifest.implements] = kept
|
|
531
|
+
log(`${c.green("added")} ${manifest.implements} → ${entryText(kept)} ${c.dim(`(a half of this project, over the first-party plugin)`)}`)
|
|
532
|
+
} else {
|
|
533
|
+
config.plugins[bare] = id
|
|
534
|
+
log(`${c.green("added")} ${bare} → ${id} ${c.dim("(a plugin of this project: its folder, as it is)")}`)
|
|
535
|
+
}
|
|
473
536
|
} else if (id.startsWith("github:")) {
|
|
474
537
|
const bare = id.slice("github:".length).split("@")[0].split("/").pop()!
|
|
475
538
|
config.plugins[bare] = id
|
|
@@ -591,7 +654,8 @@ export const runAppUpdate = async (opts: AppUpdateOptions = {}): Promise<void> =
|
|
|
591
654
|
// ---- plugins ----
|
|
592
655
|
const lockPath = join(root, "plugins.lock.json")
|
|
593
656
|
const lock = readJson<{ plugins?: Record<string, LockEntry> }>(lockPath)?.plugins ?? {}
|
|
594
|
-
|
|
657
|
+
// (one lock for the shells: the spec of the shell being updated — the first, with both)
|
|
658
|
+
const specs = pluginSpecsFor(config, doIos ? "ios" : "android")
|
|
595
659
|
if (specs.length > 0 && !opts.noPlugins) {
|
|
596
660
|
const kept: Record<string, LockEntry> = {}
|
|
597
661
|
for (const [id, spec] of specs) {
|
|
@@ -749,7 +813,7 @@ const doctorIosChecks = async (root: string, config: AppConfig, check: Check) =>
|
|
|
749
813
|
const doctor = defineCommand({
|
|
750
814
|
name: "doctor",
|
|
751
815
|
summary: "Preflight checks for building / uploading the shells",
|
|
752
|
-
description: "Toolchain (Xcode / JDK + Android SDK), the synced bundle, SDK variant + version vs app.json, plugin lock, signing (team id / release keystore), icons, OTA wiring and the App Store Connect key. Fatal checks fail the command (exit 1); soft ones print a `!` and a hint.",
|
|
816
|
+
description: "Toolchain (Xcode / JDK + Android SDK), the synced bundle, SDK variant + version vs app.json, plugin lock, signing (team id / release keystore), icons, the splash theme (Android), OTA wiring and the App Store Connect key. Fatal checks fail the command (exit 1); soft ones print a `!` and a hint.",
|
|
753
817
|
examples: ["lecodes app doctor"],
|
|
754
818
|
run: async () => {
|
|
755
819
|
const root = projectRoot()
|
|
@@ -765,6 +829,9 @@ const doctor = defineCommand({
|
|
|
765
829
|
if (config?.icon) {
|
|
766
830
|
check(existsSync(join(root, config.icon)), `Icon master ${config.icon}`, `app.json "icon" points at a missing file — sync will fail`)
|
|
767
831
|
}
|
|
832
|
+
if (typeof config?.splash?.image === "string") {
|
|
833
|
+
check(existsSync(join(root, config.splash.image)), `Splash image ${config.splash.image}`, `app.json "splash.image" points at a missing file — sync will fail`)
|
|
834
|
+
}
|
|
768
835
|
if (hasIos) await doctorIosChecks(root, config!, check)
|
|
769
836
|
if (hasAndroid) {
|
|
770
837
|
await doctorAndroidChecks(root, config!, check)
|