@livx.cc/appwrap 0.61.25 → 0.61.26
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/package.json
CHANGED
|
@@ -35,6 +35,11 @@ const KVO_KEYS = ['URL', 'title', 'canGoBack', 'canGoForward', 'loading'];
|
|
|
35
35
|
* SF Symbol (iOS, default 'sparkles'), `text` its glyph on Android ('✦'),
|
|
36
36
|
* `label` = accessibility, `radius` (default round), symbol/text '' = a
|
|
37
37
|
* plain shape. show:false hides it. Taps emit `webview.fab`.
|
|
38
|
+
* webview.float {on, hit?: [{x,y,w,h}]} — the app's WebView draws OVER the overlay (transparent wherever its own page
|
|
39
|
+
* is), e.g. chat bubbles floating over a shown page without resizing it.
|
|
40
|
+
* Touches inside `hit` (app CSS px) go to the app; everywhere else to the
|
|
41
|
+
* overlay. Call again to move the rects; on:false restores. iOS; Android
|
|
42
|
+
* resolves {floating:false} (not supported — keep a fallback).
|
|
38
43
|
* Events: `webview.state` {url,title,canGoBack,canGoForward,loading} on every navigation change; `webview.fab` on a tap.
|
|
39
44
|
*
|
|
40
45
|
* Swipe back/forward on, target=_blank/window.open loads in place (shared ios-ui-delegate), keyboard +
|
|
@@ -66,6 +71,9 @@ const fabOpts = (p?: Fab | null) => ({
|
|
|
66
71
|
symbol: p?.symbol === '' ? '' : String(p?.symbol || 'sparkles'), text: p?.text === '' ? '' : String(p?.text || '✦'),
|
|
67
72
|
radius: p?.radius === undefined ? null : Math.max(0, Number(p.radius) || 0), label: String(p?.label || 'Assistant'),
|
|
68
73
|
});
|
|
74
|
+
/** webview.float hit rects (app CSS px); junk entries dropped. */
|
|
75
|
+
const floatRects = (l: unknown) => (Array.isArray(l) ? l : []).map((r: any) => ({ x: +r?.x, y: +r?.y, w: +r?.w, h: +r?.h }))
|
|
76
|
+
.filter((r) => [r.x, r.y, r.w, r.h].every(Number.isFinite) && r.w > 0 && r.h > 0);
|
|
69
77
|
const parsed = (s: unknown) => { try { return JSON.parse(String(s)); } catch { return s ?? null; } };
|
|
70
78
|
const EVAL_TIMEOUT = 30000;
|
|
71
79
|
|
|
@@ -75,6 +83,9 @@ const snapOpts = (p?: { width?: number; quality?: number } | null) => ({
|
|
|
75
83
|
quality: Math.min(1, Math.max(0.1, Number(p?.quality) || 0.6)),
|
|
76
84
|
});
|
|
77
85
|
|
|
86
|
+
/** webview.float is on: the app WebView must stay see-through (keyboard.ts's backdrop paint skips it). */
|
|
87
|
+
export let appFloating = false;
|
|
88
|
+
|
|
78
89
|
export function registerWebViewHandlers(): void {
|
|
79
90
|
if (isIOS) registerIos();
|
|
80
91
|
else if (isAndroid) registerAndroid();
|
|
@@ -94,6 +105,17 @@ function registerIos(): void {
|
|
|
94
105
|
let fab: UIButton | null = null;
|
|
95
106
|
let fabTarget: any = null, FabTargetClass: any; // its tap target (an exposedMethods NSObject, built once)
|
|
96
107
|
let fabBottom: NSLayoutConstraint | null = null, fabRight: NSLayoutConstraint | null = null, fabW: NSLayoutConstraint | null = null, fabH: NSLayoutConstraint | null = null;
|
|
108
|
+
// webview.float: the app WebView renders above the overlay (layer zPosition — draw order only) while the overlay stays
|
|
109
|
+
// first in hit-testing (subview order): it sits in a pass-through wrapper that declines the touches inside `floatHit`
|
|
110
|
+
// (app CSS px), which fall to the app. (A wrapper, not a WKWebView subclass: its super hitTest recursed in NativeScript.)
|
|
111
|
+
let floating = false, floatHit: { x: number; y: number; w: number; h: number }[] = [], hostBg: UIColor | null = null, hostOpaque = true;
|
|
112
|
+
let wrap: UIView | null = null, WrapClass: any;
|
|
113
|
+
const appHit = (view: UIView, pt: CGPoint) => {
|
|
114
|
+
const host = bridge.getWebView()?.ios as UIView | undefined;
|
|
115
|
+
if (!floating || !host) return false;
|
|
116
|
+
const p = view.convertPointToView(pt, host);
|
|
117
|
+
return floatHit.some((r) => p.x >= r.x && p.x <= r.x + r.w && p.y >= r.y && p.y <= r.y + r.h);
|
|
118
|
+
};
|
|
97
119
|
/** The button stays above the overlay (which comes to the front on open/show). */
|
|
98
120
|
const fabFront = () => { if (fab && !fab.hidden) fab.superview?.bringSubviewToFront(fab); };
|
|
99
121
|
|
|
@@ -134,7 +156,16 @@ function registerIos(): void {
|
|
|
134
156
|
if ((view as any).inspectable !== undefined) (view as any).inspectable = true; // iOS 16.4+ Safari inspector
|
|
135
157
|
|
|
136
158
|
view.translatesAutoresizingMaskIntoConstraints = false;
|
|
137
|
-
|
|
159
|
+
WrapClass ??= (UIView as any).extend({
|
|
160
|
+
hitTestWithEvent(pt: CGPoint, ev: _UIEvent) {
|
|
161
|
+
if (!wv || appHit(this, pt)) return null;
|
|
162
|
+
return wv.hitTestWithEvent(this.convertPointToView(pt, wv), ev);
|
|
163
|
+
},
|
|
164
|
+
}, { name: 'AppwrapOverlayWrap' });
|
|
165
|
+
wrap = WrapClass.alloc().initWithFrame(CGRectZero);
|
|
166
|
+
wrap!.translatesAutoresizingMaskIntoConstraints = false;
|
|
167
|
+
container.addSubview(wrap!);
|
|
168
|
+
wrap!.addSubview(view);
|
|
138
169
|
// Anchor to the app WebView's box so `top`/`bottom` are in the page's own CSS px (== points).
|
|
139
170
|
const ref: any = host ?? container;
|
|
140
171
|
topConstraint = view.topAnchor.constraintEqualToAnchorConstant(ref.topAnchor, top);
|
|
@@ -144,6 +175,8 @@ function registerIos(): void {
|
|
|
144
175
|
bottomConstraint,
|
|
145
176
|
view.leadingAnchor.constraintEqualToAnchor(ref.leadingAnchor),
|
|
146
177
|
view.trailingAnchor.constraintEqualToAnchor(ref.trailingAnchor),
|
|
178
|
+
wrap!.topAnchor.constraintEqualToAnchor(ref.topAnchor), wrap!.bottomAnchor.constraintEqualToAnchor(ref.bottomAnchor),
|
|
179
|
+
wrap!.leadingAnchor.constraintEqualToAnchor(ref.leadingAnchor), wrap!.trailingAnchor.constraintEqualToAnchor(ref.trailingAnchor),
|
|
147
180
|
] as any);
|
|
148
181
|
|
|
149
182
|
ObserverClass ??= (NSObject as any).extend(
|
|
@@ -166,7 +199,7 @@ function registerIos(): void {
|
|
|
166
199
|
if (!wv) return;
|
|
167
200
|
for (const k of KVO_KEYS) { try { wv.removeObserverForKeyPath(observer!, k); } catch { /* not observed */ } }
|
|
168
201
|
wv.stopLoading();
|
|
169
|
-
wv.removeFromSuperview();
|
|
202
|
+
wv.removeFromSuperview(); wrap?.removeFromSuperview(); wrap = null;
|
|
170
203
|
wv = null; observer = null; uiDelegate = null; topConstraint = null; bottomConstraint = null;
|
|
171
204
|
bridge.emit('webview.closed', {});
|
|
172
205
|
};
|
|
@@ -190,7 +223,7 @@ function registerIos(): void {
|
|
|
190
223
|
if (!wv) create(inset(top), inset(bottom));
|
|
191
224
|
else setInsets(top, bottom);
|
|
192
225
|
wv!.hidden = false;
|
|
193
|
-
|
|
226
|
+
wrap?.superview?.bringSubviewToFront(wrap);
|
|
194
227
|
fabFront();
|
|
195
228
|
load(target);
|
|
196
229
|
return state();
|
|
@@ -211,7 +244,7 @@ function registerIos(): void {
|
|
|
211
244
|
|
|
212
245
|
bridge.register('webview.hide', () => onMain(() => { if (wv) wv.hidden = true; return { open: !!wv }; }));
|
|
213
246
|
bridge.register('webview.show', (p?: { top?: number; bottom?: number } | null) => onMain(() => {
|
|
214
|
-
if (wv) { setInsets(p?.top, p?.bottom); wv.hidden = false;
|
|
247
|
+
if (wv) { setInsets(p?.top, p?.bottom); wv.hidden = false; wrap?.superview?.bringSubviewToFront(wrap); fabFront(); }
|
|
215
248
|
return { open: !!wv };
|
|
216
249
|
}));
|
|
217
250
|
bridge.register('webview.close', () => onMain(() => { destroy(); }));
|
|
@@ -256,6 +289,21 @@ function registerIos(): void {
|
|
|
256
289
|
});
|
|
257
290
|
});
|
|
258
291
|
|
|
292
|
+
bridge.register('webview.float', (p?: { on?: boolean; hit?: unknown } | null) => onMain(() => {
|
|
293
|
+
const host = bridge.getWebView()?.ios as WKWebView | undefined;
|
|
294
|
+
if (!host) throw err('NATIVE_ERROR', 'webview.float: no host view');
|
|
295
|
+
floatHit = floatRects(p?.hit);
|
|
296
|
+
const on = !!p?.on;
|
|
297
|
+
if (on !== floating) {
|
|
298
|
+
floating = appFloating = on;
|
|
299
|
+
if (on) { hostBg = host.backgroundColor; hostOpaque = host.opaque; host.opaque = false; host.backgroundColor = UIColor.clearColor; host.scrollView.backgroundColor = UIColor.clearColor; }
|
|
300
|
+
else { host.opaque = hostOpaque; host.backgroundColor = hostBg; host.scrollView.backgroundColor = hostBg; }
|
|
301
|
+
host.layer.zPosition = on ? 1 : 0;
|
|
302
|
+
if (fab) fab.layer.zPosition = on ? 2 : 0;
|
|
303
|
+
}
|
|
304
|
+
return { floating };
|
|
305
|
+
}));
|
|
306
|
+
|
|
259
307
|
bridge.register('webview.snapshot', (p?: { width?: number; quality?: number } | null) => {
|
|
260
308
|
const o = snapOpts(p);
|
|
261
309
|
return onMain(() => new Promise<{ jpeg: string; width: number; height: number }>((resolve, reject) => {
|
|
@@ -537,6 +585,8 @@ function registerAndroid(): void {
|
|
|
537
585
|
})();
|
|
538
586
|
});
|
|
539
587
|
|
|
588
|
+
bridge.register('webview.float', () => ({ floating: false })); // (not supported here: callers keep their fallback)
|
|
589
|
+
|
|
540
590
|
bridge.register('webview.cookies', (p: { cookies?: Cookie[] } | null) => {
|
|
541
591
|
const list = cookieList(p);
|
|
542
592
|
return onMain(() => new Promise<{ set: number }>((resolve) => {
|
|
@@ -3,6 +3,7 @@ import { bridge } from './bridge';
|
|
|
3
3
|
import { SHELL_CONFIG } from './config';
|
|
4
4
|
import { createKeyboardShowCycle, type KeyboardEventTag } from './keyboard-frame';
|
|
5
5
|
import { appwrapNativeLog } from './native-log';
|
|
6
|
+
import { appFloating } from './handlers-webview';
|
|
6
7
|
|
|
7
8
|
declare const android: any;
|
|
8
9
|
// iOS keyboard-notification globals (marshalled from UIKit at runtime).
|
|
@@ -231,7 +232,8 @@ function syncBackdropColor(): void {
|
|
|
231
232
|
const color = UIColor.colorWithRedGreenBlueAlpha(+m[1] / 255, +m[2] / 255, +m[3] / 255, 1);
|
|
232
233
|
// The keyboard's rounded corners (iOS 26) show whatever native view is behind the webview — paint
|
|
233
234
|
// the whole chain up to the window, not only the webview + window (a dark host view showed through).
|
|
234
|
-
|
|
235
|
+
// (floating over a page — webview.float — the webview itself stays clear, or it covers the page)
|
|
236
|
+
for (let v: UIView | null = appFloating ? w.superview : w; v; v = v.superview) v.backgroundColor = color;
|
|
235
237
|
if (w.window) w.window.backgroundColor = color;
|
|
236
238
|
if (SHELL_CONFIG.debug) appwrapNativeLog(`[native:keyboard] backdrop ← rgb(${m[1]},${m[2]},${m[3]})`);
|
|
237
239
|
});
|