miaoda-expo-devkit 0.1.1-beta.49 → 0.1.1-beta.50

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 CHANGED
@@ -10,6 +10,7 @@ Expo / React Native 开发环境工具集,通过 Metro 构建层注入以下
10
10
  - **expo-notifications stub** — Expo Go(Android)中提供 no-op 实现,核心 API 调用时弹出带参数校验的调试 Alert,Dev Build 透传真实模块
11
11
  - **expo-media-library stub** — Expo Go / Web 中提供 no-op 实现,`saveToLibraryAsync`、`createAssetAsync`、权限请求等 API 调用时弹出 Alert 提示,Dev Build 原生环境透传真实模块
12
12
  - **expo-calendar stub** — Expo Go / Web 中提供 no-op 实现,`getEventsAsync`、`createEventAsync`、`getCalendarsAsync`、权限请求等核心 API 调用时弹出 Alert 提示并校验参数,Dev Build 原生环境透传真实模块
13
+ - **expo-image-picker stub** — 桌面 Web 中 `launchCameraAsync` 通过 `getUserMedia` 打开摄像头预览弹窗(浏览器原生 `capture` 属性在 PC 端被忽略),移动端浏览器透传 expo 原实现,Native 不受影响
13
14
 
14
15
  ## 安装
15
16
 
@@ -249,6 +250,14 @@ metro.config.js
249
250
  │ ├─ Expo Go / Web:no-op + Alert 提示 + 参数校验(不崩溃)
250
251
  │ └─ Dev Build(原生):透传真实 expo-calendar
251
252
 
253
+ ├─ withExpoImagePickerStub → resolver.resolveRequest
254
+ │ └─ expo-image-picker → dist/stubs/expo-image-picker-stub.js (仅 web)
255
+ │ ├─ 桌面 Web:launchCameraAsync → getUserMedia + #__devkit_camera_overlay__ 弹窗
256
+ │ │ ├─ 点"拍照":canvas.toDataURL → ImagePickerResult { canceled:false, assets }
257
+ │ │ └─ 点"取消":返回 { canceled:true, assets:null }
258
+ │ ├─ 移动端浏览器:透传 expo 原实现(capture 属性正常工作)
259
+ │ └─ getUserMedia 不可用:降级透传 expo 原实现(不崩溃)
260
+
252
261
  └─ withEntryInjection → resolver.resolveRequest
253
262
  └─ expo-router/entry-classic → dist/stubs/expo-router-entry-stub.js
254
263
  ├─ require('./entry-inject') ← 注入脚本(bundle 首部执行)
@@ -288,6 +297,7 @@ sentry-react-native-stub.js
288
297
  | `./expo-notifications-stub` | `dist/stubs/expo-notifications-stub.js` | `expo-notifications` Expo Go Android stub |
289
298
  | `./expo-media-library-stub` | `dist/stubs/expo-media-library-stub.js` | `expo-media-library` Expo Go / Web stub |
290
299
  | `./expo-calendar-stub` | `dist/stubs/expo-calendar-stub.js` | `expo-calendar` Expo Go / Web stub |
300
+ | `./expo-image-picker-stub` | `dist/stubs/expo-image-picker-stub.js` | `expo-image-picker` 桌面 Web stub |
291
301
 
292
302
  ---
293
303
 
@@ -495,6 +505,60 @@ Expo Go 扫码预览不支持访问手机相册
495
505
 
496
506
  ---
497
507
 
