diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml
index a6519e9..b439366 100644
--- a/apps/mobile/android/app/src/main/AndroidManifest.xml
+++ b/apps/mobile/android/app/src/main/AndroidManifest.xml
@@ -9,6 +9,16 @@
+
+
+
+
client.close());
return client;
}),
- multiImagePickerProvider.overrideWithValue(() async => picker.consume()),
+ multiImagePickerProvider.overrideWithValue((context) async => picker.consume()),
singleImagePickerProvider.overrideWithValue(() async {
final paths = picker.consume();
return paths.isEmpty ? null : paths.first;
diff --git a/apps/mobile/lib/features/uploads/enqueue.dart b/apps/mobile/lib/features/uploads/enqueue.dart
index 62f36e3..e62df7c 100644
--- a/apps/mobile/lib/features/uploads/enqueue.dart
+++ b/apps/mobile/lib/features/uploads/enqueue.dart
@@ -1,10 +1,11 @@
/// 选图注入点 + 批量入队动作(P3 计划 Task 9)。
///
-/// image_picker 的平台调用经 Provider 注入([singleImagePickerProvider]/
-/// [multiImagePickerProvider]),widget 测试 override 为替身——不经平台
-/// 通道即可驱动入队流;[enqueueForUpload] 为连拍页与相册详情 FAB 的共用
-/// 入队路径:首次动作触发 POST_NOTIFICATIONS 请求(静默降级)+ 逐张经
-/// 准备器入队(准备器内部串行复制,防连拍 IO 风暴)。
+/// 选图的平台调用经 Provider 注入([singleImagePickerProvider]=image_picker
+/// 单图;[multiImagePickerProvider]=wechat_assets_picker 应用内多选),
+/// widget 测试 override 为替身——不经平台通道即可驱动入队流;
+/// [enqueueForUpload] 为连拍页与相册详情 FAB 的共用入队路径:首次动作
+/// 触发 POST_NOTIFICATIONS 请求(静默降级)+ 批量经准备器入队(准备器
+/// 内部有界并行复制,默认并发 3,防连拍 IO 风暴)。
///
/// P0-1 层 1(UI06 分析 v2):入队前**本地查重**——同账号 + 同相册 +
/// state=uploaded + filename+bytes 匹配 → 返回 [DuplicateCheck],由
@@ -13,9 +14,11 @@ library;
import 'dart:io';
+import 'package:flutter/widgets.dart' show BuildContext, Localizations;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as p;
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import '../../core/notification_permission.dart';
import 'providers.dart';
@@ -25,7 +28,7 @@ import 'task_state.dart';
typedef SingleImagePicker = Future Function();
/// 多图选择(相册多选入队);返回源文件路径列表(空=取消)。
-typedef MultiImagePicker = Future> Function();
+typedef MultiImagePicker = Future> Function(BuildContext context);
/// 单图选择 Provider(生产=image_picker;测试 override)。
final singleImagePickerProvider = Provider(
@@ -35,14 +38,45 @@ final singleImagePickerProvider = Provider(
},
);
-/// 多图选择 Provider(生产=image_picker;测试 override)。
+/// 多图选择 Provider(生产=wechat_assets_picker;测试 override)。
+///
+/// - 应用内选择器:长按进入多选 + 拖动连选 + 全选;无系统 Photo Picker
+/// 的 100 张硬上限(软上限 9999 防极端内存,photo_manager 网格分页
+/// 加载,实际不受限);
+/// - originFile 取原始文件路径:API 30+ 直接返回原路径;Android 10 非
+/// legacy 经 ScopedCache 复制到应用缓存(较慢、占缓存空间)。路径
+/// 解析并行发起(批量数百张时避免串行平台通道往返阻塞入队启动;
+/// 平台通道调用在平台线程自然串行化,无 IO 风暴);
+/// - 单个资产解析失败(选择后被删/云下架等 PlatformException)仅跳过
+/// 该张(null 过滤),不丢弃整批;入队数由调用方 SnackBar 反映实际
+/// 张数;
+/// - 权限由 picker 内部请求;永久拒绝时 picker 自带 denied/limited 视图
+/// (非「返回空」);返回 null 仅发生在用户取消。
final multiImagePickerProvider = Provider(
- (ref) => () async {
- final files = await ImagePicker().pickMultiImage();
- return files.map((file) => file.path).toList();
+ (ref) => (context) async {
+ final locale = Localizations.maybeLocaleOf(context);
+ final assets = await AssetPicker.pickAssets(
+ context,
+ pickerConfig: AssetPickerConfig(
+ requestType: RequestType.image,
+ maxAssets: kPickerSoftMaxAssets,
+ textDelegate: assetPickerTextDelegateFromLocale(locale),
+ ),
+ );
+ if (assets == null) return const [];
+ final files = await Future.wait(
+ assets.map((a) => a.originFile.then((f) => f, onError: (_) => null)),
+ );
+ return [
+ for (final file in files)
+ if (file != null) file.path,
+ ];
},
);
+/// 选图软上限(用户要求不设硬限制;9999 为防误触极端值)。
+const int kPickerSoftMaxAssets = 9999;
+
/// 查重结果(单张):匹配到的已上传任务摘要。
class DuplicateMatch {
const DuplicateMatch({
diff --git a/apps/mobile/lib/pages/album_detail_page.dart b/apps/mobile/lib/pages/album_detail_page.dart
index 600dd5f..8fe0eaf 100644
--- a/apps/mobile/lib/pages/album_detail_page.dart
+++ b/apps/mobile/lib/pages/album_detail_page.dart
@@ -337,8 +337,8 @@ class _AlbumDetailPageState extends ConsumerState {
}
final l10n = AppLocalizations.of(context);
final account = await _currentAccountId();
- if (account == null) return;
- final paths = await ref.read(multiImagePickerProvider)();
+ if (account == null || !mounted) return;
+ final paths = await ref.read(multiImagePickerProvider)(context);
if (paths.isEmpty || !mounted) return;
final messenger = ScaffoldMessenger.of(context);
diff --git a/apps/mobile/lib/pages/capture_page.dart b/apps/mobile/lib/pages/capture_page.dart
index 5673c4f..a5f22e1 100644
--- a/apps/mobile/lib/pages/capture_page.dart
+++ b/apps/mobile/lib/pages/capture_page.dart
@@ -304,7 +304,7 @@ class _CapturePageState extends ConsumerState
/// 相册多选入队(目标 = 当前相册与分类;入口=底部缩略图位)。
Future _pickFromGallery() async {
- final paths = await ref.read(multiImagePickerProvider)();
+ final paths = await ref.read(multiImagePickerProvider)(context);
if (paths.isEmpty || !mounted) return;
final l10n = AppLocalizations.of(context);
final messenger = ScaffoldMessenger.of(context);
diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock
index e548fb8..00cf77f 100644
--- a/apps/mobile/pubspec.lock
+++ b/apps/mobile/pubspec.lock
@@ -109,26 +109,26 @@ packages:
dependency: transitive
description:
name: camera_android_camerax
- sha256: "977f54512d338b3b36d7f82929794886766d8f41ae84a48fe103eb0106ecfe25"
+ sha256: a9d527411429f97976dc1f97b0dbe57560c9d3ae482e2f45b7d0a2a61c6bf8f9
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.7.4+8"
+ version: "0.7.5"
camera_avfoundation:
dependency: transitive
description:
name: camera_avfoundation
- sha256: "843247230a583e04742ea1434ce3f91e536760922fab67c72ce46aa34fac3e9d"
+ sha256: e409a731fad3fca10daa9567cf981720398659e6390f871a4e0f576058ff8940
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.10.3"
+ version: "0.10.3+1"
camera_platform_interface:
dependency: transitive
description:
name: camera_platform_interface
- sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498"
+ sha256: "9365babc7488a9e93d0ed6dcccd917619fb12ea6f9e601519e1191d4890ee2fe"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "2.13.1"
+ version: "2.14.0"
camera_web:
dependency: transitive
description:
@@ -233,6 +233,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.7"
+ csslib:
+ dependency: transitive
+ description:
+ name: csslib
+ sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.0.2"
dart_style:
dependency: transitive
description:
@@ -281,6 +289,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.35.0"
+ extended_image:
+ dependency: transitive
+ description:
+ name: extended_image
+ sha256: e81d801cd692921f152834e01f39bba8b3d6302c48ad420d499c6062a7e24737
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "10.1.0"
+ extended_image_library:
+ dependency: transitive
+ description:
+ name: extended_image_library
+ sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "5.0.1"
fake_async:
dependency: transitive
description:
@@ -462,10 +486,10 @@ packages:
dependency: "direct main"
description:
name: fluwx
- sha256: "4b526d281be8560a490bd1b945373a23ab2a12088c8fc5997ffb31cc6fb41082"
+ sha256: "766439fadfcaf29063cd89b330953d46f395b79a8840e377a060e2f6ada9c187"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "6.0.2"
+ version: "6.0.4"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
@@ -503,6 +527,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
+ html:
+ dependency: transitive
+ description:
+ name: html
+ sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.15.7"
http:
dependency: transitive
description:
@@ -511,6 +543,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.6.0"
+ http_client_helper:
+ dependency: transitive
+ description:
+ name: http_client_helper
+ sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.0.0"
http_multi_server:
dependency: transitive
description:
@@ -563,10 +603,10 @@ packages:
dependency: transitive
description:
name: image_picker_ios
- sha256: ee3885b6fcd71958fbc79770dd194c63371439d536d69c47b279171a486482ae
+ sha256: "852a7762962f3bb3ad54ef1d342669e41557f466033a676baa8876b367cf4050"
url: "https://pub.flutter-io.cn"
source: hosted
- version: "0.8.13+7"
+ version: "0.8.13+8"
image_picker_linux:
dependency: transitive
description:
@@ -644,6 +684,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.7.2"
json_annotation:
dependency: transitive
description:
@@ -740,6 +788,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.19.2"
+ nested:
+ dependency: transitive
+ description:
+ name: nested
+ sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.0.0"
nm:
dependency: transitive
description:
@@ -828,6 +884,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.2"
+ photo_manager:
+ dependency: transitive
+ description:
+ name: photo_manager
+ sha256: "2d00a785b501bf1e5f6180bbef772846e767be11d45d11112b63092ef54a419e"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "3.12.0"
+ photo_manager_image_provider:
+ dependency: transitive
+ description:
+ name: photo_manager_image_provider
+ sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.2.0"
platform:
dependency: transitive
description:
@@ -868,6 +940,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.6"
+ provider:
+ dependency: transitive
+ description:
+ name: provider
+ sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
@@ -1217,6 +1297,54 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
+ video_player:
+ dependency: transitive
+ description:
+ name: video_player
+ sha256: "8c837b570dccb9ae6ff73d2e0b03c7e708bfefd3bd1194faa7f3e7f200dfc399"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.14.0"
+ video_player_android:
+ dependency: transitive
+ description:
+ name: video_player_android
+ sha256: d27054dea34d748a44f06d433a57005d2aac69944485b0abbaed3303dbcfa3f1
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.12.2"
+ video_player_avfoundation:
+ dependency: transitive
+ description:
+ name: video_player_avfoundation
+ sha256: "2fc56e537324a19eb649fca991aa308ac961b5406292876fa657b062de461fd8"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.12.0"
+ video_player_platform_interface:
+ dependency: transitive
+ description:
+ name: video_player_platform_interface
+ sha256: "92c0fbabe20c788e71fd10d26cea998d0d253282e65d145aed0818731cf593ce"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "6.9.0"
+ video_player_web:
+ dependency: transitive
+ description:
+ name: video_player_web
+ sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "2.4.0"
+ visibility_detector:
+ dependency: transitive
+ description:
+ name: visibility_detector
+ sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "0.4.0+2"
vm_service:
dependency: transitive
description:
@@ -1265,6 +1393,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.0"
+ wechat_assets_picker:
+ dependency: "direct main"
+ description:
+ name: wechat_assets_picker
+ sha256: ccd92f3e8c968e2cbad939bce82b454e798a7669c1e4ab74e871822ce2440b2d
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "10.1.3"
+ wechat_picker_library:
+ dependency: transitive
+ description:
+ name: wechat_picker_library
+ sha256: "5cb61b9aa935b60da5b043f8446fbb9c5077419f20ccc4856bf444aec4f44bc1"
+ url: "https://pub.flutter-io.cn"
+ source: hosted
+ version: "1.0.7"
win32:
dependency: transitive
description:
diff --git a/apps/mobile/pubspec.yaml b/apps/mobile/pubspec.yaml
index f9322c9..b5cf839 100644
--- a/apps/mobile/pubspec.yaml
+++ b/apps/mobile/pubspec.yaml
@@ -26,6 +26,8 @@ dependencies:
shared_preferences: ^2.5.3
# 选图 / 连拍
image_picker: ^1.2.3
+ # 应用内相册选择器(连选/全选/无系统 100 张上限;基于 photo_manager)
+ wechat_assets_picker: ^10.1.3
camera: ^0.12.1
# 分享二维码
qr_flutter: ^4.1.0
diff --git a/apps/mobile/test/pages/album_detail_page_test.dart b/apps/mobile/test/pages/album_detail_page_test.dart
index e981645..4f6695e 100644
--- a/apps/mobile/test/pages/album_detail_page_test.dart
+++ b/apps/mobile/test/pages/album_detail_page_test.dart
@@ -336,7 +336,7 @@ void main() {
client: clientOf(adapter),
),
multiImagePickerProvider
- .overrideWith((ref) => () async => galleryPaths),
+ .overrideWith((ref) => (context) async => galleryPaths),
uploadPreparerProvider.overrideWith(
(ref) => UploadPreparer(
taskStore,
diff --git a/apps/mobile/test/pages/capture_page_test.dart b/apps/mobile/test/pages/capture_page_test.dart
index 7e8414e..ef199f3 100644
--- a/apps/mobile/test/pages/capture_page_test.dart
+++ b/apps/mobile/test/pages/capture_page_test.dart
@@ -177,7 +177,7 @@ void main() {
(ref) => NotificationPermission(isAndroid: false),
),
multiImagePickerProvider
- .overrideWith((ref) => () async => galleryPaths),
+ .overrideWith((ref) => (context) async => galleryPaths),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,