diff --git a/Dockerfile b/Dockerfile index 8ed8ce5..d4ac800 100644 --- a/Dockerfile +++ b/Dockerfile @@ -165,7 +165,7 @@ RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \ RUN printf '%s\n' \ 'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./public/js/context-js/context-js.js' \ 'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./resources/js/context-js.js' \ - 'b40ba5e968e70e7f6d5c9e00c03e25e1 ./resources/views/user/images.blade.php' \ + 'e0b5b669408e8a5f3dee7f332ad21e97 ./resources/views/user/images.blade.php' \ | md5sum -c - \ && grep -q "assetVersion('js/context-js/context-js.js')" ./resources/views/user/images.blade.php \ && grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \ @@ -208,7 +208,7 @@ RUN APP_SRC_MD5=$(find app config routes -type f -print0 2>/dev/null | sort -z | "fork_sha=${FORK_SHA}" \ "lsky_commit=${LSKY_COMMIT}" \ "context_js_md5=c0e513ec8e93fd81b34b3c6de5cf5eb8" \ - "images_blade_md5=b40ba5e968e70e7f6d5c9e00c03e25e1" \ + "images_blade_md5=e0b5b669408e8a5f3dee7f332ad21e97" \ "app_src_md5=${APP_SRC_MD5}" \ "app_js_md5=${APP_JS_MD5}" \ "app_css_md5=${APP_CSS_MD5}" \ diff --git a/patches/ios-longpress.patch b/patches/ios-longpress.patch index ec0f818..f2cb98b 100644 --- a/patches/ios-longpress.patch +++ b/patches/ios-longpress.patch @@ -52,7 +52,7 @@ # 三个文件各自的 md5(与 Dockerfile「自证 2」里的期望值一致): # c0e513ec8e93fd81b34b3c6de5cf5eb8 src/resources/js/context-js.js # c0e513ec8e93fd81b34b3c6de5cf5eb8 src/public/js/context-js/context-js.js -# b40ba5e968e70e7f6d5c9e00c03e25e1 src/resources/views/user/images.blade.php +# e0b5b669408e8a5f3dee7f332ad21e97 src/resources/views/user/images.blade.php # # 上游原文件 md5(改动前,供比对): # c8e57f6232848ca8277341ddf1a3a7a6 resources/js/context-js.js diff --git a/src/resources/views/user/images.blade.php b/src/resources/views/user/images.blade.php index 6b70233..afeddba 100644 --- a/src/resources/views/user/images.blade.php +++ b/src/resources/views/user/images.blade.php @@ -535,6 +535,42 @@ class="relative inline-flex h-4 w-7 shrink-0 items-center rounded-full bg-brand }; const viewer = new Viewer(document.getElementById('images-grid'), {url: 'data-original'}); + /* 修 Viewer.js 的「闩锁」bug(真机打点 + 源码逐行核对 + jsdom 复现确认): + * others.js:216-223 change() 里 + * case ACTION_SWITCH: { + * this.action = 'switched'; // ← 第一次 pointermove 就先上闩 + * if (|offsetX| > 1 && |offsetX| > |offsetY|) { … this.view(index) } + * 上闩之后 action 成了字符串 'switched',而外层 switch 没有这个分支 ⇒ 之后 + * 每一次 pointermove 全部落空:不切图、不平移、不放大 =「纹丝不动」。 + * 判据又只看开头那次采样,真手指刚落下天然带纵向抖动(dy ≥ dx)⇒ 大多数手势 + * 一开始就被闩死,偶尔开头偏横向才成功 —— 这就是「大多数失败、少数成功、无规律」。 + * 从外面把闩松开:手势期间发现 action 变成那个闩值,就恢复成刚才那个动作值, + * 后续采样继续参与判定(切图判据看的是"最近一次位移",所以横滑一定能切到)。 + * 不改 viewer.min.js(压缩产物,改了没法维护)。 */ + (function () { + let inViewer = false; + let lastAction = null; + let switchAction = null; + const isViewer = (el) => el instanceof Element && !!el.closest('.viewer-container'); + document.addEventListener('pointerdown', (e) => { + inViewer = isViewer(e.target); + lastAction = null; + }, true); + document.addEventListener('pointermove', (e) => { + if (!inViewer) return; + const a = viewer.action; + if (a === 'switched') { + // 学到切图动作的真值后,把闩松开 + if (!switchAction && lastAction) switchAction = lastAction; + if (switchAction && viewer.action !== switchAction) viewer.action = switchAction; + } else if (a) { + lastAction = a; // 手势开始时 Viewer 刚算出来的动作值 + } + }, true); + document.addEventListener('pointerup', () => { inViewer = false; }, true); + document.addEventListener('pointercancel', () => { inViewer = false; }, true); + })(); + /* 安卓 Chromium:看图控件自己有"模拟双击"逻辑(viewerjs handlers.js:424-451)—— * 两次落在**图片上**的抬手间隔 <500ms,它就 setTimeout 后自己派发一个 dblclick, * 于是放大。真机打点证实:老师连续滑图时凭空出现 dblclick×23 ⇒ 一放大,图片就不再 diff --git a/test/repro-swipe-latch.cjs b/test/repro-swipe-latch.cjs new file mode 100644 index 0000000..1320bb6 --- /dev/null +++ b/test/repro-swipe-latch.cjs @@ -0,0 +1,43 @@ +const {JSDOM}=require('jsdom'); +const fs=require('fs'); +function run(file, moves, label){ + const dom=new JSDOM('',{runScripts:'outside-only',pretendToBeVisual:true}); + const w=dom.window; + w.ontouchstart=null; + if(!w.PointerEvent){ w.PointerEvent=class extends w.MouseEvent{constructor(t,i={}){super(t,i);this.pointerId=i.pointerId||1;this.pointerType=i.pointerType||'touch';}}; } + w.eval(fs.readFileSync(file,'utf8')); + const v=new w.Viewer(w.document.getElementById('g'),{transition:false}); + v.show(); + // fake a loaded, fitted image + v.viewed=true; v.viewing=false; v.showing=false; + v.imageData={x:0,y:0,width:300,height:200}; v.viewerData={width:360,height:640}; + const idx0=v.index; + const ev=(t,x,y)=>new w.PointerEvent(t,{bubbles:true,cancelable:true,pointerId:7,pointerType:'touch',clientX:x,clientY:y,view:w}); + const mk=(t,x,y)=>{const e=ev(t,x,y);Object.defineProperty(e,'pageX',{value:x});Object.defineProperty(e,'pageY',{value:y});return e;}; + const canvas=v.canvas; + // ==== 阿罗娜的"松闩"补丁(从外面松开 Viewer 的 'switched' 闩锁)==== + let inV=false,lastA=null,swA=null; + w.document.addEventListener('pointerdown',(e)=>{ inV=!!e.target.closest('.viewer-container'); lastA=null; },true); + w.document.addEventListener('pointermove',()=>{ + if(!inV) return; + const a=v.action; + if(a==='switched'){ if(!swA&&lastA) swA=lastA; if(swA&&v.action!==swA) v.action=swA; } + else if(a){ lastA=a; } + },true); + w.document.addEventListener('pointerup',()=>{ inV=false; },true); + // ==== 补丁结束 ==== + canvas.dispatchEvent(mk('pointerdown',300,400)); + for(const [x,y] of moves) w.document.dispatchEvent(mk('pointermove',x,y)); + w.document.dispatchEvent(mk('pointerup',moves[moves.length-1][0],moves[moves.length-1][1])); + console.log(label.padEnd(58),'index',idx0,'->',v.index, v.index!==idx0?'SWITCHED':'no switch'); +} +const f=process.argv[2]; +run(f,[[250,402],[200,405],[150,410]],'first move big & horizontal'); +run(f,[[300,401],[250,402],[200,405],[150,410]],'first move dx=0,dy=1 (tiny, vertical noise)'); +run(f,[[299,402],[250,402],[200,405],[150,410]],'first move dx=-1,dy=2'); +run(f,[[298,403],[250,402],[200,405],[150,410]],'first move dx=-2,dy=3 (dy>dx)'); +run(f,[[296,401],[250,402],[200,405],[150,410]],'first move dx=-4,dy=1 (ok)'); +run(f,[[300,401],[300,402],[299,402],[250,402],[150,410]],'jitter 3 samples then swipe'); +run(f,[[300,420],[300,450],[301,500]],'vertical drag (should NOT switch)'); +run(f,[[299,405],[298,412],[250,415],[150,418]],'starts diagonal dy>dx, then horizontal'); +run(f,[[330,401],[360,403],[400,405]],'swipe right (should go prev -> stays 0 at index 0 / no loop)');