dsh-mobile 0.6.5 → 0.6.6

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  Notable changes are recorded here. GitHub Releases provide the downloadable packages and generated commit notes.
4
4
 
5
+ ## Unreleased Android changes
6
+
7
+ - Prefer the system Photo Picker on Android 13+ for supported image requests, with document selection for other requests or an unavailable picker. Preserve multi-selection, MIME filtering and selected URI results without adding storage permission or forcing a vendor gallery.
8
+ - Bind standard file callbacks to the active WebView, Origin and document. Cancel them on navigation or teardown and prevent old results from consuming a newer request. These changes require a future App release; the published Android 0.6.3 APK is unchanged.
9
+
10
+ ## 0.6.6 - 2026-10-14
11
+
12
+ - Fix captured images not reaching DSH's attachment draft. Supply both files and iterable file items, with compatible drag-and-drop objects for browsers whose constructed objects are incomplete.
13
+ - Improve browser camera and extension file selection: returning to the page no longer discards a pending selection after one second. Handle explicit cancellation, late results, aborted requests and timeouts while preserving the current draft.
14
+ - Add packed DSH regressions for multiple images, decoded previews, cancellation and superseded requests. The Android App remains independently published as 0.6.3; this plugin update does not replace its system file chooser.
15
+ - Thanks @Void-LifeStream for the detailed report in [#205](https://github.com/saya-ch/dsh-mobile/issues/205), and @xhwxt for the document-picker proposal in [#203](https://github.com/saya-ch/dsh-mobile/pull/203).
16
+
5
17
  ## 0.6.5 - 2026-10-13
6
18
 
7
19
  - Make mobile module selection easier to navigate: optional extensions first, collapsed DSH and boot-required groups, name/ID search, loading filters, and exact IDs and dependencies in details. Keep close and save actions outside the scrolling list, show unsaved changes, and confirm before discarding them.
package/CONTRIBUTORS.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # 贡献者 / Contributors
2
2
 
3
- GitHub 的 [Contributors 图表](https://github.com/saya-ch/dsh-mobile/graphs/contributors) 按进入默认分支的提交自动统计,无法手动加入仅提交 issue 或 PR 提案的成员。本页按截至 2026-10-13 的实际参与方式感谢社区成员;仓库维护者与自动依赖更新仍可在 GitHub 历史记录中查看。
3
+ GitHub 的 [Contributors 图表](https://github.com/saya-ch/dsh-mobile/graphs/contributors) 按进入默认分支的提交自动统计,无法手动加入仅提交 issue 或 PR 提案的成员。本页按截至 2026-10-14 的实际参与方式感谢社区成员;仓库维护者与自动依赖更新仍可在 GitHub 历史记录中查看。
4
4
 
5
- GitHub's Contributors graph counts commits on the default branch; issue reports and PR proposals cannot be manually added to it. This page credits community members by how they contributed, through 2026-10-13. The repository maintainer and automated dependency updates remain visible in GitHub's history.
5
+ GitHub's Contributors graph counts commits on the default branch; issue reports and PR proposals cannot be manually added to it. This page credits community members by how they contributed, through 2026-10-14. The repository maintainer and automated dependency updates remain visible in GitHub's history.
6
6
 
7
7
  ## 0.6.4 代码贡献 / Code contributions in 0.6.4
8
8
 
@@ -58,6 +58,10 @@ GitHub's Contributors graph counts commits on the default branch; issue reports
58
58
 
59
59
  ## 问题反馈与建议 / Issue reports and ideas
60
60
 
61
+ 感谢 [@xhwxt](https://github.com/xhwxt) 在 [#203](https://github.com/saya-ch/dsh-mobile/pull/203) 中提出文档选择器改进方案,以及 [@Void-LifeStream](https://github.com/Void-LifeStream) 在 [#205](https://github.com/saya-ch/dsh-mobile/issues/205) 中提供附件选择与回传问题的详细反馈。
62
+
63
+ Thanks [@xhwxt](https://github.com/xhwxt) for the document-picker proposal in [#203](https://github.com/saya-ch/dsh-mobile/pull/203), and [@Void-LifeStream](https://github.com/Void-LifeStream) for the detailed attachment-selection and delivery report in [#205](https://github.com/saya-ch/dsh-mobile/issues/205).
64
+
61
65
  以下名单包括提出问题、复现缺陷和建议功能的成员;出现在本表不表示提交过代码。 / These members opened repository issues; listing them does not imply code authorship.
62
66
 
63
67
  | 反馈贡献者 / Issue reporter | 提交的问题 / Issues |
@@ -101,6 +105,7 @@ GitHub's Contributors graph counts commits on the default branch; issue reports
101
105
  | [@sortjiajun](https://github.com/sortjiajun) | [#90](https://github.com/saya-ch/dsh-mobile/issues/90) |
102
106
  | [@StrawberryAO](https://github.com/StrawberryAO) | [#39](https://github.com/saya-ch/dsh-mobile/issues/39) |
103
107
  | [@Surpass567](https://github.com/Surpass567) | [#80](https://github.com/saya-ch/dsh-mobile/issues/80), [#81](https://github.com/saya-ch/dsh-mobile/issues/81) |
108
+ | [@Void-LifeStream](https://github.com/Void-LifeStream) | [#205](https://github.com/saya-ch/dsh-mobile/issues/205) |
104
109
  | [@WalkerJay666](https://github.com/WalkerJay666) | [#201](https://github.com/saya-ch/dsh-mobile/issues/201) |
105
110
  | [@up-and-down-0618](https://github.com/up-and-down-0618) | [#115](https://github.com/saya-ch/dsh-mobile/issues/115) |
106
111
  | [@taki-ohh](https://github.com/taki-ohh) | [#19](https://github.com/saya-ch/dsh-mobile/issues/19) |
package/README.en.md CHANGED
@@ -31,7 +31,7 @@
31
31
 
32
32
  > DSH Mobile is a DeepSeek Harness community plugin; the native app supports Android only.
33
33
  >
34
- > **Current stable release: 0.6.5**. Improve mobile module management, typography and settings accessibility; fix Windows cpolar extraction and blank Trajectory views caused by some skins. [Changelog](CHANGELOG.md) · [Release downloads](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.6.5).
34
+ > **Current stable release: 0.6.6**. Fix captured images not reaching the DSH attachment draft, and improve cancellation and late-result handling in browser file selection. [Changelog](CHANGELOG.md) · [Release downloads](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.6.6).
35
35
  >
36
36
  > **Version policy**: since plugin **0.6.2**, the Android app and plugin use independent version numbers and releases. Their numbers do not need to match, and a plugin update does not imply an app update. [Compatibility notes](#compatibility).
37
37
 
@@ -396,6 +396,7 @@ On macOS, local network, cloudflared, self-hosted FRP and the own reverse proxy
396
396
  | `0.6.3` | `0.2.0-rc.2` and `0.2.1-alpha.2` (packed installation, isolated pairing, mobile boot and WebSocket Workspace reads); additional checks cover the composer, settings and extension Worker timeout, streaming and explicit recovery |
397
397
  | `0.6.4` | `0.2.0-rc.2` (packed installation, isolated pairing, mobile boot and WebSocket Workspace reads); additionally 72 real public-slot composer layouts, Windows Desktop directory-picker composition and cross-platform regressions. The reported remote timeout in community Desktop 2.0.17 remains unclaimed as accepted |
398
398
  | `0.6.5` | Packed module-management and mobile-settings regressions on `0.2.0-rc.2` and `0.2.1-alpha.2`; additionally Windows cpolar extraction and populated Trajectory layout with published `maid-atelier` 0.1.7. This does not establish acceptance of every third-party skin or reporter device |
399
+ | `0.6.6` | Packed attachment intake and decoded-preview regressions on `0.2.0-rc.2` and `0.2.1-alpha.2`, covering multiple images, cancellation, late results and superseded requests. Android system-picker changes are released independently, not through the npm update |
399
400
 
400
401
  <details>
401
402
  <summary>Historical version checks</summary>
package/README.md CHANGED
@@ -31,7 +31,7 @@
31
31
 
32
32
  > DSH Mobile 是 DeepSeek Harness 社区插件,原生 App 仅支持 Android。
33
33
  >
34
- > **当前正式版本:0.6.5**。优化移动页面模块管理与字体设置,改善设置操作可达性,修复 Windows cpolar 解包及部分皮肤造成的轨迹空白。[更新记录](CHANGELOG.md) · [版本下载](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.6.5)。
34
+ > **当前正式版本:0.6.6**。修复拍照图片无法进入 DSH 附件草稿的问题,改善浏览器文件选择的取消与迟到结果处理。[更新记录](CHANGELOG.md) · [版本下载](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.6.6)。
35
35
  >
36
36
  > **版本说明**:自插件 **0.6.2** 起,Android App 与插件独立编号、独立发布,版本号无需一致;插件升级不代表 App 需要同步升级。[兼容说明](#兼容性)。
37
37
 
@@ -397,6 +397,7 @@ macOS 上局域网、cloudflared、自建 FRP 与自有反向代理可用;Funn
397
397
  | `0.6.3` | `0.2.0-rc.2`、`0.2.1-alpha.2`(打包安装、隔离配对、移动页面与 WebSocket 工作区读取);另验证输入栏、设置与扩展 Worker 的超时、流式传输和显式恢复 |
398
398
  | `0.6.4` | `0.2.0-rc.2`(打包安装、隔离配对、移动页面与 WebSocket 工作区读取);另验证 72 组真实公共插槽输入栏布局、Windows Desktop 目录选择器组合及跨平台回归。社区 Desktop 2.0.17 的原报告远程超时仍未宣称验收 |
399
399
  | `0.6.5` | `0.2.0-rc.2`、`0.2.1-alpha.2` 的打包模块管理与移动设置回归;另验证 Windows cpolar 解包及正式 `maid-atelier` 0.1.7 下有内容轨迹的布局修复。不代表全部第三方皮肤或报告者设备已验收 |
400
+ | `0.6.6` | `0.2.0-rc.2`、`0.2.1-alpha.2` 的打包附件接收与图片预览回归,覆盖多图、取消、迟到结果和旧请求;Android 系统选择器改进独立发布,不包含在 npm 更新中 |
400
401
 
401
402
  <details>
402
403
  <summary>历史版本验证记录</summary>
@@ -8,6 +8,10 @@ Android is the only supported native target. The iOS client remains an unpublish
8
8
 
9
9
  The current stable app is **0.6.3** (build 79). The app and npm plugin update independently; plugin web UI or connection fixes do not require reinstalling the APK. Install a new signed release APK for native updates or when the app reports that its version is too old. Older official APKs with the same signer retain pairing during in-place upgrades; a differently signed Debug build cannot overwrite the official app.
10
10
 
11
+ ### Attachment changes in development
12
+
13
+ Unreleased App code uses the system Photo Picker on Android 13+ when the webpage requests a single supported image MIME filter. Multiple selection follows the system limit; unavailable pickers, older systems, capture requests and filters that cannot be represented exactly use the document picker instead. Document requests start directly, retain MIME and multiple settings, and do not promise a particular manufacturer's gallery. Standard callbacks are bound to the current WebView, Origin and document; obsolete results cannot complete a newer request. These native changes are not in the published 0.6.3 APK, and need a future signed App update.
14
+
11
15
  ## 0.6.3 update
12
16
 
13
17
  Build 79 adds per-computer recovery guidance, readable expired-CA records, duplicate-pairing protection and deduplicated status checks that leave capacity for an explicit connection. TLS still refuses expired certificates. It also improves dark-mode primary buttons and explicitly excludes encrypted pairings and WebView sessions from cloud backup and device transfer. The earlier automatic-recovery ownership corrections are included. This signed App is published independently from plugin 0.6.4.
@@ -106,7 +110,9 @@ For ordinary DSH attachments, the App forwards the webpage's multiple-selection
106
110
 
107
111
  Task reminders use Host completion events and explicit pending-input cards while the page remains alive. Enable notifications or open system settings from DSH **Settings → General** inside the app. Lock-screen text is generic and tapping opens the app. Plugin 0.6.4 distinguishes full session and turn IDs, including long and non-ASCII IDs; earlier versions can replace notifications when their tags collide. Completion reminders require SSE and have no offline event replay; Cloudflare Quick Tunnels do not support the stream. See [channel limits](../../docs/CLOUDFLARE_TUNNEL.en.md#quick-tunnel-feature-limits).
108
112
 
109
- Only one file-selection or camera interaction runs at a time, with a five-minute deadline. Cancellation, WebView destruction, timeout and stale-session results are cleaned up or rejected. Browsers use corresponding Web APIs and return `unsupported` when unavailable.
113
+ Bridge file/camera actions are serialized with a five-minute deadline. Cancellation, WebView destruction, timeout and stale-session results are cleaned up or rejected. Standard WebView file inputs are a separate path. Since plugin 0.6.6, browser media selection waits for change, explicit cancellation, abort or the deadline rather than treating restored focus as cancellation. Unsupported browser capabilities return `unsupported`.
114
+
115
+ For image attachment diagnosis, compare the native DSH file button and camera action using a small JPEG or PNG and check the unsent draft preview. DSH keeps images in the browser draft before submission, so no new computer-side image object does not establish picker failure. Checking the selected file count/type and the draft preview is sufficient without sending a model request; generic files can use a different storage path. File selection, webpage intake and later submission are separate stages.
110
116
 
111
117
  From App 0.4.7, its native keyboard adapter uses plain Enter for a new draft line when an inset-backed on-screen keyboard is visible, Android reports no hardware keyboard, and an active session composer is focused. The Send button still submits a multiline draft. Floating keyboards, unknown state and older Apps do not use this native rule; the plugin's touch-browser rule below can still apply. Physical keyboards can use Shift+Enter for a line break.
112
118
 
package/lib/client.js CHANGED
@@ -3204,6 +3204,127 @@ window.__ModuleLoader__.load({
3204
3204
  [data-dsh-mobile-composer-model-label] { flex:1 1 auto !important; max-width:none !important; min-width:0 !important; overflow:hidden !important; text-overflow:ellipsis !important; white-space:nowrap !important; }
3205
3205
  }
3206
3206
  `;
3207
+ //#endregion
3208
+ //#region src/browser-file-picker.ts
3209
+ /** Browser chooser lifetime shared by camera controls and the extension SDK. */
3210
+ const BROWSER_FILE_PICKER_DEADLINE_MS = 3e5;
3211
+ const browserEnvironment = {
3212
+ createSource: () => {
3213
+ const input = document.createElement("input");
3214
+ return {
3215
+ input,
3216
+ open: () => {
3217
+ document.body.append(input);
3218
+ input.click();
3219
+ },
3220
+ remove: () => {
3221
+ input.remove();
3222
+ }
3223
+ };
3224
+ },
3225
+ deadline: (callback, milliseconds) => {
3226
+ const timer = window.setTimeout(callback, milliseconds);
3227
+ return () => {
3228
+ window.clearTimeout(timer);
3229
+ };
3230
+ }
3231
+ };
3232
+ /** Select files without treating focus/visibility restoration as cancellation.
3233
+ * @param options - MIME selection, lifetime and optional camera capture mode.
3234
+ * @param environment - Browser DOM and timer owner; tests may supply an owned source.
3235
+ * @returns Selected files, or undefined for explicit cancellation or deadline; abort rejects.
3236
+ */
3237
+ function pickBrowserFiles(options, environment = browserEnvironment) {
3238
+ const abortReason = () => options.abortReason?.() ?? options.signal.reason ?? new DOMException("File selection aborted", "AbortError");
3239
+ if (options.signal.aborted) return Promise.reject(abortReason());
3240
+ return new Promise((resolve, reject) => {
3241
+ let source;
3242
+ let input;
3243
+ let settled = false;
3244
+ let cancelDeadline;
3245
+ const cleanup = () => {
3246
+ const errors = [];
3247
+ const attempt = (operation) => {
3248
+ try {
3249
+ operation();
3250
+ } catch (error) {
3251
+ errors.push(error);
3252
+ }
3253
+ };
3254
+ if (cancelDeadline !== void 0) attempt(cancelDeadline);
3255
+ cancelDeadline = void 0;
3256
+ attempt(() => {
3257
+ input?.removeEventListener("change", onChange);
3258
+ });
3259
+ attempt(() => {
3260
+ input?.removeEventListener("cancel", onCancel);
3261
+ });
3262
+ attempt(() => {
3263
+ options.signal.removeEventListener("abort", onAbort);
3264
+ });
3265
+ attempt(() => {
3266
+ source?.remove();
3267
+ });
3268
+ return errors;
3269
+ };
3270
+ const finish = (outcome) => {
3271
+ if (settled) return;
3272
+ settled = true;
3273
+ const cleanupErrors = cleanup();
3274
+ if (outcome.kind === "failed") reject(outcome.error);
3275
+ else if (cleanupErrors.length > 0) reject(cleanupErrors[0]);
3276
+ else if (outcome.kind === "cancelled") resolve(void 0);
3277
+ else resolve(outcome.files);
3278
+ };
3279
+ const onChange = () => {
3280
+ if (settled || input === void 0) return;
3281
+ try {
3282
+ const files = input.files === null ? [] : [...input.files];
3283
+ finish(files.length === 0 ? { kind: "cancelled" } : {
3284
+ kind: "files",
3285
+ files: Object.freeze(files)
3286
+ });
3287
+ } catch (error) {
3288
+ finish({
3289
+ kind: "failed",
3290
+ error
3291
+ });
3292
+ }
3293
+ };
3294
+ const onCancel = () => {
3295
+ finish({ kind: "cancelled" });
3296
+ };
3297
+ const onAbort = () => {
3298
+ finish({
3299
+ kind: "failed",
3300
+ error: abortReason()
3301
+ });
3302
+ };
3303
+ try {
3304
+ source = environment.createSource();
3305
+ input = source.input;
3306
+ input.type = "file";
3307
+ input.accept = options.accept;
3308
+ if (options.capture !== void 0) input.capture = options.capture;
3309
+ input.multiple = options.multiple ?? false;
3310
+ input.hidden = true;
3311
+ input.addEventListener("change", onChange);
3312
+ input.addEventListener("cancel", onCancel);
3313
+ options.signal.addEventListener("abort", onAbort, { once: true });
3314
+ cancelDeadline = environment.deadline(onCancel, BROWSER_FILE_PICKER_DEADLINE_MS);
3315
+ if (options.signal.aborted) {
3316
+ onAbort();
3317
+ return;
3318
+ }
3319
+ source.open();
3320
+ } catch (error) {
3321
+ finish({
3322
+ kind: "failed",
3323
+ error
3324
+ });
3325
+ }
3326
+ });
3327
+ }
3207
3328
  //#endregion
3208
3329
  //#region src/native-mobile.ts
3209
3330
  /**
@@ -3624,19 +3745,66 @@ window.__ModuleLoader__.load({
3624
3745
  }
3625
3746
  };
3626
3747
  }
3627
- function controlledFileDragEvent(type, files, initialDropEffect) {
3628
- const event = new Event(type, {
3629
- bubbles: true,
3630
- cancelable: true
3748
+ /** A read-only, file-only transfer for engines without usable native constructors. */
3749
+ function controlledFileTransfer(files, initialDropEffect) {
3750
+ const snapshot = [...files];
3751
+ const fileList = Object.assign(snapshot, { item: (index) => snapshot[index] ?? null });
3752
+ const items = Object.assign(snapshot.map((file) => ({
3753
+ kind: "file",
3754
+ type: file.type,
3755
+ getAsFile: () => file,
3756
+ getAsString: () => {},
3757
+ webkitGetAsEntry: () => null
3758
+ })), {
3759
+ add: () => null,
3760
+ clear: () => {},
3761
+ remove: () => {}
3631
3762
  });
3632
- const dataTransfer = {
3763
+ Object.freeze(fileList);
3764
+ Object.freeze(items);
3765
+ return {
3633
3766
  dropEffect: initialDropEffect,
3634
3767
  effectAllowed: "copy",
3635
- files: Object.freeze([...files]),
3636
- types: Object.freeze(["Files"])
3637
- };
3638
- Object.defineProperty(event, "dataTransfer", { value: dataTransfer });
3639
- return event;
3768
+ files: fileList,
3769
+ items,
3770
+ types: Object.freeze(["Files"]),
3771
+ clearData: () => {},
3772
+ getData: () => "",
3773
+ setData: () => {},
3774
+ setDragImage: () => {}
3775
+ };
3776
+ }
3777
+ function nativeFileTransfer(files, initialDropEffect) {
3778
+ const transfer = new DataTransfer();
3779
+ for (const file of files) if (transfer.items.add(file) === null) throw new Error("file_transfer_unavailable");
3780
+ if (transfer.files.length !== files.length) throw new Error("file_transfer_unavailable");
3781
+ transfer.effectAllowed = "copy";
3782
+ transfer.dropEffect = "copy";
3783
+ if (transfer.effectAllowed !== "copy" || transfer.dropEffect !== "copy") throw new Error("file_transfer_unavailable");
3784
+ transfer.dropEffect = initialDropEffect;
3785
+ if (transfer.dropEffect !== initialDropEffect) throw new Error("file_transfer_unavailable");
3786
+ return transfer;
3787
+ }
3788
+ function controlledFileDragEvent(type, files, initialDropEffect) {
3789
+ let transfer;
3790
+ if (typeof DataTransfer === "function") try {
3791
+ transfer = nativeFileTransfer(files, initialDropEffect);
3792
+ } catch (_error) {
3793
+ transfer = controlledFileTransfer(files, initialDropEffect);
3794
+ }
3795
+ else transfer = controlledFileTransfer(files, initialDropEffect);
3796
+ if (typeof DragEvent === "function") try {
3797
+ const event = new DragEvent(type, {
3798
+ bubbles: true,
3799
+ cancelable: true,
3800
+ dataTransfer: transfer
3801
+ });
3802
+ if (event.dataTransfer !== null) return event;
3803
+ } catch (_error) {}
3804
+ return Object.assign(new Event(type, {
3805
+ bubbles: true,
3806
+ cancelable: true
3807
+ }), { dataTransfer: transfer });
3640
3808
  }
3641
3809
  /** Ask the official document dragover listener whether the current composer accepts files. */
3642
3810
  function preflightComposerImageDrop(target, files) {
@@ -4086,7 +4254,6 @@ window.__ModuleLoader__.load({
4086
4254
  const preflightFile = new File([], "dsh-mobile-preflight.png", { type: "image/png" });
4087
4255
  const canAcceptComposerDrop = () => preflightComposerImageDrop(document, [preflightFile]);
4088
4256
  const mediaPickerAbortController = new AbortController();
4089
- const browserPickerCleanups = /* @__PURE__ */ new Set();
4090
4257
  let boundEnterEditor = null;
4091
4258
  let boundEnterCard = null;
4092
4259
  let disposeEnterBinding;
@@ -4133,63 +4300,18 @@ window.__ModuleLoader__.load({
4133
4300
  };
4134
4301
  const deliverImages = (files, context) => {
4135
4302
  if (files.length === 0 || !mediaRequestIsCurrent(context)) return;
4136
- dispatchComposerImageDrop(document, files);
4303
+ if (!dispatchComposerImageDrop(document, files)) showMediaToast(label("Allegati immagine non disponibili", "Image attachments are unavailable", "图片附件不可用"));
4137
4304
  };
4138
4305
  const launchBrowserCamera = (context) => {
4139
- const input = document.createElement("input");
4140
- input.type = "file";
4141
- input.accept = "image/*";
4142
- input.capture = "environment";
4143
- input.hidden = true;
4144
- const signal = mediaPickerAbortController.signal;
4145
- let cleanupTimer = 0;
4146
- let watchdogTimer = 0;
4147
- let cleaned = false;
4148
- const scheduleCleanup = () => {
4149
- if (!cleaned && cleanupTimer === 0) cleanupTimer = window.setTimeout(cleanup, 1e3);
4150
- };
4151
- const onVisibilityChange = () => {
4152
- if (document.visibilityState === "visible") scheduleCleanup();
4153
- };
4154
- const onChange = () => {
4155
- if (cleaned) return;
4156
- const files = input.files === null ? [] : [...input.files];
4157
- cleanup();
4158
- deliverImages(files, context);
4159
- };
4160
- const cleanup = () => {
4161
- if (cleaned) return;
4162
- cleaned = true;
4163
- if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
4164
- if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
4165
- cleanupTimer = 0;
4166
- watchdogTimer = 0;
4167
- input.removeEventListener("change", onChange);
4168
- input.removeEventListener("cancel", cleanup);
4169
- window.removeEventListener("focus", scheduleCleanup);
4170
- document.removeEventListener("visibilitychange", onVisibilityChange);
4171
- signal.removeEventListener("abort", cleanup);
4172
- browserPickerCleanups.delete(cleanup);
4173
- input.remove();
4174
- };
4175
- input.addEventListener("change", onChange);
4176
- input.addEventListener("cancel", cleanup);
4177
- window.addEventListener("focus", scheduleCleanup);
4178
- document.addEventListener("visibilitychange", onVisibilityChange);
4179
- signal.addEventListener("abort", cleanup, { once: true });
4180
- watchdogTimer = window.setTimeout(cleanup, 3e5);
4181
- browserPickerCleanups.add(cleanup);
4182
- if (signal.aborted) {
4183
- cleanup();
4184
- return;
4185
- }
4186
- try {
4187
- document.body.append(input);
4188
- input.click();
4189
- } catch {
4190
- cleanup();
4191
- if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aprire la fotocamera", "Could not open the camera", "无法打开相机"));
4192
- }
4306
+ pickBrowserFiles({
4307
+ accept: "image/*",
4308
+ capture: "environment",
4309
+ signal: mediaPickerAbortController.signal
4310
+ }).then((files) => {
4311
+ if (files !== void 0) deliverImages(files, context);
4312
+ }).catch((_error) => {
4313
+ if (mediaRequestIsCurrent(context)) showMediaToast(label("Impossibile aggiungere la foto", "Could not attach the photo", "无法附加照片"));
4314
+ });
4193
4315
  };
4194
4316
  const dismissCommandMenu = () => {
4195
4317
  (boundComposer?.querySelector("button[aria-haspopup=\"listbox\"][aria-expanded=\"true\"]"))?.click();
@@ -4647,7 +4769,6 @@ window.__ModuleLoader__.load({
4647
4769
  mediaRequestGeneration += 1;
4648
4770
  mediaPickerAbortController.abort();
4649
4771
  restoreLanguageMarker();
4650
- for (const cleanup of [...browserPickerCleanups]) cleanup();
4651
4772
  disposeEnterBinding?.();
4652
4773
  disposeBrowserComposerSoftEnter();
4653
4774
  observer.disconnect();
@@ -11326,73 +11447,12 @@ window.__ModuleLoader__.load({
11326
11447
  await navigator.clipboard.writeText(typeof input === "object" && input !== null && "text" in input ? String(input.text) : "");
11327
11448
  return { ok: true };
11328
11449
  }
11329
- if (action === "files.pick" || action === "camera.capture") {
11330
- const inputElement = element("input");
11331
- inputElement.type = "file";
11332
- inputElement.accept = action === "camera.capture" ? "image/*" : "*/*";
11333
- if (action === "camera.capture") inputElement.capture = "environment";
11334
- inputElement.hidden = true;
11335
- return new Promise((resolve, reject) => {
11336
- let settled = false;
11337
- let cleaned = false;
11338
- let cleanupTimer = 0;
11339
- let watchdogTimer = 0;
11340
- const cleanup = () => {
11341
- if (cleaned) return;
11342
- cleaned = true;
11343
- if (cleanupTimer !== 0) window.clearTimeout(cleanupTimer);
11344
- if (watchdogTimer !== 0) window.clearTimeout(watchdogTimer);
11345
- window.removeEventListener("focus", scheduleCancel);
11346
- document.removeEventListener("visibilitychange", onVisibilityChange);
11347
- signal.removeEventListener("abort", onAbort);
11348
- inputElement.removeEventListener("change", onChange);
11349
- inputElement.removeEventListener("cancel", onCancel);
11350
- inputElement.remove();
11351
- };
11352
- const finish = (callback) => {
11353
- if (settled) return;
11354
- settled = true;
11355
- cleanup();
11356
- callback();
11357
- };
11358
- const onChange = () => {
11359
- const file = inputElement.files?.[0];
11360
- finish(() => {
11361
- resolve(file);
11362
- });
11363
- };
11364
- const onCancel = () => {
11365
- finish(() => {
11366
- resolve(void 0);
11367
- });
11368
- };
11369
- const onAbort = () => {
11370
- finish(() => {
11371
- reject(abortReason());
11372
- });
11373
- };
11374
- const scheduleCancel = () => {
11375
- if (!settled && cleanupTimer === 0) cleanupTimer = window.setTimeout(onCancel, 1e3);
11376
- };
11377
- const onVisibilityChange = () => {
11378
- if (document.visibilityState === "visible") scheduleCancel();
11379
- };
11380
- inputElement.addEventListener("change", onChange, { once: true });
11381
- inputElement.addEventListener("cancel", onCancel, { once: true });
11382
- window.addEventListener("focus", scheduleCancel);
11383
- document.addEventListener("visibilitychange", onVisibilityChange);
11384
- signal.addEventListener("abort", onAbort, { once: true });
11385
- watchdogTimer = window.setTimeout(onCancel, 3e5);
11386
- try {
11387
- document.body.append(inputElement);
11388
- inputElement.click();
11389
- } catch (error) {
11390
- finish(() => {
11391
- reject(error);
11392
- });
11393
- }
11394
- });
11395
- }
11450
+ if (action === "files.pick" || action === "camera.capture") return pickBrowserFiles({
11451
+ signal,
11452
+ accept: action === "camera.capture" ? "image/*" : "*/*",
11453
+ ...action === "camera.capture" ? { capture: "environment" } : {},
11454
+ abortReason
11455
+ }).then((files) => files?.[0]);
11396
11456
  throw new Error("native capability is unavailable");
11397
11457
  };
11398
11458
  const makeApi = (id, hostGeneration, controller, surfaces, surfaceIds) => {