|
1470 | 1470 | // ================================================================ |
1471 | 1471 |
|
1472 | 1472 | const MULTI_WINDOW_RETURN_BALL_DRAG_SHRINK_SIZE = 160; |
| 1473 | + const RETURN_BALL_DRAG_RECOVERY_POLL_MS = 250; |
| 1474 | + const RETURN_BALL_DRAG_STALE_RECOVERY_MS = 12000; |
1473 | 1475 | const MULTI_WINDOW_RETURN_BALL_DRAG_SHRINK_FALLBACK_MS = 220; |
1474 | 1476 | const MULTI_WINDOW_RETURN_BALL_DRAG_RESTORE_FALLBACK_MS = 600; |
1475 | 1477 | const MULTI_WINDOW_RETURN_BALL_REVEAL_FALLBACK_MS = 600; |
|
2263 | 2265 | } |
2264 | 2266 | } |
2265 | 2267 |
|
| 2268 | + function clearReturnBallDragRecoveryTimer(state) { |
| 2269 | + if (!state || !state.dragRecoveryTimer) return; |
| 2270 | + clearTimeout(state.dragRecoveryTimer); |
| 2271 | + state.dragRecoveryTimer = null; |
| 2272 | + } |
| 2273 | + |
| 2274 | + function getReturnBallDragScreenCoordinate(value, fallback) { |
| 2275 | + return Number.isFinite(value) ? value : fallback; |
| 2276 | + } |
| 2277 | + |
2266 | 2278 | function isNativeReturnBallDragDisabled() { |
2267 | 2279 | const runtime = window.__NEKO_DESKTOP_RUNTIME__ || {}; |
2268 | 2280 | return !!( |
|
2275 | 2287 | const state = multiWindowReturnBallDragState; |
2276 | 2288 | if (!state) return; |
2277 | 2289 |
|
| 2290 | + const shouldStopNativeDrag = state.isDragging; |
| 2291 | + const stopScreenX = getReturnBallDragScreenCoordinate(state.releaseScreenX, state.startScreenX); |
| 2292 | + const stopScreenY = getReturnBallDragScreenCoordinate(state.releaseScreenY, state.startScreenY); |
| 2293 | + |
2278 | 2294 | state.dragSessionToken += 1; |
| 2295 | + state.isDragging = false; |
| 2296 | + clearReturnBallDragRecoveryTimer(state); |
2279 | 2297 | clearMultiWindowReturnBallDeferredWork(state); |
2280 | 2298 | if (state.container) { |
2281 | 2299 | state.container.removeEventListener('mousedown', state.handleMouseDown, true); |
2282 | 2300 | state.container.removeEventListener('touchstart', state.handleTouchStart, true); |
2283 | 2301 | } |
2284 | 2302 | document.removeEventListener('mousemove', state.handleMouseMove); |
2285 | 2303 | document.removeEventListener('mouseup', state.handleMouseUp); |
| 2304 | + document.removeEventListener('pointermove', state.handlePointerMove, true); |
| 2305 | + document.removeEventListener('pointerup', state.handlePointerUp, true); |
| 2306 | + document.removeEventListener('pointercancel', state.handlePointerCancel, true); |
2286 | 2307 | document.removeEventListener('touchmove', state.handleTouchMove); |
2287 | 2308 | document.removeEventListener('touchend', state.handleTouchEnd); |
2288 | 2309 | document.removeEventListener('touchcancel', state.handleTouchEnd); |
| 2310 | + window.removeEventListener('blur', state.handleWindowBlur); |
| 2311 | + window.removeEventListener('pagehide', state.handlePageHide); |
| 2312 | + document.removeEventListener('visibilitychange', state.handleVisibilityChange); |
2289 | 2313 |
|
2290 | 2314 | if (state.container) { |
2291 | 2315 | restoreSavedReturnBallStyle(state.container, state); |
|
2294 | 2318 | } |
2295 | 2319 | delete document.body.dataset.nekoBallDrag; |
2296 | 2320 | multiWindowReturnBallDragState = null; |
| 2321 | + |
| 2322 | + if (shouldStopNativeDrag && window.nekoPetDrag && typeof window.nekoPetDrag.stop === 'function') { |
| 2323 | + Promise.resolve() |
| 2324 | + .then(() => window.nekoPetDrag.stop(stopScreenX, stopScreenY)) |
| 2325 | + .finally(() => { |
| 2326 | + if (window.nekoPetDrag && typeof window.nekoPetDrag.reveal === 'function') { |
| 2327 | + return window.nekoPetDrag.reveal(); |
| 2328 | + } |
| 2329 | + return null; |
| 2330 | + }) |
| 2331 | + .catch(() => {}); |
| 2332 | + } |
2297 | 2333 | } |
2298 | 2334 |
|
2299 | 2335 | function ensureMultiWindowReturnBallDrag(container) { |
|
2328 | 2364 | viewportWaitFallbackTimer: null, |
2329 | 2365 | transitionCleanupTimer: null, |
2330 | 2366 | dragSessionToken: 0, |
| 2367 | + dragRecoveryTimer: null, |
| 2368 | + lastPointerEventAt: 0, |
2331 | 2369 | handleMouseDown: null, |
2332 | 2370 | handleMouseMove: null, |
2333 | 2371 | handleMouseUp: null, |
| 2372 | + handlePointerMove: null, |
| 2373 | + handlePointerUp: null, |
| 2374 | + handlePointerCancel: null, |
2334 | 2375 | handleTouchStart: null, |
2335 | 2376 | handleTouchMove: null, |
2336 | 2377 | handleTouchEnd: null, |
| 2378 | + handleWindowBlur: null, |
| 2379 | + handlePageHide: null, |
| 2380 | + handleVisibilityChange: null, |
2337 | 2381 | }; |
2338 | 2382 |
|
2339 | 2383 | function getTouchScreenPoint(touch) { |
|
2453 | 2497 | dispatchClickEvent(); |
2454 | 2498 | } |
2455 | 2499 |
|
| 2500 | + function markDragPointerActivity() { |
| 2501 | + state.lastPointerEventAt = Date.now(); |
| 2502 | + } |
| 2503 | + |
| 2504 | + function cancelActiveDrag(reason) { |
| 2505 | + if (!state.isDragging) return; |
| 2506 | + const screenX = getReturnBallDragScreenCoordinate(state.releaseScreenX, state.startScreenX); |
| 2507 | + const screenY = getReturnBallDragScreenCoordinate(state.releaseScreenY, state.startScreenY); |
| 2508 | + void finishDrag(screenX, screenY, { |
| 2509 | + reason: reason || 'return-ball-drag-cancel', |
| 2510 | + suppressClick: true |
| 2511 | + }); |
| 2512 | + } |
| 2513 | + |
| 2514 | + function scheduleReturnBallDragRecoveryCheck() { |
| 2515 | + clearReturnBallDragRecoveryTimer(state); |
| 2516 | + if (!state.isDragging) return; |
| 2517 | + state.dragRecoveryTimer = setTimeout(() => { |
| 2518 | + state.dragRecoveryTimer = null; |
| 2519 | + if (!state.isDragging) return; |
| 2520 | + if (document.hidden) { |
| 2521 | + cancelActiveDrag('document-hidden'); |
| 2522 | + return; |
| 2523 | + } |
| 2524 | + if (Date.now() - state.lastPointerEventAt > RETURN_BALL_DRAG_STALE_RECOVERY_MS) { |
| 2525 | + cancelActiveDrag('stale-pointer-timeout'); |
| 2526 | + return; |
| 2527 | + } |
| 2528 | + scheduleReturnBallDragRecoveryCheck(); |
| 2529 | + }, RETURN_BALL_DRAG_RECOVERY_POLL_MS); |
| 2530 | + } |
| 2531 | + |
| 2532 | + function finishDragIfMouseButtonReleased(event, reason) { |
| 2533 | + if (!state.isDragging || !event || (event.pointerType && event.pointerType !== 'mouse')) { |
| 2534 | + return false; |
| 2535 | + } |
| 2536 | + if (!Number.isFinite(event.buttons) || event.buttons !== 0) { |
| 2537 | + return false; |
| 2538 | + } |
| 2539 | + void finishDrag(event.screenX, event.screenY, { |
| 2540 | + reason: reason || 'buttons-released' |
| 2541 | + }); |
| 2542 | + return true; |
| 2543 | + } |
| 2544 | + |
2456 | 2545 | function isViewportRestored(expectedWidth, expectedHeight) { |
2457 | 2546 | if (!Number.isFinite(expectedWidth) || !Number.isFinite(expectedHeight)) { |
2458 | 2547 | return true; |
|
2600 | 2689 | state.releaseScreenY = screenY; |
2601 | 2690 | state.savedWindowW = window.innerWidth; |
2602 | 2691 | state.savedWindowH = window.innerHeight; |
| 2692 | + markDragPointerActivity(); |
2603 | 2693 |
|
2604 | 2694 | const rect = container.getBoundingClientRect(); |
2605 | 2695 | state.savedBallWidth = Math.round(rect.width) || 64; |
|
2664 | 2754 | continueOnFallback: true |
2665 | 2755 | } |
2666 | 2756 | ); |
| 2757 | + scheduleReturnBallDragRecoveryCheck(); |
2667 | 2758 |
|
2668 | 2759 | if (event) { |
2669 | 2760 | event.preventDefault(); |
|
2673 | 2764 |
|
2674 | 2765 | function updateDrag(screenX, screenY) { |
2675 | 2766 | if (!state.isDragging) return; |
| 2767 | + markDragPointerActivity(); |
2676 | 2768 | state.releaseScreenX = screenX; |
2677 | 2769 | state.releaseScreenY = screenY; |
2678 | 2770 |
|
|
2705 | 2797 | async function finishDrag(screenX, screenY) { |
2706 | 2798 | if (!state.isDragging) return; |
2707 | 2799 |
|
| 2800 | + const options = arguments[2] && typeof arguments[2] === 'object' ? arguments[2] : {}; |
| 2801 | + const suppressClick = options.suppressClick === true; |
2708 | 2802 | state.isDragging = false; |
2709 | 2803 | state.releaseScreenX = screenX; |
2710 | 2804 | state.releaseScreenY = screenY; |
2711 | 2805 | const dragToken = state.dragSessionToken; |
| 2806 | + clearReturnBallDragRecoveryTimer(state); |
2712 | 2807 | clearMultiWindowReturnBallDeferredWork(state); |
2713 | 2808 |
|
2714 | 2809 | // 先瞬间隐藏球,防止恢复 UI 时球在 (8,8) 闪烁 |
|
2732 | 2827 | restoreSavedBallStyle(); |
2733 | 2828 | delete document.body.dataset.nekoBallDrag; |
2734 | 2829 | container.setAttribute('data-dragging', 'false'); |
2735 | | - scheduleIdleReturnBallDesktopBridge('return-ball-drag-click', container); |
| 2830 | + scheduleIdleReturnBallDesktopBridge( |
| 2831 | + suppressClick ? 'return-ball-drag-cancel' : 'return-ball-drag-click', |
| 2832 | + container |
| 2833 | + ); |
2736 | 2834 | revealReturnBallDragWindow(); |
2737 | | - dispatchReturnBallClick(); |
| 2835 | + if (suppressClick) { |
| 2836 | + window.dispatchEvent(new CustomEvent('neko:return-ball-manual-move', { |
| 2837 | + detail: { |
| 2838 | + reason: 'return-ball-drag-end', |
| 2839 | + container: container, |
| 2840 | + movedDistancePx: 0, |
| 2841 | + dragCancelled: true |
| 2842 | + } |
| 2843 | + })); |
| 2844 | + } else { |
| 2845 | + dispatchReturnBallClick(); |
| 2846 | + } |
2738 | 2847 | }, { |
2739 | 2848 | fallbackMs: MULTI_WINDOW_RETURN_BALL_DRAG_RESTORE_FALLBACK_MS, |
2740 | 2849 | continueOnFallback: true |
|
2778 | 2887 | detail: { |
2779 | 2888 | reason: 'return-ball-drag-end', |
2780 | 2889 | container: container, |
2781 | | - movedDistancePx: movedDistancePx |
| 2890 | + movedDistancePx: movedDistancePx, |
| 2891 | + dragCancelled: suppressClick |
2782 | 2892 | } |
2783 | 2893 | })); |
2784 | 2894 | revealReturnBallDragWindow(); |
|
2796 | 2906 | detail: { |
2797 | 2907 | reason: 'return-ball-drag-end', |
2798 | 2908 | container: container, |
2799 | | - movedDistancePx: movedDistancePx |
| 2909 | + movedDistancePx: movedDistancePx, |
| 2910 | + dragCancelled: suppressClick |
2800 | 2911 | } |
2801 | 2912 | })); |
2802 | 2913 | revealReturnBallDragWindow(); |
|
2822 | 2933 | beginDrag(event.screenX, event.screenY, event); |
2823 | 2934 | }; |
2824 | 2935 | state.handleMouseMove = (event) => { |
| 2936 | + if (finishDragIfMouseButtonReleased(event, 'mousemove-buttons-released')) return; |
2825 | 2937 | updateDrag(event.screenX, event.screenY); |
2826 | 2938 | }; |
2827 | 2939 | state.handleMouseUp = (event) => { |
2828 | 2940 | void finishDrag(event.screenX, event.screenY); |
2829 | 2941 | }; |
| 2942 | + state.handlePointerMove = (event) => { |
| 2943 | + if (finishDragIfMouseButtonReleased(event, 'pointermove-buttons-released')) return; |
| 2944 | + if (event && event.pointerType === 'mouse') { |
| 2945 | + updateDrag(event.screenX, event.screenY); |
| 2946 | + } |
| 2947 | + }; |
| 2948 | + state.handlePointerUp = (event) => { |
| 2949 | + void finishDrag(event.screenX, event.screenY); |
| 2950 | + }; |
| 2951 | + state.handlePointerCancel = () => { |
| 2952 | + cancelActiveDrag('pointercancel'); |
| 2953 | + }; |
2830 | 2954 | state.handleTouchStart = (event) => { |
2831 | 2955 | const point = getTouchScreenPoint(event.touches[0]); |
2832 | 2956 | if (!point) return; |
|
2846 | 2970 | point ? point.y : state.releaseScreenY |
2847 | 2971 | ); |
2848 | 2972 | }; |
| 2973 | + state.handleWindowBlur = () => { |
| 2974 | + cancelActiveDrag('window-blur'); |
| 2975 | + }; |
| 2976 | + state.handlePageHide = () => { |
| 2977 | + cancelActiveDrag('pagehide'); |
| 2978 | + }; |
| 2979 | + state.handleVisibilityChange = () => { |
| 2980 | + if (document.hidden) { |
| 2981 | + cancelActiveDrag('visibility-hidden'); |
| 2982 | + } |
| 2983 | + }; |
2849 | 2984 |
|
2850 | 2985 | container.addEventListener('mousedown', state.handleMouseDown, true); |
2851 | 2986 | container.addEventListener('touchstart', state.handleTouchStart, true); |
2852 | 2987 | document.addEventListener('mousemove', state.handleMouseMove); |
2853 | 2988 | document.addEventListener('mouseup', state.handleMouseUp); |
| 2989 | + document.addEventListener('pointermove', state.handlePointerMove, true); |
| 2990 | + document.addEventListener('pointerup', state.handlePointerUp, true); |
| 2991 | + document.addEventListener('pointercancel', state.handlePointerCancel, true); |
2854 | 2992 | document.addEventListener('touchmove', state.handleTouchMove, { passive: false }); |
2855 | 2993 | document.addEventListener('touchend', state.handleTouchEnd); |
2856 | 2994 | document.addEventListener('touchcancel', state.handleTouchEnd); |
| 2995 | + window.addEventListener('blur', state.handleWindowBlur); |
| 2996 | + window.addEventListener('pagehide', state.handlePageHide); |
| 2997 | + document.addEventListener('visibilitychange', state.handleVisibilityChange); |
2857 | 2998 |
|
2858 | 2999 | multiWindowReturnBallDragState = state; |
2859 | 3000 | } |
|
0 commit comments