@livx.cc/appwrap 0.61.24 → 0.61.25
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
|
@@ -28,7 +28,14 @@ const KVO_KEYS = ['URL', 'title', 'canGoBack', 'canGoForward', 'loading'];
|
|
|
28
28
|
* overlay's page; resolves {result} (the returned value, JSON round-tripped).
|
|
29
29
|
* `world` = a named isolated JS world (iOS; Android has one world). The
|
|
30
30
|
* hub's own (trusted) origin only — never in bridge-origin's FOREIGN_ALLOWED.
|
|
31
|
-
*
|
|
31
|
+
* webview.fab {show, bottom?, right?, size?, color?, symbol?, text?, label?}
|
|
32
|
+
* — a round floating button over the app AND the overlay (an "assistant"
|
|
33
|
+
* button that stays tappable while a page is shown): `bottom`/`right` = CSS px
|
|
34
|
+
* from the app WebView's bottom-right, `size` (52), `color` #rrggbb, `symbol` an
|
|
35
|
+
* SF Symbol (iOS, default 'sparkles'), `text` its glyph on Android ('✦'),
|
|
36
|
+
* `label` = accessibility, `radius` (default round), symbol/text '' = a
|
|
37
|
+
* plain shape. show:false hides it. Taps emit `webview.fab`.
|
|
38
|
+
* Events: `webview.state` {url,title,canGoBack,canGoForward,loading} on every navigation change; `webview.fab` on a tap.
|
|
32
39
|
*
|
|
33
40
|
* Swipe back/forward on, target=_blank/window.open loads in place (shared ios-ui-delegate), keyboard +
|
|
34
41
|
* safe areas handled by WKWebView's own scroll-view insets (contentInsetAdjustment automatic).
|
|
@@ -51,6 +58,14 @@ const evalJs = (p?: { js?: string } | null) => {
|
|
|
51
58
|
if (!js) throw err('NATIVE_ERROR', 'webview.eval: empty js');
|
|
52
59
|
return js;
|
|
53
60
|
};
|
|
61
|
+
/** webview.fab params (sizes in CSS px). */
|
|
62
|
+
type Fab = { show?: boolean; bottom?: number; right?: number; size?: number; radius?: number; color?: string; symbol?: string; text?: string; label?: string };
|
|
63
|
+
const fabOpts = (p?: Fab | null) => ({
|
|
64
|
+
show: !!p?.show, bottom: Math.max(0, Number(p?.bottom) || 16), right: Math.max(0, Number(p?.right) || 16),
|
|
65
|
+
size: Math.min(96, Math.max(32, Number(p?.size) || 52)), rgb: /^#?([0-9a-f]{6})$/i.exec(String(p?.color ?? '').trim())?.[1] ?? '2f6b4c',
|
|
66
|
+
symbol: p?.symbol === '' ? '' : String(p?.symbol || 'sparkles'), text: p?.text === '' ? '' : String(p?.text || '✦'),
|
|
67
|
+
radius: p?.radius === undefined ? null : Math.max(0, Number(p.radius) || 0), label: String(p?.label || 'Assistant'),
|
|
68
|
+
});
|
|
54
69
|
const parsed = (s: unknown) => { try { return JSON.parse(String(s)); } catch { return s ?? null; } };
|
|
55
70
|
const EVAL_TIMEOUT = 30000;
|
|
56
71
|
|
|
@@ -76,6 +91,11 @@ function registerIos(): void {
|
|
|
76
91
|
let bottomConstraint: NSLayoutConstraint | null = null;
|
|
77
92
|
let emitQueued = false;
|
|
78
93
|
let ObserverClass: any; // KVO sink, built once (ObjC class names are global)
|
|
94
|
+
let fab: UIButton | null = null;
|
|
95
|
+
let fabTarget: any = null, FabTargetClass: any; // its tap target (an exposedMethods NSObject, built once)
|
|
96
|
+
let fabBottom: NSLayoutConstraint | null = null, fabRight: NSLayoutConstraint | null = null, fabW: NSLayoutConstraint | null = null, fabH: NSLayoutConstraint | null = null;
|
|
97
|
+
/** The button stays above the overlay (which comes to the front on open/show). */
|
|
98
|
+
const fabFront = () => { if (fab && !fab.hidden) fab.superview?.bringSubviewToFront(fab); };
|
|
79
99
|
|
|
80
100
|
const state = () => ({
|
|
81
101
|
url: wv?.URL?.absoluteString ?? '',
|
|
@@ -171,6 +191,7 @@ function registerIos(): void {
|
|
|
171
191
|
else setInsets(top, bottom);
|
|
172
192
|
wv!.hidden = false;
|
|
173
193
|
wv!.superview?.bringSubviewToFront(wv!);
|
|
194
|
+
fabFront();
|
|
174
195
|
load(target);
|
|
175
196
|
return state();
|
|
176
197
|
});
|
|
@@ -190,11 +211,51 @@ function registerIos(): void {
|
|
|
190
211
|
|
|
191
212
|
bridge.register('webview.hide', () => onMain(() => { if (wv) wv.hidden = true; return { open: !!wv }; }));
|
|
192
213
|
bridge.register('webview.show', (p?: { top?: number; bottom?: number } | null) => onMain(() => {
|
|
193
|
-
if (wv) { setInsets(p?.top, p?.bottom); wv.hidden = false; wv.superview?.bringSubviewToFront(wv); }
|
|
214
|
+
if (wv) { setInsets(p?.top, p?.bottom); wv.hidden = false; wv.superview?.bringSubviewToFront(wv); fabFront(); }
|
|
194
215
|
return { open: !!wv };
|
|
195
216
|
}));
|
|
196
217
|
bridge.register('webview.close', () => onMain(() => { destroy(); }));
|
|
197
218
|
|
|
219
|
+
bridge.register('webview.fab', (p?: Fab | null) => {
|
|
220
|
+
const o = fabOpts(p);
|
|
221
|
+
return onMain(() => {
|
|
222
|
+
if (!fab) {
|
|
223
|
+
if (!o.show) return { shown: false };
|
|
224
|
+
const host = bridge.getWebView()?.ios as WKWebView | undefined;
|
|
225
|
+
const container = host?.superview ?? Utils.ios.getRootViewController()?.view;
|
|
226
|
+
if (!container) throw err('NATIVE_ERROR', 'webview.fab: no host view');
|
|
227
|
+
FabTargetClass ??= (NSObject as any).extend({ tap() { bridge.emit('webview.fab', {}); } }, { name: 'AppwrapFabTarget', exposedMethods: { tap: { returns: interop.types.void } } });
|
|
228
|
+
fabTarget = FabTargetClass.new();
|
|
229
|
+
const b = UIButton.buttonWithType(UIButtonType.System);
|
|
230
|
+
b.translatesAutoresizingMaskIntoConstraints = false;
|
|
231
|
+
b.addTargetActionForControlEvents(fabTarget, 'tap', UIControlEvents.TouchUpInside);
|
|
232
|
+
b.layer.shadowColor = UIColor.blackColor.CGColor; b.layer.shadowOpacity = 0.28; b.layer.shadowRadius = 9; b.layer.shadowOffset = CGSizeMake(0, 5);
|
|
233
|
+
container.addSubview(b);
|
|
234
|
+
const ref: any = host ?? container;
|
|
235
|
+
fabBottom = b.bottomAnchor.constraintEqualToAnchorConstant(ref.bottomAnchor, -o.bottom);
|
|
236
|
+
fabRight = b.trailingAnchor.constraintEqualToAnchorConstant(ref.trailingAnchor, -o.right);
|
|
237
|
+
fabW = b.widthAnchor.constraintEqualToConstant(o.size); fabH = b.heightAnchor.constraintEqualToConstant(o.size);
|
|
238
|
+
NSLayoutConstraint.activateConstraints([fabBottom, fabRight, fabW, fabH] as any);
|
|
239
|
+
fab = b;
|
|
240
|
+
}
|
|
241
|
+
const n = parseInt(o.rgb, 16);
|
|
242
|
+
fab.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1);
|
|
243
|
+
// the glyph in black or white, whichever reads on the colour
|
|
244
|
+
const light = (0.299 * ((n >> 16) & 255) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255)) > 150;
|
|
245
|
+
fab.tintColor = light ? UIColor.colorWithRedGreenBlueAlpha(0.06, 0.15, 0.1, 1) : UIColor.whiteColor;
|
|
246
|
+
fab.setImageForState(o.symbol ? UIImage.systemImageNamedWithConfiguration(o.symbol, UIImageSymbolConfiguration.configurationWithPointSizeWeight(o.size * 0.42, UIImageSymbolWeight.Semibold)) : null, UIControlState.Normal);
|
|
247
|
+
fab.layer.cornerRadius = o.radius ?? o.size / 2;
|
|
248
|
+
fab.accessibilityLabel = o.label;
|
|
249
|
+
if (fabBottom) fabBottom.constant = -o.bottom;
|
|
250
|
+
if (fabRight) fabRight.constant = -o.right;
|
|
251
|
+
if (fabW) fabW.constant = o.size;
|
|
252
|
+
if (fabH) fabH.constant = o.size; // (a later call may resize it)
|
|
253
|
+
fab.hidden = !o.show;
|
|
254
|
+
fabFront();
|
|
255
|
+
return { shown: o.show };
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
198
259
|
bridge.register('webview.snapshot', (p?: { width?: number; quality?: number } | null) => {
|
|
199
260
|
const o = snapOpts(p);
|
|
200
261
|
return onMain(() => new Promise<{ jpeg: string; width: number; height: number }>((resolve, reject) => {
|
|
@@ -355,7 +416,22 @@ function registerAndroid(): void {
|
|
|
355
416
|
if (bottom !== undefined) insets.bottom = inset(bottom);
|
|
356
417
|
layout();
|
|
357
418
|
};
|
|
358
|
-
|
|
419
|
+
let fab: any = null, fabO = fabOpts(null);
|
|
420
|
+
/** Place the button at the app WebView's bottom-right (`bottom`/`right` CSS px inside it), above the overlay. */
|
|
421
|
+
const fabLayout = () => {
|
|
422
|
+
const frame = content();
|
|
423
|
+
if (!fab || !frame) return;
|
|
424
|
+
const d = density(), px = Math.round(fabO.size * d), fl = [0, 0], hl = [0, 0];
|
|
425
|
+
const h = host ?? bridge.getWebView()?.android;
|
|
426
|
+
let boxBottom = 0, boxRight = 0;
|
|
427
|
+
if (h) { frame.getLocationInWindow(fl); h.getLocationInWindow(hl); boxBottom = frame.getHeight() - (hl[1] - fl[1] + h.getHeight()); boxRight = frame.getWidth() - (hl[0] - fl[0] + h.getWidth()); }
|
|
428
|
+
const lp = new android.widget.FrameLayout.LayoutParams(px, px, android.view.Gravity.BOTTOM | android.view.Gravity.END);
|
|
429
|
+
lp.bottomMargin = Math.max(0, Math.round(boxBottom + fabO.bottom * d));
|
|
430
|
+
lp.rightMargin = Math.max(0, Math.round(boxRight + fabO.right * d));
|
|
431
|
+
fab.setLayoutParams(lp);
|
|
432
|
+
if (fab.getVisibility() === android.view.View.VISIBLE) fab.bringToFront();
|
|
433
|
+
};
|
|
434
|
+
const reveal = () => { wv.setVisibility(android.view.View.VISIBLE); wv.bringToFront(); if (fab?.getVisibility() === android.view.View.VISIBLE) fab.bringToFront(); };
|
|
359
435
|
|
|
360
436
|
bridge.register('webview.open', ({ url, top, bottom }: { url: string; top?: number; bottom?: number }) => {
|
|
361
437
|
const target = String(url ?? '');
|
|
@@ -388,6 +464,35 @@ function registerAndroid(): void {
|
|
|
388
464
|
}));
|
|
389
465
|
bridge.register('webview.close', () => onMain(() => { destroy(); }));
|
|
390
466
|
|
|
467
|
+
bridge.register('webview.fab', (p?: Fab | null) => {
|
|
468
|
+
fabO = fabOpts(p);
|
|
469
|
+
return onMain(() => {
|
|
470
|
+
const frame = content();
|
|
471
|
+
if (!fab) {
|
|
472
|
+
if (!fabO.show) return { shown: false };
|
|
473
|
+
if (!frame) throw err('NATIVE_ERROR', 'webview.fab: no host view');
|
|
474
|
+
fab = new android.widget.TextView(activity());
|
|
475
|
+
fab.setGravity(android.view.Gravity.CENTER);
|
|
476
|
+
fab.setElevation(8 * density());
|
|
477
|
+
fab.setOnClickListener(new android.view.View.OnClickListener({ onClick() { bridge.emit('webview.fab', {}); } }));
|
|
478
|
+
frame.addView(fab);
|
|
479
|
+
}
|
|
480
|
+
const n = parseInt(fabO.rgb, 16), light = (0.299 * ((n >> 16) & 255) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255)) > 150;
|
|
481
|
+
const bg = new android.graphics.drawable.GradientDrawable();
|
|
482
|
+
if (fabO.radius === null) bg.setShape(android.graphics.drawable.GradientDrawable.OVAL);
|
|
483
|
+
else bg.setCornerRadius(fabO.radius * density());
|
|
484
|
+
bg.setColor(android.graphics.Color.rgb((n >> 16) & 255, (n >> 8) & 255, n & 255));
|
|
485
|
+
fab.setBackground(bg);
|
|
486
|
+
fab.setText(fabO.text);
|
|
487
|
+
fab.setTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, fabO.size * 0.42);
|
|
488
|
+
fab.setTextColor(light ? android.graphics.Color.rgb(16, 38, 26) : android.graphics.Color.WHITE);
|
|
489
|
+
fab.setContentDescription(fabO.label);
|
|
490
|
+
fab.setVisibility(fabO.show ? android.view.View.VISIBLE : android.view.View.GONE);
|
|
491
|
+
fabLayout();
|
|
492
|
+
return { shown: fabO.show };
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
391
496
|
bridge.register('webview.snapshot', (p?: { width?: number; quality?: number } | null) => {
|
|
392
497
|
const o = snapOpts(p);
|
|
393
498
|
return onMain(() => {
|