508
+ ---
509
+
510
+ ### expo-image-picker-stub.js
511
+
512
+ `expo-image-picker` 模块替换 stub,由 `withExpoImagePickerStub()` 在 Metro 层注入(**仅 web 平台**)。
513
+
514
+ **背景:** `expo-image-picker` 的 `launchCameraAsync` 在 web 上底层使用 `<input type="file" capture="environment">`。桌面浏览器(Chrome、Firefox、Safari)**有意忽略 `capture` 属性**,直接弹出文件选择框而不是摄像头。这是浏览器厂商的硬限制,无法通过任何 HTML 属性或 meta 标签绕过。
515
+
516
+ **运行时行为:**
517
+ - **桌面 Web(非 mobile UA)**:`launchCameraAsync` 调用 `navigator.mediaDevices.getUserMedia` 打开摄像头预览弹窗(`#__devkit_camera_overlay__`),用户点击"📷 拍照"后截取一帧并以 `data:image/jpeg` 格式返回,点击"取消"返回 `{ canceled: true }`
518
+ - **移动端浏览器(Android Chrome / iOS Safari)**:透传 expo 原实现,`capture` 属性在移动端正常调起系统相机
519
+ - **`getUserMedia` 不可用**(无摄像头或浏览器限制):静默降级,透传 expo 原实现(不崩溃)
520
+ - **Native(iOS / Android)**:Metro resolver 不拦截,直接使用原生 `expo-image-picker`,功能完全不变
521
+
522
+ **弹窗 DOM 结构(供 Playwright 定位):**
523
+ ```html
524
+ <div id="__devkit_camera_overlay__"> <!-- 全屏遮罩 -->
525
+ <div> <!-- 面板 -->
526
+ <p>拍照</p>
527
+ <video autoplay muted playsinline> <!-- 摄像头预览 -->
528
+ <div>
529
+ <button>📷 拍照</button>
530
+ <button>取消</button>
531
+ </div>
532
+ <p><!-- 错误信息(getUserMedia 失败时显示) --></p>
533
+ </div>
534
+ </div>
535
+ ```
536
+
537
+ **返回值格式(与 expo 原实现兼容):**
538
+ ```ts
539
+ // 拍照成功
540
+ {
541
+ canceled: false,
542
+ assets: [{
543
+ uri: 'data:image/jpeg;base64,...',
544
+ width: 640,
545
+ height: 480,
546
+ type: 'image',
547
+ fileName: 'photo_1234567890.jpg',
548
+ mimeType: 'image/jpeg',
549
+ base64: null,
550
+ exif: null,
551
+ }]
552
+ }
553
+
554
+ // 取消
555
+ { canceled: true, assets: null }
556
+ ```
557
+
558
+ **E2E 测试:** `devkit-e2e/tests/e2e/image-picker-stub.spec.ts`,使用 `addInitScript` mock `getUserMedia`(注入 canvas stream 替代真实摄像头),覆盖弹窗出现/消失、拍照返回结果、取消、降级等场景。
559
+
560
+ ---
561
+
498
562
  ## Babel 插件:jsx-source
499
563
 
500
564
  `babel-plugin-jsx-source` 为 JSX 元素注入 source 属性(文件路径、行列号),用于开发调试。通过 `dataSet` 对象注入,这是 React Web 可识别的数据通道。
