From 8cf919283b536557cfb32dec7a400381e68e2897 Mon Sep 17 00:00:00 2001 From: jingyun <> Date: Tue, 23 Jun 2026 10:07:35 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E5=A2=9E=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=80=89=E6=8B=A9=E5=92=8C=E6=8B=8D=E7=85=A7=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/AndroidManifest.xml | 4 + ios/Podfile | 8 + ios/Podfile.lock | 35 +- ios/Runner.xcodeproj/project.pbxproj | 18 + ios/Runner/Info.plist | 6 + .../services/device_header_service.dart | 16 + lib/common/upload/upload_repository.dart | 20 + lib/common/upload/upload_service.dart | 13 + lib/common/util/image_util.dart | 119 ++++++ lib/features/assets/data/asset_models.dart | 163 +++++++ .../assets/presentation/asset_add_page.dart | 402 ++++++++++++++++-- .../assets/presentation/asset_edit_page.dart | 1 + .../asset_image_preview_page.dart | 95 +++++ .../presentation/holding_detail_page.dart | 136 +++++- pubspec.lock | 290 ++++++++++++- pubspec.yaml | 7 + 16 files changed, 1288 insertions(+), 45 deletions(-) create mode 100644 lib/common/upload/upload_repository.dart create mode 100644 lib/common/upload/upload_service.dart create mode 100644 lib/common/util/image_util.dart create mode 100644 lib/features/assets/presentation/asset_image_preview_page.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c9894ec..313899d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,9 @@ + + + + diff --git a/ios/Podfile b/ios/Podfile index 620e46e..d8ca124 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -39,5 +39,13 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + 'PERMISSION_CAMERA=1', + 'PERMISSION_PHOTOS=1', + ] + end end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e60e3ec..165f08a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,16 +11,29 @@ PODS: - FlutterMacOS - fluttertoast (0.0.2): - Flutter + - gal (1.0.0): + - Flutter + - FlutterMacOS + - image_picker_ios (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.4.8): + - Flutter + - photo_manager (3.9.0): + - Flutter + - FlutterMacOS - platform_metadata (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS - url_launcher_ios (0.0.1): - Flutter - video_player_avfoundation (0.0.1): @@ -34,10 +47,15 @@ DEPENDENCIES: - flutter_paid (from `.symlinks/plugins/flutter_paid/ios`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - gal (from `.symlinks/plugins/gal/darwin`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - photo_manager (from `.symlinks/plugins/photo_manager/darwin`) - platform_metadata (from `.symlinks/plugins/platform_metadata/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) @@ -54,14 +72,24 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" + gal: + :path: ".symlinks/plugins/gal/darwin" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + photo_manager: + :path: ".symlinks/plugins/photo_manager/darwin" platform_metadata: :path: ".symlinks/plugins/platform_metadata/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: @@ -74,13 +102,18 @@ SPEC CHECKSUMS: flutter_paid: 6055d15f439071fefb849e3cebc279915ed70db5 flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + gal: baecd024ebfd13c441269ca7404792a7152fde89 + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e + photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb platform_metadata: a4d1e9569219bb2dcbb54510554611df032ea023 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a -PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e +PODFILE CHECKSUM: b6c30c22d7ac3adf21bf40601823fcf685f5b3d6 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1e95057..35a4fa2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -198,6 +198,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 38380B0F0A71332BD02F1E6B /* [CP] Embed Pods Frameworks */, + EA4FE8E3948A035FEE08B35D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -361,6 +362,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + EA4FE8E3948A035FEE08B35D /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 00af064..dfdde91 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -56,5 +56,11 @@ UIViewControllerBasedStatusBarAppearance + NSCameraUsageDescription + 用于为贵金属持仓添加发票、证书及实物照片。 + NSPhotoLibraryUsageDescription + 用于为贵金属持仓添加发票、证书及实物照片。 + NSPhotoLibraryAddUsageDescription + 用于为贵金属持仓添加发票、证书及实物照片。 diff --git a/lib/common/services/device_header_service.dart b/lib/common/services/device_header_service.dart index 65002e9..eb56c72 100644 --- a/lib/common/services/device_header_service.dart +++ b/lib/common/services/device_header_service.dart @@ -43,6 +43,8 @@ class DeviceHeaderService { String _androidOaid = ''; String _androidImei = ''; String _androidMac = ''; + IosDeviceInfo? iosDeviceInfo; + AndroidDeviceInfo? androidDeviceInfo; Future? _baseInitialization; Future? _identityInitialization; @@ -120,9 +122,23 @@ class DeviceHeaderService { _iosIdfv = await _resolveIdfv(); await _collectIosPaidIfMissing(); } + await getDeviceInfo(); } catch (_) {} } + Future getDeviceInfo() async { + try { + final deviceInfoPlugin = DeviceInfoPlugin(); + if (Platform.isIOS) { + iosDeviceInfo = await deviceInfoPlugin.iosInfo; + } else if (Platform.isAndroid) { + androidDeviceInfo = await deviceInfoPlugin.androidInfo; + } + } catch (e) { + print('$e'); + } + } + Future _collectAndroidIdIfAgreed() async { if (_androidId.isNotEmpty) return; if (!StorageService.to.getBool(storagePrivacyAgreed)) return; // 隐私同意后才采集 diff --git a/lib/common/upload/upload_repository.dart b/lib/common/upload/upload_repository.dart new file mode 100644 index 0000000..8b4105b --- /dev/null +++ b/lib/common/upload/upload_repository.dart @@ -0,0 +1,20 @@ +import '../../features/assets/data/asset_models.dart'; + +class UploadRepository { + const UploadRepository(); + + Future uploadAssetImage(AssetImage image) async { + // TODO: Connect to upload backend. + return UploadResult( + remoteUrl: image.remoteUrl, + thumbnailUrl: image.thumbnailUrl, + ); + } +} + +class UploadResult { + const UploadResult({this.remoteUrl, this.thumbnailUrl}); + + final String? remoteUrl; + final String? thumbnailUrl; +} diff --git a/lib/common/upload/upload_service.dart b/lib/common/upload/upload_service.dart new file mode 100644 index 0000000..7ed4247 --- /dev/null +++ b/lib/common/upload/upload_service.dart @@ -0,0 +1,13 @@ +import '../../features/assets/data/asset_models.dart'; +import 'upload_repository.dart'; + +class UploadService { + const UploadService({required this.repository}); + + final UploadRepository repository; + + Future uploadAssetImage(AssetImage image) async { + // Placeholder: future implementation will upload the file and return URLs. + return repository.uploadAssetImage(image); + } +} diff --git a/lib/common/util/image_util.dart b/lib/common/util/image_util.dart new file mode 100644 index 0000000..f4b7729 --- /dev/null +++ b/lib/common/util/image_util.dart @@ -0,0 +1,119 @@ +import 'dart:io'; +import 'dart:ui' as ui; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:gal/gal.dart'; +import 'package:http/http.dart' as http; +import 'package:jinzhi_flutter_app/common/services/device_header_service.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class ImageUtil { + static const String _albumName = 'jinzhi'; + + static Stream downloadImage(String url, String key) { + return DefaultCacheManager().getFileStream( + url, + key: key, + withProgress: true, + ); + } + + static Future downloadImageInFile(String url, String key) async { + final file = await DefaultCacheManager().getSingleFile(url, key: key); + return file.path; + } + + static Future requestForImagePermission() async { + if (Platform.isIOS) { + PermissionStatus photoStatus = await Permission.photosAddOnly.status; + if (photoStatus != PermissionStatus.granted) { + photoStatus = await Permission.photosAddOnly.request(); + if (photoStatus != PermissionStatus.granted) { + return false; + } + } + } else if (Platform.isAndroid) { + int? sdkInt = DeviceHeaderService.to.androidDeviceInfo?.version.sdkInt; + if (sdkInt != null && sdkInt >= 33) { + // ✅ Android 13+ 不需要读权限,直接返回 true + // gal 插件内部通过 MediaStore 写入 + return true; + } else { + // ✅ Android 10-12 仍可能需要 storage 权限 + final status = await Permission.storage.request(); + return status.isGranted; + } + } else { + return false; + } + return true; + } + + static Future saveImage(FileInfo fileInfo) async { + await Gal.putImage(fileInfo.file.path, album: _albumName); + } + + static Future saveImageData(ByteData byteData) async { + Uint8List imageBytes = byteData.buffer.asUint8List(); + await Gal.putImageBytes( + imageBytes, + name: 'XWallpaper_share', + album: _albumName, + ); + } + + static Future saveImageUint8List(Uint8List imageBytes) async { + await Gal.putImageBytes( + imageBytes, + name: 'XWallpaper_share', + album: _albumName, + ); + } + + static Future saveUIImageData(ui.Image image) async { + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + Uint8List imageBytes = byteData!.buffer.asUint8List(); + await Gal.putImageBytes( + imageBytes, + name: 'XWallpaper_share', + album: _albumName, + ); + } + + static Future createTransparentImage(double aspectRatio) async { + double minSize = 100; + + double width = minSize * aspectRatio; + double height = minSize; + + final recorder = PictureRecorder(); + final canvas = Canvas( + recorder, + Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()), + ); + final paint = Paint()..color = Colors.transparent; + canvas.drawRect( + Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()), + paint, + ); + + final picture = recorder.endRecording(); + final img = await picture.toImage(width.toInt(), height.toInt()); + final pngBytes = await img.toByteData(format: ImageByteFormat.png); + + return pngBytes!.buffer.asUint8List(); + } + + // 下载图片并返回 Uint8List(简单实现) + static Future fetchThumbBytes(String? url) async { + if (url == null || url.isEmpty) return null; + try { + final resp = await http.get(Uri.parse(url)); + if (resp.statusCode == 200) return resp.bodyBytes; + } catch (_) {} + return null; + } +} diff --git a/lib/features/assets/data/asset_models.dart b/lib/features/assets/data/asset_models.dart index c2e00a6..00bdef6 100644 --- a/lib/features/assets/data/asset_models.dart +++ b/lib/features/assets/data/asset_models.dart @@ -1,5 +1,58 @@ import '../../../common/domain/metal_type.dart'; +class AssetImage { + const AssetImage({ + required this.localId, + required this.localPath, + required this.createdAt, + this.remoteUrl, + this.thumbnailUrl, + }); + + final String localId; + final String localPath; + final String? remoteUrl; + final String? thumbnailUrl; + final DateTime createdAt; + + AssetImage copyWith({ + String? localId, + String? localPath, + String? remoteUrl, + String? thumbnailUrl, + DateTime? createdAt, + }) { + return AssetImage( + localId: localId ?? this.localId, + localPath: localPath ?? this.localPath, + remoteUrl: remoteUrl ?? this.remoteUrl, + thumbnailUrl: thumbnailUrl ?? this.thumbnailUrl, + createdAt: createdAt ?? this.createdAt, + ); + } + + Map toJson() { + return { + 'localId': localId, + 'localPath': localPath, + 'remoteUrl': remoteUrl, + 'thumbnailUrl': thumbnailUrl, + 'createdAt': createdAt.toIso8601String(), + }; + } + + factory AssetImage.fromJson(Map json) { + return AssetImage( + localId: json['localId'] as String? ?? '', + localPath: json['localPath'] as String? ?? '', + remoteUrl: json['remoteUrl'] as String?, + thumbnailUrl: json['thumbnailUrl'] as String?, + createdAt: DateTime.tryParse(json['createdAt'] as String? ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0), + ); + } +} + enum AssetCategory { bar('金条'), necklace('项链'), @@ -28,6 +81,7 @@ class GoldAssetHolding { required this.weightGram, required this.createdAt, required this.updatedAt, + this.images = const [], this.costAmount, this.purchaseDate, this.channel, @@ -42,6 +96,7 @@ class GoldAssetHolding { final double purity; final String purityLabel; final double weightGram; + final List images; final double? costAmount; final DateTime? purchaseDate; final String? channel; @@ -50,7 +105,92 @@ class GoldAssetHolding { final DateTime updatedAt; final HoldingStatus status; + GoldAssetHolding copyWith({ + String? id, + String? name, + MetalType? metal, + AssetCategory? category, + double? purity, + String? purityLabel, + double? weightGram, + List? images, + double? costAmount, + DateTime? purchaseDate, + String? channel, + String? note, + DateTime? createdAt, + DateTime? updatedAt, + HoldingStatus? status, + }) { + return GoldAssetHolding( + id: id ?? this.id, + name: name ?? this.name, + metal: metal ?? this.metal, + category: category ?? this.category, + purity: purity ?? this.purity, + purityLabel: purityLabel ?? this.purityLabel, + weightGram: weightGram ?? this.weightGram, + images: images ?? this.images, + costAmount: costAmount ?? this.costAmount, + purchaseDate: purchaseDate ?? this.purchaseDate, + channel: channel ?? this.channel, + note: note ?? this.note, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + status: status ?? this.status, + ); + } + double materialValue(double spotPrice) => weightGram * purity * spotPrice; + + Map toJson() { + return { + 'id': id, + 'name': name, + 'metalId': metal.id, + 'categoryId': category.name, + 'purity': purity, + 'purityLabel': purityLabel, + 'weightGram': weightGram, + 'images': images.map((item) => item.toJson()).toList(growable: false), + 'costAmount': costAmount, + 'purchaseDate': purchaseDate?.toIso8601String(), + 'channel': channel, + 'note': note, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + 'status': status.name, + }; + } + + factory GoldAssetHolding.fromJson(Map json) { + final rawImages = json['images']; + return GoldAssetHolding( + id: json['id'] as String? ?? '', + name: json['name'] as String? ?? '', + metal: _metalFromId(json['metalId'] as String?), + category: _categoryFromId(json['categoryId'] as String?), + purity: (json['purity'] as num?)?.toDouble() ?? 1, + purityLabel: json['purityLabel'] as String? ?? '', + weightGram: (json['weightGram'] as num?)?.toDouble() ?? 0, + images: rawImages is List + ? [ + for (final raw in rawImages) + if (raw is Map) + AssetImage.fromJson(Map.from(raw)), + ] + : const [], + costAmount: (json['costAmount'] as num?)?.toDouble(), + purchaseDate: DateTime.tryParse(json['purchaseDate'] as String? ?? ''), + channel: json['channel'] as String?, + note: json['note'] as String?, + createdAt: DateTime.tryParse(json['createdAt'] as String? ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0), + updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? '') ?? + DateTime.fromMillisecondsSinceEpoch(0), + status: _statusFromName(json['status'] as String?), + ); + } } class HoldingValuation { @@ -100,3 +240,26 @@ class PortfolioSummary { final List breakdowns; final List holdings; } + +MetalType _metalFromId(String? id) { + return switch (id) { + 'gold' => MetalType.gold, + 'platinum' => MetalType.platinum, + 'silver' => MetalType.silver, + _ => MetalType.gold, + }; +} + +AssetCategory _categoryFromId(String? id) { + return AssetCategory.values.firstWhere( + (item) => item.name == id, + orElse: () => AssetCategory.bar, + ); +} + +HoldingStatus _statusFromName(String? value) { + return HoldingStatus.values.firstWhere( + (item) => item.name == value, + orElse: () => HoldingStatus.active, + ); +} diff --git a/lib/features/assets/presentation/asset_add_page.dart b/lib/features/assets/presentation/asset_add_page.dart index a60a4f7..cf7c7d9 100644 --- a/lib/features/assets/presentation/asset_add_page.dart +++ b/lib/features/assets/presentation/asset_add_page.dart @@ -1,6 +1,13 @@ -import 'package:flutter/material.dart'; +import 'dart:io'; + +import 'package:flutter/material.dart' hide AssetImage; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:uuid/uuid.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import '../../../common/domain/metal_type.dart'; import '../../../common/domain/money.dart'; @@ -43,6 +50,16 @@ class _AssetAddPageState extends ConsumerState { final _noteCtrl = TextEditingController(); final _dateCtrl = TextEditingController(text: '2025-08-12'); final _channelCtrl = TextEditingController(text: '银行'); + final _picker = ImagePicker(); + final _images = []; + final _uuid = const Uuid(); + static const _maxImages = 9; + + int get _remainingImageSlots => _maxImages - _images.length; + + bool get _isImageLimitReached => _remainingImageSlots <= 0; + + String get _imageLimitText => '最多可添加 $_maxImages 张图片'; @override void dispose() { @@ -91,6 +108,345 @@ class _AssetAddPageState extends ConsumerState { }; } + Future pickDate(BuildContext context) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now(), + locale: const Locale('zh'), + ); + + if (picked != null) { + print(picked.toString()); + } + } + + Future _showImageSourceSheet() async { + if (!mounted) return; + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + ), + builder: (ctx) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Container( + width: 38, + height: 4, + decoration: BoxDecoration( + color: const Color(0xFFE3DFD4), + borderRadius: BorderRadius.circular(999), + ), + ), + const SizedBox(height: 14), + ListTile( + leading: const Icon(Icons.camera_alt_outlined), + title: const Text('拍照'), + onTap: () { + Navigator.of(ctx).pop(); + _handleTakePhoto(); + }, + ), + ListTile( + leading: const Icon(Icons.photo_library_outlined), + title: const Text('从相册选择'), + onTap: () { + Navigator.of(ctx).pop(); + _handlePickFromGallery(); + }, + ), + const SizedBox(height: 8), + ListTile( + title: const Text( + '取消', + textAlign: TextAlign.center, + style: TextStyle(color: Color(0xFF8A8780)), + ), + onTap: () => Navigator.of(ctx).pop(), + ), + ], + ), + ), + ); + } + + Future _handleTakePhoto() async { + final granted = await _requestCameraPermission(); + if (!granted) { + _showPermissionDeniedDialog('相机'); + return; + } + final file = await _picker.pickImage( + source: ImageSource.camera, + imageQuality: 80, + maxWidth: 2048, + ); + if (file == null) return; + await _addPickedFile(file); + } + + Future _handlePickFromGallery() async { + final availableSlots = _remainingImageSlots; + if (availableSlots <= 0) return; + + final assets = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + maxAssets: availableSlots, + requestType: RequestType.image, + themeColor: const Color(0xFFB5862A), + ), + ); + if (assets == null || assets.isEmpty) { + return; + } + + final pickedImages = []; + for (final asset in assets.take(availableSlots)) { + final file = await asset.file; + if (file == null) continue; + pickedImages.add(await _assetImageFromSourceFile(file)); + } + if (pickedImages.isEmpty || !mounted) return; + + setState(() { + _images.addAll(pickedImages); + }); + } + + Future _assetImageFromXFile(XFile file) { + return _assetImageFromSourceFile(File(file.path)); + } + + Future _assetImageFromSourceFile(File sourceFile) async { + final localId = + 'img${DateTime.now().microsecondsSinceEpoch}${_uuid.v4().split('-').first}'; + final extension = _extensionForPath(sourceFile.path); + final directory = await getApplicationSupportDirectory(); + final imageDirectory = Directory('${directory.path}/asset_images'); + if (!await imageDirectory.exists()) { + await imageDirectory.create(recursive: true); + } + final copiedFile = await sourceFile.copy( + '${imageDirectory.path}/$localId$extension', + ); + + return AssetImage( + localId: localId, + localPath: copiedFile.path, + createdAt: DateTime.now(), + ); + } + + String _extensionForPath(String path) { + final fileName = path.split(Platform.pathSeparator).last; + final dotIndex = fileName.lastIndexOf('.'); + if (dotIndex <= 0 || dotIndex == fileName.length - 1) { + return '.jpg'; + } + return fileName.substring(dotIndex); + } + + Future _requestCameraPermission() async { + final status = await Permission.camera.status; + if (status.isGranted) return true; + final result = await Permission.camera.request(); + return result.isGranted; + } + + void _showPermissionDeniedDialog(String target) { + if (!mounted) return; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('权限不足'), + content: Text('无法访问$target,请在系统设置中开启权限。'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + openAppSettings(); + }, + child: const Text('去设置'), + ), + ], + ), + ); + } + + Future _addPickedFile(XFile file) async { + if (_images.length >= _maxImages) return; + final image = await _assetImageFromXFile(file); + if (!mounted) return; + setState(() { + _images.add(image); + }); + } + + void _removeImage(String localId) { + setState(() { + _images.removeWhere((item) => item.localId == localId); + }); + } + + Widget _buildImagesSection() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFECEAE3), width: 0.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_images.isEmpty) ...[ + GestureDetector( + onTap: _showImageSourceSheet, + child: Row( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(11), + border: Border.all( + color: const Color(0xFFD3CDBE), + width: 0.5, + ), + ), + child: const Icon( + Icons.camera_alt_outlined, + color: Color(0xFFB3AFA5), + ), + ), + const SizedBox(width: 10), + const Expanded( + child: Text( + '添加发票 / 实物照片(选填,仅存本机)', + style: TextStyle(fontSize: 12, color: Color(0xFFB3AFA5)), + ), + ), + ], + ), + ), + const SizedBox(height: 8), + const Text( + '支持发票、收据、金条证书、实物照片。', + style: TextStyle(fontSize: 12, color: Color(0xFFB3AFA5)), + ), + ] else ...[ + const Text( + '凭证照片', + style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final image in _images) _buildImageTile(image), + if (!_isImageLimitReached) + GestureDetector( + onTap: _showImageSourceSheet, + child: Container( + width: 84, + height: 84, + decoration: BoxDecoration( + color: const Color(0xFFF6ECD6), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.add, color: Color(0xFFB5862A)), + SizedBox(height: 5), + Text( + '继续补充', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 11, + color: Color(0xFFB5862A), + ), + ), + ], + ), + ), + ), + if (_isImageLimitReached) + Container( + width: 84, + height: 84, + decoration: BoxDecoration( + color: const Color(0xFFF2F0EA), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: const Color(0xFFE3DFD4), + width: 0.5, + ), + ), + alignment: Alignment.center, + child: const Text( + '已满 9 张', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 11, color: Color(0xFF8A8780)), + ), + ), + ], + ), + const SizedBox(height: 10), + Text( + _imageLimitText, + style: const TextStyle(fontSize: 12, color: Color(0xFFB3AFA5)), + ), + ], + ], + ), + ); + } + + Widget _buildImageTile(AssetImage image) { + return Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.file( + File(image.localPath), + width: 84, + height: 84, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 6, + right: 6, + child: GestureDetector( + onTap: () => _removeImage(image.localId), + child: Container( + width: 22, + height: 22, + decoration: const BoxDecoration( + color: Color(0xAA000000), + shape: BoxShape.circle, + ), + child: const Icon(Icons.close, size: 14, color: Colors.white), + ), + ), + ), + ], + ); + } + @override Widget build(BuildContext context) { final cs = ShadTheme.of(context).colorScheme; @@ -404,48 +760,9 @@ class _AssetAddPageState extends ConsumerState { ), ), const SizedBox(height: 10), - Container( - padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFFECEAE3), - width: 0.5, - ), - ), - child: Row( - children: [ - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(11), - border: Border.all( - color: const Color(0xFFD3CDBE), - width: 0.5, - ), - ), - child: const Icon( - Icons.camera_alt_outlined, - color: Color(0xFFB3AFA5), - ), - ), - const SizedBox(width: 10), - const Expanded( - child: Text( - '添加发票 / 实物照片(选填,仅存本机)', - style: TextStyle( - fontSize: 12, - color: Color(0xFFB3AFA5), - ), - ), - ), - ], - ), - ), + _buildImagesSection(), + const SizedBox(height: 16), ], - const SizedBox(height: 16), SizedBox( width: double.infinity, child: FilledButton( @@ -468,6 +785,7 @@ class _AssetAddPageState extends ConsumerState { note: _noteCtrl.text.trim().isEmpty ? null : _noteCtrl.text.trim(), + images: List.unmodifiable(_images), createdAt: DateTime.now(), updatedAt: DateTime.now(), ); diff --git a/lib/features/assets/presentation/asset_edit_page.dart b/lib/features/assets/presentation/asset_edit_page.dart index 3e804f6..dac4684 100644 --- a/lib/features/assets/presentation/asset_edit_page.dart +++ b/lib/features/assets/presentation/asset_edit_page.dart @@ -277,6 +277,7 @@ class _AssetEditPageState extends ConsumerState { purity: _holding.purity, purityLabel: _holding.purityLabel, weightGram: grams ?? _holding.weightGram, + images: _holding.images, costAmount: cost, purchaseDate: parsedDate, channel: _channelCtrl.text.trim().isEmpty diff --git a/lib/features/assets/presentation/asset_image_preview_page.dart b/lib/features/assets/presentation/asset_image_preview_page.dart new file mode 100644 index 0000000..616b62f --- /dev/null +++ b/lib/features/assets/presentation/asset_image_preview_page.dart @@ -0,0 +1,95 @@ +import 'dart:io'; + +import 'package:flutter/material.dart' hide AssetImage; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; + +import '../data/asset_models.dart'; + +class AssetImagePreviewPage extends StatefulWidget { + const AssetImagePreviewPage({ + super.key, + required this.images, + required this.initialIndex, + }); + + final List images; + final int initialIndex; + + @override + State createState() => _AssetImagePreviewPageState(); +} + +class _AssetImagePreviewPageState extends State { + late final PageController _pageController; + late int _currentIndex; + + @override + void initState() { + super.initState(); + _currentIndex = widget.initialIndex.clamp(0, widget.images.length - 1); + _pageController = PageController(initialPage: _currentIndex); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: Stack( + children: [ + PhotoViewGallery.builder( + pageController: _pageController, + itemCount: widget.images.length, + builder: (context, index) { + final image = widget.images[index]; + return PhotoViewGalleryPageOptions( + imageProvider: FileImage(File(image.localPath)), + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 2.2, + ); + }, + onPageChanged: (index) => setState(() { + _currentIndex = index; + }), + backgroundDecoration: const BoxDecoration(color: Colors.black), + ), + Positioned( + top: 16, + left: 16, + child: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ), + Positioned( + bottom: 24, + left: 0, + right: 0, + child: Center( + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + '${_currentIndex + 1}/${widget.images.length}', + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/assets/presentation/holding_detail_page.dart b/lib/features/assets/presentation/holding_detail_page.dart index 5b0d80f..aded490 100644 --- a/lib/features/assets/presentation/holding_detail_page.dart +++ b/lib/features/assets/presentation/holding_detail_page.dart @@ -1,4 +1,6 @@ -import 'package:flutter/material.dart'; +import 'dart:io'; + +import 'package:flutter/material.dart' hide AssetImage; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; @@ -10,6 +12,7 @@ import '../../profile/application/price_color_theme.dart'; import '../../profile/application/profile_settings_controller.dart'; import '../application/asset_portfolio_controller.dart'; import '../data/asset_models.dart'; +import 'asset_image_preview_page.dart'; class HoldingDetailPage extends ConsumerWidget { const HoldingDetailPage({super.key, this.id}); @@ -240,6 +243,8 @@ class HoldingDetailPage extends ConsumerWidget { ), ), const SizedBox(height: 12), + _buildImageSection(context, ref, holding), + const SizedBox(height: 12), Row( children: [ Expanded( @@ -362,6 +367,135 @@ class HoldingDetailPage extends ConsumerWidget { ).showSnackBar(const SnackBar(content: Text('已标记卖出 / 转赠'))); Navigator.of(context).pop(); } + + Widget _buildImageSection( + BuildContext context, + WidgetRef ref, + GoldAssetHolding holding, + ) { + final images = holding.images; + return PrototypeCard( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '凭证照片', + style: TextStyle(fontSize: 13, color: Color(0xFF8A8780)), + ), + const SizedBox(height: 12), + if (images.isEmpty) + const Text( + '暂无凭证照片,可在添加资产时上传发票、证书或实物照片。', + style: TextStyle(fontSize: 12, color: Color(0xFFB3AFA5)), + ) + else + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (var index = 0; index < images.length; index++) + _buildDetailImageTile( + context, + ref, + holding, + images[index], + index, + ), + ], + ), + ], + ), + ); + } + + Widget _buildDetailImageTile( + BuildContext context, + WidgetRef ref, + GoldAssetHolding holding, + AssetImage image, + int index, + ) { + return GestureDetector( + onTap: () => _openImagePreview(context, holding.images, index), + onLongPress: () => _confirmDeleteImage(context, ref, holding, image), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Stack( + children: [ + Image.file( + File(image.localPath), + width: 84, + height: 84, + fit: BoxFit.cover, + ), + Positioned( + right: 4, + top: 4, + child: Container( + width: 20, + height: 20, + decoration: const BoxDecoration( + color: Color(0xAA000000), + shape: BoxShape.circle, + ), + child: const Icon(Icons.remove, size: 14, color: Colors.white), + ), + ), + ], + ), + ), + ); + } + + void _openImagePreview( + BuildContext context, + List images, + int initialIndex, + ) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => AssetImagePreviewPage( + images: images, + initialIndex: initialIndex, + ), + ), + ); + } + + void _confirmDeleteImage( + BuildContext context, + WidgetRef ref, + GoldAssetHolding holding, + AssetImage image, + ) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('删除照片'), + content: const Text('确认删除这张凭证照片吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + final updated = holding.copyWith( + images: List.unmodifiable( + holding.images.where((item) => item.localId != image.localId).toList(), + ), + updatedAt: DateTime.now(), + ); + ref.read(assetPortfolioControllerProvider.notifier).updateHolding(updated); + }, + child: const Text('删除'), + ), + ], + ), + ); + } } class _ActionMenuTile extends StatelessWidget { diff --git a/pubspec.lock b/pubspec.lock index df3df76..1fb2477 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -215,6 +215,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: transitive description: @@ -359,6 +367,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: "direct main" description: @@ -380,6 +420,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_dotenv: dependency: "direct main" description: @@ -440,6 +488,14 @@ packages: relative: true source: path version: "0.0.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" flutter_remix: dependency: "direct main" description: @@ -554,6 +610,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + gal: + dependency: "direct main" + description: + name: gal + sha256: "969598f986789127fd407a750413249e1352116d4c2be66e81837ffeeaafdfee" + url: "https://pub.dev" + source: hosted + version: "2.3.2" glob: dependency: transitive description: @@ -658,6 +722,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f + url: "https://pub.dev" + source: hosted + version: "0.8.13+17" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + url: "https://pub.dev" + source: hosted + version: "0.8.13+3" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" intl: dependency: transitive description: @@ -802,6 +930,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_config: dependency: transitive description: @@ -843,7 +979,7 @@ packages: source: hosted version: "1.1.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -890,6 +1026,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420" + url: "https://pub.dev" + source: hosted + version: "9.4.10" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -898,6 +1082,30 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.2" + photo_manager: + dependency: transitive + description: + name: photo_manager + sha256: fb3bc8ea653370f88742b3baa304700107c83d12748aa58b2b9f2ed3ef15e6c2 + url: "https://pub.dev" + source: hosted + version: "3.9.0" + photo_manager_image_provider: + dependency: transitive + description: + name: photo_manager_image_provider + sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f + url: "https://pub.dev" + source: hosted + version: "2.2.0" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.dev" + source: hosted + version: "0.15.0" platform: dependency: transitive description: @@ -947,6 +1155,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -1176,6 +1392,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -1216,6 +1472,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -1424,6 +1688,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: @@ -1464,6 +1736,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + wechat_assets_picker: + dependency: "direct main" + description: + name: wechat_assets_picker + sha256: e2e447c12a6254e523b83415deb0b77953a96eafcb4586901b7d93039d2ca9d6 + url: "https://pub.dev" + source: hosted + version: "10.1.2" + wechat_picker_library: + dependency: transitive + description: + name: wechat_picker_library + sha256: "5cb61b9aa935b60da5b043f8446fbb9c5077419f20ccc4856bf444aec4f44bc1" + url: "https://pub.dev" + source: hosted + version: "1.0.7" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8907a20..0d312c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,13 @@ dependencies: video_player: ^2.9.2 # 启动页视频背景 # 工具 url_launcher: ^6.3.1 + gal: ^2.3.2 + image_picker: ^1.1.2 + wechat_assets_picker: ^10.1.2 + flutter_cache_manager: ^3.4.1 + photo_view: ^0.15.0 + path_provider: ^2.1.5 + permission_handler: ^11.3.1 fancy_dio_inspector: ^2.0.1 cupertino_icons: ^1.0.8 android_id: