dsh-mobile-access 1.1.5 → 1.1.7
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/README.md +20 -3
- package/client.js +114 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ iPhone 打开「设置 → 无线局域网」,确认和 Mac 连的是同一个
|
|
|
79
79
|
### 第 2 步:在 Mac 终端执行一条命令
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
dsh plugin --profile web add dsh-mobile-access@1.1.
|
|
82
|
+
dsh plugin --profile web add dsh-mobile-access@1.1.7 --registry=https://registry.npmjs.org/
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
这条命令做三件事:
|
|
@@ -190,6 +190,21 @@ iOS Safari 会把第一次触摸先用去聚焦,第二次才派发 `click`。
|
|
|
190
190
|
如果左侧栏仍然是窄窄的图标条,第一下是展开侧栏——这是官方布局,不是会话行的问题;
|
|
191
191
|
展开后每个会话只需单击一次。
|
|
192
192
|
|
|
193
|
+
### iPhone 上展开 / 收起左侧栏
|
|
194
|
+
|
|
195
|
+
从 `1.1.7` 起,iPhone / iPad 上支持:
|
|
196
|
+
|
|
197
|
+
- 在对话内容区**向右滑**:展开左侧栏(仅在左侧栏已收起时生效);
|
|
198
|
+
- 在对话内容区**单击一下**:收起已经展开的左侧栏;
|
|
199
|
+
- 右滑需要水平距离至少 70px,且明显大于上下移动,避免和滚动、
|
|
200
|
+
横向代码块、输入框、模型菜单和右侧面板冲突;
|
|
201
|
+
- 单击收起只对“内容区”生效,点按钮、输入框、链接、代码块、会话行、
|
|
202
|
+
模型菜单或右侧面板不会误收起。
|
|
203
|
+
|
|
204
|
+
普通 Safari 标签页里,右滑会避开屏幕左右各 24px 的边缘,以免和 Safari
|
|
205
|
+
自带的“后退 / 前进”边缘手势抢事件;添加到主屏幕的独立窗口里没有这个
|
|
206
|
+
系统手势,因此边缘也可以滑动。Mac 没有触摸事件,完全不受影响。
|
|
207
|
+
|
|
193
208
|
`1.1.3` 的旧实现只移除 `tabindex`,在部分 iOS Safari 版本上仍可能被第一次触摸吃掉;
|
|
194
209
|
如果更新到 `1.1.4` 后仍是两次,请彻底关闭这个 Safari 标签页再重新打开(不要只刷新),
|
|
195
210
|
以排除旧的 `client.js` 缓存。
|
|
@@ -370,7 +385,7 @@ iPhone 软键盘没有可用的 Shift 键,因此插件在 **iOS / iPadOS** 上
|
|
|
370
385
|
### 推荐:npm 官方命令
|
|
371
386
|
|
|
372
387
|
```bash
|
|
373
|
-
dsh plugin --profile web add dsh-mobile-access@1.1.
|
|
388
|
+
dsh plugin --profile web add dsh-mobile-access@1.1.7 --registry=https://registry.npmjs.org/
|
|
374
389
|
```
|
|
375
390
|
|
|
376
391
|
### 使用 `@latest`
|
|
@@ -500,7 +515,9 @@ dsh plugin --profile web remove dsh-mobile-access
|
|
|
500
515
|
- `1.1.0` — 回环监听自动局域网网关(macOS / Windows,HTTP + `/api/remote.mux` WebSocket)。
|
|
501
516
|
- `1.1.1` — iPhone / iPad 上模型选择页 16px 缩放守卫。
|
|
502
517
|
- `1.1.2` — 打开模型选择时不自动弹键盘、不整体上移。
|
|
503
|
-
- `1.1.
|
|
518
|
+
- `1.1.7` — iPhone / iPad 上右滑展开左侧栏,点击内容区收起;当前 npm `latest`。
|
|
519
|
+
- `1.1.6` — iPhone / iPad 上右滑展开、左滑收起左侧栏。
|
|
520
|
+
- `1.1.5` — Tailscale Serve 经 HTTPS 访问时,允许带 Tailscale 身份头的 `*.ts.net` Host(仅限本机回环转发)。
|
|
504
521
|
- `1.1.4` — iPhone / iPad 上第一次点击直接切换会话,并在切换后自动收起左侧栏。
|
|
505
522
|
- `1.1.3` — 首次尝试临时移除会话行 `tabindex`。
|
|
506
523
|
|
package/client.js
CHANGED
|
@@ -291,6 +291,108 @@ function installIOSSidebarTapGuard(layout) {
|
|
|
291
291
|
timers.clear();
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Right swipe on iPhone opens the left sidebar. The closing gesture is a tap
|
|
296
|
+
* on the content area (not a left swipe): a normal tap is easier to discover
|
|
297
|
+
* and does not compete with horizontal scrolling. The swipe is scoped to the
|
|
298
|
+
* content area and ignores vertical movement, editable controls, menus, code
|
|
299
|
+
* blocks and the right panel. On a normal Safari tab we stay 24px away from the
|
|
300
|
+
* screen edges so the system back/forward gesture keeps priority; an installed
|
|
301
|
+
* PWA has no such gesture, so edge starts are allowed there. Mac never installs
|
|
302
|
+
* this listener.
|
|
303
|
+
*/
|
|
304
|
+
function installIOSSidebarSwipeGuard(layout) {
|
|
305
|
+
if (!isAppleMobile() || layout === undefined)
|
|
306
|
+
return () => undefined;
|
|
307
|
+
const doc = document;
|
|
308
|
+
let pending = null;
|
|
309
|
+
const isBlocked = (target) => target.closest('input, textarea, select, [contenteditable="true"], [data-composer-card], pre, code, [role="slider"], [data-rightbar], [role="menu"], [role="listbox"], button, a, [role="button"]') !== null;
|
|
310
|
+
const point = (event) => {
|
|
311
|
+
const touches = event.changedTouches ?? event.touches;
|
|
312
|
+
const touch = touches?.[0];
|
|
313
|
+
return touch === undefined ? null : { x: touch.clientX, y: touch.clientY };
|
|
314
|
+
};
|
|
315
|
+
const onTouchStart = (event) => {
|
|
316
|
+
pending = null;
|
|
317
|
+
const touches = event.touches;
|
|
318
|
+
if (touches !== undefined && touches.length !== 1)
|
|
319
|
+
return;
|
|
320
|
+
const target = event.target;
|
|
321
|
+
if (target === null || typeof target.closest !== 'function' || isBlocked(target))
|
|
322
|
+
return;
|
|
323
|
+
const at = point(event);
|
|
324
|
+
if (at === null)
|
|
325
|
+
return;
|
|
326
|
+
const standalone = navigator.standalone === true;
|
|
327
|
+
if (!standalone && (at.x < 24 || at.x > window.innerWidth - 24))
|
|
328
|
+
return;
|
|
329
|
+
pending = { x: at.x, y: at.y, startedAt: Date.now() };
|
|
330
|
+
};
|
|
331
|
+
const onTouchEnd = (event) => {
|
|
332
|
+
if (pending === null)
|
|
333
|
+
return;
|
|
334
|
+
const { x, y, startedAt } = pending;
|
|
335
|
+
pending = null;
|
|
336
|
+
if (Date.now() - startedAt > 800)
|
|
337
|
+
return;
|
|
338
|
+
const at = point(event);
|
|
339
|
+
if (at === null)
|
|
340
|
+
return;
|
|
341
|
+
const dx = at.x - x;
|
|
342
|
+
const dy = at.y - y;
|
|
343
|
+
if (Math.abs(dx) < 70 || Math.abs(dx) <= Math.abs(dy) * 1.5)
|
|
344
|
+
return;
|
|
345
|
+
const collapsed = doc.querySelector('[data-sidebar-collapsed]') !== null;
|
|
346
|
+
if (!collapsed)
|
|
347
|
+
return;
|
|
348
|
+
try {
|
|
349
|
+
layout.toggleSidebar();
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
doc.addEventListener('touchstart', onTouchStart, true);
|
|
355
|
+
doc.addEventListener('touchend', onTouchEnd, true);
|
|
356
|
+
return () => {
|
|
357
|
+
doc.removeEventListener('touchstart', onTouchStart, true);
|
|
358
|
+
doc.removeEventListener('touchend', onTouchEnd, true);
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* A tap on the conversation content closes an expanded narrow sidebar. It has
|
|
363
|
+
* to run in the bubble phase so message links and buttons still receive their
|
|
364
|
+
* click first, and it skips every interactive/row/right-panel target. The
|
|
365
|
+
* sidebar width comes from the frame's inline grid template, so a tap on the
|
|
366
|
+
* left column is not mistaken for content. Mac and wide layouts are untouched.
|
|
367
|
+
*/
|
|
368
|
+
function installIOSContentTapCollapse(layout) {
|
|
369
|
+
if (!isAppleMobile() || layout === undefined)
|
|
370
|
+
return () => undefined;
|
|
371
|
+
const doc = document;
|
|
372
|
+
const interactive = 'button, a, input, textarea, select, pre, code, [contenteditable="true"], [data-composer-card], [data-row-key], [role="treeitem"], [role="tree"], nav, [role="menu"], [role="listbox"], [role="button"], [role="menuitem"], [data-rightbar]';
|
|
373
|
+
const onClick = (event) => {
|
|
374
|
+
if (window.innerWidth >= 1024)
|
|
375
|
+
return;
|
|
376
|
+
if (doc.querySelector('[data-sidebar-collapsed]') !== null)
|
|
377
|
+
return;
|
|
378
|
+
const target = event.target;
|
|
379
|
+
if (target === null || typeof target.closest !== 'function' || target.closest(interactive) !== null)
|
|
380
|
+
return;
|
|
381
|
+
const frame = doc.querySelector('[style*="grid-template-columns"]');
|
|
382
|
+
const first = frame?.style.gridTemplateColumns.split(' ')[0] ?? '';
|
|
383
|
+
const width = Number.parseFloat(first);
|
|
384
|
+
const x = event.clientX;
|
|
385
|
+
if (Number.isFinite(width) && x < width)
|
|
386
|
+
return;
|
|
387
|
+
try {
|
|
388
|
+
layout.toggleSidebar();
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
doc.addEventListener('click', onClick, true);
|
|
394
|
+
return () => doc.removeEventListener('click', onClick, true);
|
|
395
|
+
}
|
|
294
396
|
/**
|
|
295
397
|
* The official model menu can move focus into its search field while it opens.
|
|
296
398
|
* On iPhone that opens the software keyboard and shifts the fixed-position menu
|
|
@@ -637,6 +739,18 @@ function factory(require) {
|
|
|
637
739
|
catch (error) {
|
|
638
740
|
ctx.logger?.warn?.(`[mobile-access] iOS sidebar tap guard unavailable: ${String(error)}`);
|
|
639
741
|
}
|
|
742
|
+
try {
|
|
743
|
+
cleanups.push(installIOSSidebarSwipeGuard(ctx.layout));
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
ctx.logger?.warn?.(`[mobile-access] iOS sidebar swipe guard unavailable: ${String(error)}`);
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
cleanups.push(installIOSContentTapCollapse(ctx.layout));
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
ctx.logger?.warn?.(`[mobile-access] iOS content tap collapse unavailable: ${String(error)}`);
|
|
753
|
+
}
|
|
640
754
|
return () => {
|
|
641
755
|
for (const cleanup of cleanups.reverse()) {
|
|
642
756
|
try {
|
package/package.json
CHANGED