dsh-mobile 0.8.8 → 0.8.9
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/CHANGELOG.md +15 -0
- package/CONTRIBUTORS.md +10 -0
- package/README.en.md +8 -6
- package/README.md +8 -6
- package/apps/mobile/README.md +19 -3
- package/apps/mobile/README.zh-CN.md +19 -3
- package/apps/mobile/release.json +3 -3
- package/docs/ATTACH_EXISTING_FRPS.en.md +1 -1
- package/docs/IROH.en.md +4 -4
- package/docs/IROH.md +4 -4
- package/docs/README.md +8 -1
- package/docs/TRUSTED_NETWORKS.en.md +2 -0
- package/docs/TRUSTED_NETWORKS.md +2 -0
- package/docs/WEB_PUSH.en.md +1 -1
- package/docs/WEB_PUSH.md +1 -1
- package/lib/client.js +191 -0
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +2 -1
- package/lib/index.mjs.map +1 -1
- package/lib/mobile-layout.js +17 -4
- package/lib/mobile-layout.js.map +1 -1
- package/native/iroh-host/README.md +1 -1
- package/package.json +3 -2
- package/packages/question-fixes/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -4305,6 +4305,182 @@ window.__ModuleLoader__.load({
|
|
|
4305
4305
|
}
|
|
4306
4306
|
};
|
|
4307
4307
|
}
|
|
4308
|
+
// src/mobile-path-copy.ts
|
|
4309
|
+
/** Tap-to-copy for the stock preview header path on touch devices.
|
|
4310
|
+
*
|
|
4311
|
+
* Taps land on `PathLabel`'s nested directory/name spans, so the titled owner is
|
|
4312
|
+
* resolved through `closest()`; the changes-review file switcher keeps its own tap.
|
|
4313
|
+
*/
|
|
4314
|
+
/** The repository's shared touch predicate; kept identical to `TOUCH_PRIMARY_QUERY` (mobile-layout.ts)
|
|
4315
|
+
* and `BROWSER_TOUCH_PRIMARY_QUERY` (native-mobile.ts) by a source drift test. */
|
|
4316
|
+
const PATH_COPY_TOUCH_QUERY = "(hover: none), (pointer: coarse)";
|
|
4317
|
+
/** Stock-DSH element whose `title` carries the path shown to the user: the preview
|
|
4318
|
+
* header path bar. The presented-file card is deliberately NOT a target — its tap
|
|
4319
|
+
* stays a pure open action, so opening a file never replaces the clipboard. */
|
|
4320
|
+
const PATH_COPY_TARGET_SELECTOR = "[data-textpreview-path]";
|
|
4321
|
+
/** Replace `{path}` in a localized toast template with the copied value, literally.
|
|
4322
|
+
*
|
|
4323
|
+
* The replacement is a function because a string replacement would interpret `$&`,
|
|
4324
|
+
* `` $` `` and `$'` inside the path as replacement patterns, which would show a
|
|
4325
|
+
* mangled message for a legal file name that contains them. */
|
|
4326
|
+
function formatPathCopyMessage(template, path) {
|
|
4327
|
+
return template.replaceAll("{path}", () => path);
|
|
4328
|
+
}
|
|
4329
|
+
/** The `title` of a copy target, verbatim — trailing spaces are legal in file names —
|
|
4330
|
+
* or `undefined` when the attribute is absent or blank (blankness judged after trimming). */
|
|
4331
|
+
function pathCopyText(element) {
|
|
4332
|
+
const title = element.getAttribute("title");
|
|
4333
|
+
const blank = title?.trim();
|
|
4334
|
+
return title !== null && blank !== void 0 && blank !== "" ? title : void 0;
|
|
4335
|
+
}
|
|
4336
|
+
function isClosestCarrier(value) {
|
|
4337
|
+
return typeof value?.closest === "function";
|
|
4338
|
+
}
|
|
4339
|
+
/** Resolve the copy target owning the tapped node: the titled ancestor outside the review scope.
|
|
4340
|
+
* @param target - an event target; non-elements resolve to `null`.
|
|
4341
|
+
* @returns the element whose `title` to copy, or `null` when the tap is not a copy target. */
|
|
4342
|
+
function pathCopyOwner(target) {
|
|
4343
|
+
if (!isClosestCarrier(target)) return null;
|
|
4344
|
+
const owner = target.closest(PATH_COPY_TARGET_SELECTOR);
|
|
4345
|
+
if (owner === null) return null;
|
|
4346
|
+
if (target.closest("[data-changes-review]") !== null) return null;
|
|
4347
|
+
return pathCopyText(owner) === void 0 ? null : owner;
|
|
4348
|
+
}
|
|
4349
|
+
/** Copy through the API, falling back to `execCopy`; resolves, never rejects.
|
|
4350
|
+
* @param text - the value to place on the clipboard.
|
|
4351
|
+
* @param io - the clipboard operations to use.
|
|
4352
|
+
* @returns whether the value reached the clipboard by either route. */
|
|
4353
|
+
async function copyPathText(text, io) {
|
|
4354
|
+
try {
|
|
4355
|
+
await io.writeText(text);
|
|
4356
|
+
return true;
|
|
4357
|
+
} catch {
|
|
4358
|
+
try {
|
|
4359
|
+
return io.execCopy(text);
|
|
4360
|
+
} catch {
|
|
4361
|
+
return false;
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
/** Build the browser clipboard adapter for one document.
|
|
4366
|
+
*
|
|
4367
|
+
* `writeText` delegates to `navigator.clipboard` when present and rejects otherwise, so
|
|
4368
|
+
* {@link copyPathText} routes to the fallback. `execCopy` copies through an off-screen but
|
|
4369
|
+
* rendered readonly textarea — `display:none` is not a reliable copy source — and restores
|
|
4370
|
+
* focus, selection, and scroll on every path.
|
|
4371
|
+
* @param document - the document that will host the fallback textarea.
|
|
4372
|
+
* @returns the adapter bound to that document and this window's navigator. */
|
|
4373
|
+
function domCopyIo(document) {
|
|
4374
|
+
return {
|
|
4375
|
+
writeText: (text) => {
|
|
4376
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
4377
|
+
if (clipboard?.writeText === void 0) return Promise.reject(/* @__PURE__ */ new Error("clipboard api unavailable"));
|
|
4378
|
+
return clipboard.writeText(text);
|
|
4379
|
+
},
|
|
4380
|
+
execCopy: (value) => {
|
|
4381
|
+
const activeElement = document.activeElement;
|
|
4382
|
+
const selection = document.getSelection();
|
|
4383
|
+
const savedRanges = selection === null ? [] : Array.from({ length: selection.rangeCount }, (_, at) => selection.getRangeAt(at).cloneRange());
|
|
4384
|
+
const root = document.documentElement;
|
|
4385
|
+
const scrollOwners = [root];
|
|
4386
|
+
for (let node = activeElement; node !== null && node !== root; node = node.parentElement ?? null) scrollOwners.push(node);
|
|
4387
|
+
const savedScroll = scrollOwners.map((owner) => ({
|
|
4388
|
+
owner,
|
|
4389
|
+
top: owner.scrollTop,
|
|
4390
|
+
left: owner.scrollLeft
|
|
4391
|
+
}));
|
|
4392
|
+
const area = document.createElement("textarea");
|
|
4393
|
+
area.value = value;
|
|
4394
|
+
area.readOnly = true;
|
|
4395
|
+
area.style.position = "fixed";
|
|
4396
|
+
area.style.left = "-9999px";
|
|
4397
|
+
document.body.appendChild(area);
|
|
4398
|
+
try {
|
|
4399
|
+
area.select();
|
|
4400
|
+
return document.execCommand("copy");
|
|
4401
|
+
} catch {
|
|
4402
|
+
return false;
|
|
4403
|
+
} finally {
|
|
4404
|
+
document.body.removeChild(area);
|
|
4405
|
+
if (selection !== null) {
|
|
4406
|
+
selection.removeAllRanges();
|
|
4407
|
+
for (const range of savedRanges) selection.addRange(range);
|
|
4408
|
+
}
|
|
4409
|
+
if (activeElement !== null) activeElement.focus?.({ preventScroll: true });
|
|
4410
|
+
for (const { owner, top, left } of savedScroll) {
|
|
4411
|
+
owner.scrollTop = top;
|
|
4412
|
+
owner.scrollLeft = left;
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
};
|
|
4417
|
+
}
|
|
4418
|
+
/** Host the copy confirmation toast on one document.
|
|
4419
|
+
* @param document - the document whose body hosts the toast.
|
|
4420
|
+
* @returns the toast handle. */
|
|
4421
|
+
function createPathCopyToast(document) {
|
|
4422
|
+
const node = document.createElement("div");
|
|
4423
|
+
node.className = "dsh-mobile-path-toast";
|
|
4424
|
+
node.setAttribute("role", "status");
|
|
4425
|
+
node.setAttribute("aria-live", "polite");
|
|
4426
|
+
node.lang = document.documentElement.lang;
|
|
4427
|
+
document.body.appendChild(node);
|
|
4428
|
+
let timer;
|
|
4429
|
+
return {
|
|
4430
|
+
node,
|
|
4431
|
+
show(message) {
|
|
4432
|
+
node.textContent = message;
|
|
4433
|
+
node.setAttribute("data-visible", "true");
|
|
4434
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4435
|
+
timer = setTimeout(() => {
|
|
4436
|
+
node.removeAttribute("data-visible");
|
|
4437
|
+
timer = void 0;
|
|
4438
|
+
}, 2200);
|
|
4439
|
+
},
|
|
4440
|
+
dispose() {
|
|
4441
|
+
if (timer !== void 0) {
|
|
4442
|
+
clearTimeout(timer);
|
|
4443
|
+
timer = void 0;
|
|
4444
|
+
}
|
|
4445
|
+
node.removeAttribute("data-visible");
|
|
4446
|
+
document.body.removeChild(node);
|
|
4447
|
+
}
|
|
4448
|
+
};
|
|
4449
|
+
}
|
|
4450
|
+
/** Install the delegated tap-to-copy listener and its toast.
|
|
4451
|
+
*
|
|
4452
|
+
* The listener neither prevents nor stops propagation: the preview header keeps its
|
|
4453
|
+
* own focus behavior, and taps on other elements keep their own actions. Disposal
|
|
4454
|
+
* stops new copies, cancels the fallback of a still-pending write, and drops a
|
|
4455
|
+
* completion that lands after disposal.
|
|
4456
|
+
* @param deps - document, touch gate, localized templates, optional clipboard adapter.
|
|
4457
|
+
* @returns the disposer. */
|
|
4458
|
+
function installMobilePathCopy(deps) {
|
|
4459
|
+
const source = deps.io ?? domCopyIo(deps.document);
|
|
4460
|
+
const toast = createPathCopyToast(deps.document);
|
|
4461
|
+
let disposed = false;
|
|
4462
|
+
const io = {
|
|
4463
|
+
writeText: (text) => source.writeText(text),
|
|
4464
|
+
execCopy: (value) => disposed ? false : source.execCopy(value)
|
|
4465
|
+
};
|
|
4466
|
+
const onClick = (event) => {
|
|
4467
|
+
if (disposed || !deps.isTouchPrimary()) return;
|
|
4468
|
+
const owner = pathCopyOwner(event.target);
|
|
4469
|
+
if (owner === null) return;
|
|
4470
|
+
const path = pathCopyText(owner);
|
|
4471
|
+
if (path === void 0) return;
|
|
4472
|
+
copyPathText(path, io).then((ok) => {
|
|
4473
|
+
if (disposed) return;
|
|
4474
|
+
toast.show(formatPathCopyMessage(ok ? deps.labels.copied : deps.labels.failed, path));
|
|
4475
|
+
});
|
|
4476
|
+
};
|
|
4477
|
+
deps.document.addEventListener("click", onClick);
|
|
4478
|
+
return () => {
|
|
4479
|
+
disposed = true;
|
|
4480
|
+
deps.document.removeEventListener("click", onClick);
|
|
4481
|
+
toast.dispose();
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4308
4484
|
// src/native-mobile.ts
|
|
4309
4485
|
/**
|
|
4310
4486
|
* Viewport below which the injected chrome becomes an overlay drawer: the
|
|
@@ -4324,6 +4500,12 @@ window.__ModuleLoader__.load({
|
|
|
4324
4500
|
the query restores the fixed scrim, and its more specific [hidden] rule
|
|
4325
4501
|
keeps winning there. */
|
|
4326
4502
|
.dsh-native-mobile-backdrop,.dsh-mobile-branch-toast,.dsh-mobile-media-toast { display:none; }
|
|
4503
|
+
/* The path-copy toast confirms a clipboard write whose gate is the touch
|
|
4504
|
+
predicate, not viewport width, so its box lives outside the overlay query:
|
|
4505
|
+
a landscape phone or tablet copy must stay visible. The bottom slot keeps
|
|
4506
|
+
it clear of the branch and media toasts' shared top-center position. */
|
|
4507
|
+
.dsh-mobile-path-toast { display:none; }
|
|
4508
|
+
.dsh-mobile-path-toast[data-visible="true"] { display:block; position:fixed; z-index:330; left:50%; transform:translateX(-50%); bottom:max(12px,env(safe-area-inset-bottom)); max-width:calc(100vw - 32px); box-sizing:border-box; padding:7px 14px; border:1px solid rgb(15 23 42 / 10%); border-radius:999px; background:rgb(15 23 42 / 92%); color:#fff; font-size:13px; line-height:20px; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:none; }
|
|
4327
4509
|
/* The content-font preference may exceed Safari's focused-editable minimum.
|
|
4328
4510
|
The html-level class exists on every non-desktop mobile page, so it also
|
|
4329
4511
|
covers body-level portals (model picker search) the center column never
|
|
@@ -5520,6 +5702,14 @@ window.__ModuleLoader__.load({
|
|
|
5520
5702
|
mediaToastTimer = 0;
|
|
5521
5703
|
}, 2200);
|
|
5522
5704
|
};
|
|
5705
|
+
const disposePathCopy = installMobilePathCopy({
|
|
5706
|
+
document,
|
|
5707
|
+
isTouchPrimary: () => window.matchMedia(PATH_COPY_TOUCH_QUERY).matches,
|
|
5708
|
+
labels: {
|
|
5709
|
+
copied: label("Copiato: {path}", "Copied: {path}", "已复制:{path}"),
|
|
5710
|
+
failed: label("Impossibile copiare il percorso.", "Could not copy the path.", "无法复制路径。")
|
|
5711
|
+
}
|
|
5712
|
+
});
|
|
5523
5713
|
let mediaRequestGeneration = 0;
|
|
5524
5714
|
let disposed = false;
|
|
5525
5715
|
let boundComposer = null;
|
|
@@ -6147,6 +6337,7 @@ window.__ModuleLoader__.load({
|
|
|
6147
6337
|
disposed = true;
|
|
6148
6338
|
stopStockBack();
|
|
6149
6339
|
sidebarScrollMemory.dispose();
|
|
6340
|
+
disposePathCopy();
|
|
6150
6341
|
disposeTaskWatcher();
|
|
6151
6342
|
mediaRequestGeneration += 1;
|
|
6152
6343
|
mediaPickerAbortController.abort();
|