package/dist/metro.d.mts CHANGED
@@ -120,6 +120,7 @@ declare function withRouteEndpoint(config: MetroConfig, options: RouteEndpointOp
120
120
  * withExpoMediaLibraryStub — Web / Expo Go:expo-media-library → stub(弹 Alert 提示,Dev Build 透传)
121
121
  * withExpoCalendarStub — Web / Expo Go:expo-calendar → stub(弹 Alert 提示,Dev Build 透传)
122
122
  * withExpoFileSystemStub — Web:expo-file-system/legacy → stub(弹 Dialog 提示,原生透传)
123
+ * withExpoImagePickerStub — Web 桌面:launchCameraAsync 用 getUserMedia 唤起摄像头
123
124
  * withEntryInjection — 在 expo-router 启动前注入脚本(仅 __DEV__)
124
125
  * withDevStubs — 替换 Sentry DSN / 屏蔽 LogBox(仅 __DEV__)
125
126
  * withRouteEndpoint — 添加 /__routes 端点(仅 __DEV__)
@@ -313,6 +314,21 @@ declare function withExpoCalendarStub(config: MetroConfig): MetroConfig;
313
314
  */
314
315
  declare function withExpoFileSystemStub(config: MetroConfig): MetroConfig;
315
316
 
317
+ /**
318
+ * withExpoImagePickerStub — Web 平台将 expo-image-picker 替换为支持 getUserMedia 的 stub
319
+ *
320
+ * 问题:expo-image-picker 的 launchCameraAsync 在 web 上通过
321
+ * <input type="file" capture="environment"> 打开摄像头,但桌面浏览器忽略 capture 属性,
322
+ * 直接弹出文件选择框,无法唤起摄像头。
323
+ *
324
+ * 此 wrapper 仅在 web 平台拦截 expo-image-picker,注入 stub:
325
+ * - 桌面 Web:launchCameraAsync 使用 getUserMedia + 预览弹窗,真正唤起摄像头
326
+ * - 移动 Web:stub 内部检测到移动端,自动透传回 expo 原实现
327
+ * - Native:不拦截,原生实现完全正常
328
+ */
329
+
330
+ declare function withExpoImagePickerStub(config: MetroConfig): MetroConfig;
331
+
316
332
  /**
317
333
  * 将 Metro transformer 的 minifier 切换为 esbuild。
318
334
  *
@@ -382,4 +398,4 @@ declare function withWasmSupport(config: MetroConfig): MetroConfig;
382
398
  */
383
399
  declare function withTransformLogger(config: MetroConfig): MetroConfig;
384
400
 
385
- export { type DevkitOptions, type InjectOptions, type PatchNativeWindCacheOptions, type RouteEndpointOptions, patchNativeWindCachePath, withCssInterop, withDevStubs, withDevkit, withEntryInjection, withEsbuildMinify, withExpoCalendarStub, withExpoFileSystemStub, withExpoMediaLibraryStub, withExpoNotificationsStub, withLucideResolver, withNativeWind, withRouteEndpoint, withTransformLogger, withWasmSupport, withWorkspaceNodeModules };
401
+ export { type DevkitOptions, type InjectOptions, type PatchNativeWindCacheOptions, type RouteEndpointOptions, patchNativeWindCachePath, withCssInterop, withDevStubs, withDevkit, withEntryInjection, withEsbuildMinify, withExpoCalendarStub, withExpoFileSystemStub, withExpoImagePickerStub, withExpoMediaLibraryStub, withExpoNotificationsStub, withLucideResolver, withNativeWind, withRouteEndpoint, withTransformLogger, withWasmSupport, withWorkspaceNodeModules };
package/dist/metro.d.ts CHANGED
@@ -120,6 +120,7 @@ declare function withRouteEndpoint(config: MetroConfig, options: RouteEndpointOp
120
120
  * withExpoMediaLibraryStub — Web / Expo Go:expo-media-library → stub(弹 Alert 提示,Dev Build 透传)
121
121
  * withExpoCalendarStub — Web / Expo Go:expo-calendar → stub(弹 Alert 提示,Dev Build 透传)
122
122
  * withExpoFileSystemStub — Web:expo-file-system/legacy → stub(弹 Dialog 提示,原生透传)
123
+ * withExpoImagePickerStub — Web 桌面:launchCameraAsync 用 getUserMedia 唤起摄像头
123
124
  * withEntryInjection — 在 expo-router 启动前注入脚本(仅 __DEV__)
124
125
  * withDevStubs — 替换 Sentry DSN / 屏蔽 LogBox(仅 __DEV__)
125
126
  * withRouteEndpoint — 添加 /__routes 端点(仅 __DEV__)
@@ -313,6 +314,21 @@ declare function withExpoCalendarStub(config: MetroConfig): MetroConfig;
313
314
  */
314
315
  declare function withExpoFileSystemStub(config: MetroConfig): MetroConfig;
315
316
 
317
+ /**
318
+ * withExpoImagePickerStub — Web 平台将 expo-image-picker 替换为支持 getUserMedia 的 stub
319
+ *
320
+ * 问题:expo-image-picker 的 launchCameraAsync 在 web 上通过
321
+ * <input type="file" capture="environment"> 打开摄像头,但桌面浏览器忽略 capture 属性,
322
+ * 直接弹出文件选择框,无法唤起摄像头。
323
+ *
324
+ * 此 wrapper 仅在 web 平台拦截 expo-image-picker,注入 stub:
325
+ * - 桌面 Web:launchCameraAsync 使用 getUserMedia + 预览弹窗,真正唤起摄像头
326
+ * - 移动 Web:stub 内部检测到移动端,自动透传回 expo 原实现
327
+ * - Native:不拦截,原生实现完全正常
328
+ */
329
+
330
+ declare function withExpoImagePickerStub(config: MetroConfig): MetroConfig;
331
+
316
332
  /**
317
333
  * 将 Metro transformer 的 minifier 切换为 esbuild。
318
334
  *
@@ -382,4 +398,4 @@ declare function withWasmSupport(config: MetroConfig): MetroConfig;
382
398
  */
383
399
  declare function withTransformLogger(config: MetroConfig): MetroConfig;
384
400
 
385
- export { type DevkitOptions, type InjectOptions, type PatchNativeWindCacheOptions, type RouteEndpointOptions, patchNativeWindCachePath, withCssInterop, withDevStubs, withDevkit, withEntryInjection, withEsbuildMinify, withExpoCalendarStub, withExpoFileSystemStub, withExpoMediaLibraryStub, withExpoNotificationsStub, withLucideResolver, withNativeWind, withRouteEndpoint, withTransformLogger, withWasmSupport, withWorkspaceNodeModules };
401
+ export { type DevkitOptions, type InjectOptions, type PatchNativeWindCacheOptions, type RouteEndpointOptions, patchNativeWindCachePath, withCssInterop, withDevStubs, withDevkit, withEntryInjection, withEsbuildMinify, withExpoCalendarStub, withExpoFileSystemStub, withExpoImagePickerStub, withExpoMediaLibraryStub, withExpoNotificationsStub, withLucideResolver, withNativeWind, withRouteEndpoint, withTransformLogger, withWasmSupport, withWorkspaceNodeModules };
package/dist/metro.js CHANGED
@@ -38,6 +38,7 @@ __export(metro_exports, {
38
38
  withEsbuildMinify: () => withEsbuildMinify,
39
39
  withExpoCalendarStub: () => withExpoCalendarStub,
40
40
  withExpoFileSystemStub: () => withExpoFileSystemStub,
41
+ withExpoImagePickerStub: () => withExpoImagePickerStub,
41
42
  withExpoMediaLibraryStub: () => withExpoMediaLibraryStub,
42
43
  withExpoNotificationsStub: () => withExpoNotificationsStub,
43
44
  withLucideResolver: () => withLucideResolver,
@@ -288,7 +289,7 @@ function withCssInterop(config) {
288
289
  }
289
290
 
290
291
  // src/metro/withDevkit.ts
291
- var import_path12 = __toESM(require("path"));
292
+ var import_path13 = __toESM(require("path"));
292
293
 
293
294
  // src/metro/withEsbuildMinify.ts
294
295
  function withEsbuildMinify(config) {
@@ -427,6 +428,26 @@ function withExpoFileSystemStub(config) {
427
428
  return { ...config, resolver: { ...config.resolver, resolveRequest } };
428
429
  }
429
430
 
431
+ // src/metro/withExpoImagePickerStub.ts
432
+ var import_path12 = __toESM(require("path"));
433
+ var EXPO_IMAGE_PICKER_STUB_FILENAME = "expo-image-picker-stub.js";
434
+ var EXPO_IMAGE_PICKER_STUB_PATH = import_path12.default.resolve(
435
+ __dirname,
436
+ "stubs",
437
+ EXPO_IMAGE_PICKER_STUB_FILENAME
438
+ );
439
+ function withExpoImagePickerStub(config) {
440
+ const upstream = config.resolver?.resolveRequest ?? null;
441
+ const resolveRequest = (context, moduleName, platform) => {
442
+ if (platform === "web" && moduleName === "expo-image-picker" && !context.originModulePath.includes(EXPO_IMAGE_PICKER_STUB_FILENAME)) {
443
+ return { filePath: EXPO_IMAGE_PICKER_STUB_PATH, type: "sourceFile" };
444
+ }
445
+ if (upstream) return upstream(context, moduleName, platform);
446
+ return context.resolveRequest(context, moduleName, platform);
447
+ };
448
+ return { ...config, resolver: { ...config.resolver, resolveRequest } };
449
+ }
450
+
430
451
  // src/metro/withWasmSupport.ts
431
452
  function withWasmSupport(config) {
432
453
  const existing = config.resolver?.assetExts ?? [];
@@ -533,10 +554,11 @@ function withDevkit(config, options = {}) {
533
554
  config = withExpoMediaLibraryStub(config);
534
555
  config = withExpoCalendarStub(config);
535
556
  config = withExpoFileSystemStub(config);
557
+ config = withExpoImagePickerStub(config);
536
558
  if (typeof __DEV__ !== "undefined" && __DEV__) {
537
559
  config = withEntryInjection(config);
538
560
  config = withDevStubs(config);
539
- config = withRouteEndpoint(config, { appDir: import_path12.default.join(projectRoot, "src", "app") });
561
+ config = withRouteEndpoint(config, { appDir: import_path13.default.join(projectRoot, "src", "app") });
540
562
  }
541
563
  return withNativeWind(config, { input, inlineRem: 16 });
542
564
  }
@@ -558,6 +580,7 @@ try {
558
580
  withEsbuildMinify,
559
581
  withExpoCalendarStub,
560
582
  withExpoFileSystemStub,
583
+ withExpoImagePickerStub,
561
584
  withExpoMediaLibraryStub,
562
585
  withExpoNotificationsStub,
563
586
  withLucideResolver,
package/dist/metro.mjs CHANGED
@@ -244,7 +244,7 @@ function withCssInterop(config) {
244
244
  }
245
245
 
246
246
  // src/metro/withDevkit.ts
247
- import path13 from "path";
247
+ import path14 from "path";
248
248
 
249
249
  // src/metro/withEsbuildMinify.ts
250
250
  function withEsbuildMinify(config) {
@@ -383,6 +383,26 @@ function withExpoFileSystemStub(config) {
383
383
  return { ...config, resolver: { ...config.resolver, resolveRequest } };
384
384
  }
385
385
 
386
+ // src/metro/withExpoImagePickerStub.ts
387
+ import path12 from "path";
388
+ var EXPO_IMAGE_PICKER_STUB_FILENAME = "expo-image-picker-stub.js";
389
+ var EXPO_IMAGE_PICKER_STUB_PATH = path12.resolve(
390
+ __dirname,
391
+ "stubs",
392
+ EXPO_IMAGE_PICKER_STUB_FILENAME
393
+ );
394
+ function withExpoImagePickerStub(config) {
395
+ const upstream = config.resolver?.resolveRequest ?? null;
396
+ const resolveRequest = (context, moduleName, platform) => {
397
+ if (platform === "web" && moduleName === "expo-image-picker" && !context.originModulePath.includes(EXPO_IMAGE_PICKER_STUB_FILENAME)) {
398
+ return { filePath: EXPO_IMAGE_PICKER_STUB_PATH, type: "sourceFile" };
399
+ }
400
+ if (upstream) return upstream(context, moduleName, platform);
401
+ return context.resolveRequest(context, moduleName, platform);
402
+ };
403
+ return { ...config, resolver: { ...config.resolver, resolveRequest } };
404
+ }
405
+
386
406
  // src/metro/withWasmSupport.ts
387
407
  function withWasmSupport(config) {
388
408
  const existing = config.resolver?.assetExts ?? [];
@@ -398,10 +418,10 @@ function withWasmSupport(config) {
398
418
 
399
419
  // src/metro/withTransformLogger.ts
400
420
  import fs4 from "fs";
401
- import path12 from "path";
421
+ import path13 from "path";
402
422
  import { Logger } from "metro-core";
403
423
  function installPerfLogger(logFile, existingFactory) {
404
- const dir = path12.dirname(logFile);
424
+ const dir = path13.dirname(logFile);
405
425
  if (!fs4.existsSync(dir)) {
406
426
  fs4.mkdirSync(dir, { recursive: true });
407
427
  }
@@ -489,10 +509,11 @@ function withDevkit(config, options = {}) {
489
509
  config = withExpoMediaLibraryStub(config);
490
510
  config = withExpoCalendarStub(config);
491
511
  config = withExpoFileSystemStub(config);
512
+ config = withExpoImagePickerStub(config);
492
513
  if (typeof __DEV__ !== "undefined" && __DEV__) {
493
514
  config = withEntryInjection(config);
494
515
  config = withDevStubs(config);
495
- config = withRouteEndpoint(config, { appDir: path13.join(projectRoot, "src", "app") });
516
+ config = withRouteEndpoint(config, { appDir: path14.join(projectRoot, "src", "app") });
496
517
  }
497
518
  return withNativeWind(config, { input, inlineRem: 16 });
498
519
  }
@@ -513,6 +534,7 @@ export {
513
534
  withEsbuildMinify,
514
535
  withExpoCalendarStub,
515
536
  withExpoFileSystemStub,
537
+ withExpoImagePickerStub,
516
538
  withExpoMediaLibraryStub,
517
539
  withExpoNotificationsStub,
518
540
  withLucideResolver,
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ var import_react_native = require("react-native");
3
+ function isDesktopBrowser() {
4
+ if (typeof navigator === "undefined") return false;
5
+ const ua = navigator.userAgent;
6
+ const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(ua);
7
+ return !isMobile;
8
+ }
9
+ function showMobilePassthroughToast() {
10
+ const toast = document.createElement("div");
11
+ toast.style.cssText = [
12
+ "position:fixed",
13
+ "bottom:24px",
14
+ "left:50%",
15
+ "transform:translateX(-50%)",
16
+ "z-index:99999",
17
+ "background:#1a1a1a",
18
+ "color:#a78bfa",
19
+ "font-size:12px",
20
+ "font-family:monospace",
21
+ "padding:8px 14px",
22
+ "border-radius:6px",
23
+ "border:1px solid #4c1d95",
24
+ "pointer-events:none",
25
+ "white-space:nowrap"
26
+ ].join(";");
27
+ toast.textContent = "[miaoda-expo-devkit] \u79FB\u52A8\u7AEF\u68C0\u6D4B\u5230\uFF0C\u5DF2\u900F\u4F20\u771F\u5B9E expo-image-picker\uFF0C\u4E0D\u4F1A\u5F39\u51FA\u8C03\u8BD5\u754C\u9762";
28
+ document.body.appendChild(toast);
29
+ setTimeout(() => {
30
+ if (document.body.contains(toast)) document.body.removeChild(toast);
31
+ }, 2500);
32
+ }
33
+ function openCameraDialog(facingMode) {
34
+ return new Promise((resolve) => {
35
+ const overlay = document.createElement("div");
36
+ overlay.id = "__devkit_camera_overlay__";
37
+ overlay.style.cssText = [
38
+ "position:fixed",
39
+ "inset:0",
40
+ "z-index:99999",
41
+ "background:rgba(0,0,0,0.85)",
42
+ "display:flex",
43
+ "align-items:center",
44
+ "justify-content:center"
45
+ ].join(";");
46
+ const panel = document.createElement("div");
47
+ panel.style.cssText = [
48
+ "background:#1a1a1a",
49
+ "border-radius:12px",
50
+ "overflow:hidden",
51
+ "display:flex",
52
+ "flex-direction:column",
53
+ "align-items:stretch",
54
+ "max-width:90vw",
55
+ "max-height:90vh",
56
+ "box-shadow:0 8px 32px rgba(0,0,0,0.6)",
57
+ "border:1px solid #4c1d95"
58
+ ].join(";");
59
+ const header = document.createElement("div");
60
+ header.style.cssText = [
61
+ "background:#2e1065",
62
+ "padding:12px 16px 10px",
63
+ "display:flex",
64
+ "flex-direction:column",
65
+ "gap:4px"
66
+ ].join(";");
67
+ const headerTitle = document.createElement("div");
68
+ headerTitle.textContent = "\u{1F4F7} PC \u7AEF\u6444\u50CF\u5934\u8C03\u8BD5\u754C\u9762";
69
+ headerTitle.style.cssText = "color:#e9d5ff;font-size:14px;font-weight:600;font-family:sans-serif";
70
+ const headerDesc = document.createElement("div");
71
+ headerDesc.textContent = "\u7531\u4E8E PC \u6D4F\u89C8\u5668\u65E0\u6CD5\u76F4\u63A5\u8C03\u8D77\u7CFB\u7EDF\u76F8\u673A\uFF0C\u6B64\u754C\u9762\u4F1A\u5728\u5F00\u53D1\u8C03\u8BD5\u671F\u95F4\u81EA\u52A8\u51FA\u73B0\uFF0C\u8BA9\u4F60\u53EF\u4EE5\u5728 PC \u4E0A\u6A21\u62DF\u62CD\u7167\u6D41\u7A0B\u3002\u79FB\u52A8\u7AEF\u548C\u771F\u673A\u4E0D\u4F1A\u6709\u8FD9\u4E2A\u5F39\u7A97\uFF0C\u4F1A\u76F4\u63A5\u8C03\u8D77\u7CFB\u7EDF\u539F\u751F\u76F8\u673A\u3002\u6B64\u754C\u9762\u6837\u5F0F\u56FA\u5B9A\uFF0C\u8BF7\u52FF\u5C1D\u8BD5\u4FEE\u6539\u5916\u89C2\u2014\u2014\u4EFB\u4F55\u4FEE\u6539\u90FD\u4E0D\u4F1A\u751F\u6548\u3002";
72
+ headerDesc.style.cssText = "color:#a78bfa;font-size:11px;font-family:sans-serif;line-height:1.6;max-width:480px";
73
+ header.appendChild(headerTitle);
74
+ header.appendChild(headerDesc);
75
+ const body = document.createElement("div");
76
+ body.style.cssText = [
77
+ "display:flex",
78
+ "flex-direction:column",
79
+ "align-items:center",
80
+ "padding:16px",
81
+ "gap:12px"
82
+ ].join(";");
83
+ const video = document.createElement("video");
84
+ video.autoplay = true;
85
+ video.playsInline = true;
86
+ video.muted = true;
87
+ video.style.cssText = [
88
+ "width:100%",
89
+ "max-width:480px",
90
+ "border-radius:8px",
91
+ "background:#000"
92
+ ].join(";");
93
+ const btnRow = document.createElement("div");
94
+ btnRow.style.cssText = "display:flex;gap:12px;width:100%;justify-content:center";
95
+ const btnCapture = document.createElement("button");
96
+ btnCapture.textContent = "\u{1F4F7} \u62CD\u7167";
97
+ btnCapture.style.cssText = [
98
+ "padding:10px 28px",
99
+ "border-radius:8px",
100
+ "border:none",
101
+ "cursor:pointer",
102
+ "background:#7C3AED",
103
+ "color:#fff",
104
+ "font-size:15px",
105
+ "font-weight:600",
106
+ "font-family:sans-serif"
107
+ ].join(";");
108
+ const btnCancel = document.createElement("button");
109
+ btnCancel.textContent = "\u53D6\u6D88";
110
+ btnCancel.style.cssText = [
111
+ "padding:10px 20px",
112
+ "border-radius:8px",
113
+ "border:1px solid #555",
114
+ "cursor:pointer",
115
+ "background:transparent",
116
+ "color:#ccc",
117
+ "font-size:15px",
118
+ "font-family:sans-serif"
119
+ ].join(";");
120
+ const errorMsg = document.createElement("p");
121
+ errorMsg.style.cssText = "margin:0;color:#f87171;font-size:13px;font-family:sans-serif;display:none";
122
+ btnRow.appendChild(btnCapture);
123
+ btnRow.appendChild(btnCancel);
124
+ body.appendChild(video);
125
+ body.appendChild(btnRow);
126
+ body.appendChild(errorMsg);
127
+ panel.appendChild(header);
128
+ panel.appendChild(body);
129
+ overlay.appendChild(panel);
130
+ document.body.appendChild(overlay);
131
+ let stream = null;
132
+ function cleanup() {
133
+ stream?.getTracks().forEach((t) => t.stop());
134
+ if (document.body.contains(overlay)) document.body.removeChild(overlay);
135
+ }
136
+ navigator.mediaDevices.getUserMedia({ video: { facingMode }, audio: false }).then((s) => {
137
+ stream = s;
138
+ video.srcObject = s;
139
+ }).catch((err) => {
140
+ errorMsg.textContent = `\u65E0\u6CD5\u8BBF\u95EE\u6444\u50CF\u5934\uFF1A${err.message ?? err}`;
141
+ errorMsg.style.display = "block";
142
+ btnCapture.disabled = true;
143
+ });
144
+ btnCapture.addEventListener("click", () => {
145
+ const canvas = document.createElement("canvas");
146
+ canvas.width = video.videoWidth || 640;
147
+ canvas.height = video.videoHeight || 480;
148
+ const ctx = canvas.getContext("2d");
149
+ if (!ctx) {
150
+ cleanup();
151
+ resolve({ canceled: true });
152
+ return;
153
+ }
154
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
155
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.92);
156
+ cleanup();
157
+ resolve({ canceled: false, dataUrl, mimeType: "image/jpeg" });
158
+ });
159
+ btnCancel.addEventListener("click", () => {
160
+ cleanup();
161
+ resolve({ canceled: true });
162
+ });
163
+ });
164
+ }
165
+ async function dataUrlToPickerResult(dataUrl) {
166
+ return new Promise((resolve) => {
167
+ const img = new Image();
168
+ img.onload = () => {
169
+ resolve({
170
+ canceled: false,
171
+ assets: [
172
+ {
173
+ uri: dataUrl,
174
+ width: img.naturalWidth || img.width,
175
+ height: img.naturalHeight || img.height,
176
+ type: "image",
177
+ fileName: `photo_${Date.now()}.jpg`,
178
+ mimeType: "image/jpeg",
179
+ base64: null,
180
+ exif: null,
181
+ duration: null,
182
+ rotation: null,
183
+ assetId: null,
184
+ pairedVideoAsset: null
185
+ }
186
+ ]
187
+ });
188
+ };
189
+ img.onerror = () => {
190
+ resolve({
191
+ canceled: false,
192
+ assets: [
193
+ {
194
+ uri: dataUrl,
195
+ width: 0,
196
+ height: 0,
197
+ type: "image",
198
+ fileName: `photo_${Date.now()}.jpg`,
199
+ mimeType: "image/jpeg",
200
+ base64: null,
201
+ exif: null,
202
+ duration: null,
203
+ rotation: null,
204
+ assetId: null,
205
+ pairedVideoAsset: null
206
+ }
207
+ ]
208
+ });
209
+ };
210
+ img.src = dataUrl;
211
+ });
212
+ }
213
+ function getFacingMode(options) {
214
+ const cameraType = options.cameraType;
215
+ return cameraType === "front" ? "user" : "environment";
216
+ }
217
+ if (import_react_native.Platform.OS === "web") {
218
+ const ExpoImagePicker = require("expo-image-picker");
219
+ const patchedLaunchCameraAsync = async (options = {}) => {
220
+ if (!isDesktopBrowser()) {
221
+ showMobilePassthroughToast();
222
+ return ExpoImagePicker.launchCameraAsync(options);
223
+ }
224
+ if (typeof navigator?.mediaDevices?.getUserMedia !== "function") {
225
+ return ExpoImagePicker.launchCameraAsync(options);
226
+ }
227
+ const facingMode = getFacingMode(options);
228
+ const result = await openCameraDialog(facingMode);
229
+ if (result.canceled || !result.dataUrl) {
230
+ return { canceled: true, assets: null };
231
+ }
232
+ return dataUrlToPickerResult(result.dataUrl);
233
+ };
234
+ module.exports = {
235
+ ...ExpoImagePicker,
236
+ launchCameraAsync: patchedLaunchCameraAsync
237
+ };
238
+ } else {
239
+ module.exports = require("expo-image-picker");
240
+ }
241
+ //# sourceMappingURL=expo-image-picker-stub.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-expo-devkit",
3
- "version": "0.1.1-beta.49",
3
+ "version": "0.1.1-beta.50",
4
4
  "description": "Expo 应用开发工具集:Sentry DSN 替换 stub、错误/网络捕获、Metro 符号化",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -47,6 +47,7 @@
47
47
  "./expo-notifications-stub": "./dist/stubs/expo-notifications-stub.js",
48
48
  "./expo-media-library-stub": "./dist/stubs/expo-media-library-stub.js",
49
49
  "./expo-calendar-stub": "./dist/stubs/expo-calendar-stub.js",
50
+ "./expo-image-picker-stub": "./dist/stubs/expo-image-picker-stub.js",
50
51
  "./rules/no-undeclared-expo-plugin": "./dist/rules/no-undeclared-expo-plugin.js",
51
52
  "./rules/no-unused-expo-plugin": "./dist/rules/no-unused-expo-plugin.js",
52
53
  "./rules/no-unstable-expo-router": "./dist/rules/no-unstable-expo-router.js",