feat(app): in-app asset picker (wechat_assets_picker) - unlimited multi-select with range selection
Review fixes: per-asset originFile error isolation (failed asset skipped, not whole batch); drop maxSdkVersion from READ_MEDIA_VISUAL_USER_SELECTED (API 34+, no upper bound); originFile doc accuracy (API 30+ direct path vs Android 10 ScopedCache copy). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,16 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<!-- 应用内相册选择器(wechat_assets_picker/photo_manager):图片读取。
|
||||
READ_MEDIA_VISUAL_USER_SELECTED:API 34 引入、无上限,勿加
|
||||
maxSdkVersion——加了会在 Android 15+(targetSdk 36)被裁掉,
|
||||
部分访问的「选择照片」入口随之消失,picker limited/manage 流程失效。 -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"/>
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
android:label="photo_live"
|
||||
|
||||
@@ -93,7 +93,7 @@ class ItAppHarness {
|
||||
ref.onDispose(() => 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;
|
||||
|
||||
@@ -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<String?> Function();
|
||||
|
||||
/// 多图选择(相册多选入队);返回源文件路径列表(空=取消)。
|
||||
typedef MultiImagePicker = Future<List<String>> Function();
|
||||
typedef MultiImagePicker = Future<List<String>> Function(BuildContext context);
|
||||
|
||||
/// 单图选择 Provider(生产=image_picker;测试 override)。
|
||||
final singleImagePickerProvider = Provider<SingleImagePicker>(
|
||||
@@ -35,14 +38,45 @@ final singleImagePickerProvider = Provider<SingleImagePicker>(
|
||||
},
|
||||
);
|
||||
|
||||
/// 多图选择 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<MultiImagePicker>(
|
||||
(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 <String>[];
|
||||
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({
|
||||
|
||||
@@ -337,8 +337,8 @@ class _AlbumDetailPageState extends ConsumerState<AlbumDetailPage> {
|
||||
}
|
||||
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);
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ class _CapturePageState extends ConsumerState<CapturePage>
|
||||
|
||||
/// 相册多选入队(目标 = 当前相册与分类;入口=底部缩略图位)。
|
||||
Future<void> _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);
|
||||
|
||||
+154
-10
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -336,7 +336,7 @@ void main() {
|
||||
client: clientOf(adapter),
|
||||
),
|
||||
multiImagePickerProvider
|
||||
.overrideWith((ref) => () async => galleryPaths),
|
||||
.overrideWith((ref) => (context) async => galleryPaths),
|
||||
uploadPreparerProvider.overrideWith(
|
||||
(ref) => UploadPreparer(
|
||||
taskStore,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user