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 +64 -0
- package/dist/metro.d.mts +17 -1
- package/dist/metro.d.ts +17 -1
- package/dist/metro.js +25 -2
- package/dist/metro.mjs +26 -4
- package/dist/stubs/expo-image-picker-stub.js +241 -0
- package/package.json +2 -1
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
|
|
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:
|
|
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
|
|
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
|
|
421
|
+
import path13 from "path";
|
|
402
422
|
import { Logger } from "metro-core";
|
|
403
423
|
function installPerfLogger(logFile, existingFactory) {
|
|
404
|
-
const dir =
|
|
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:
|
|
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.
|
|
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",
|