fix:增加图片选择和拍照功能

This commit is contained in:
jingyun
2026-06-23 10:07:35 +08:00
parent 75ba309e8c
commit 8cf919283b
16 changed files with 1288 additions and 45 deletions
+4
View File
@@ -1,5 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<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" />
<!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />--> <!-- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />-->
<!-- <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />--> <!-- <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />-->
+8
View File
@@ -39,5 +39,13 @@ end
post_install do |installer| post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(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
end end
+34 -1
View File
@@ -11,16 +11,29 @@ PODS:
- FlutterMacOS - FlutterMacOS
- fluttertoast (0.0.2): - fluttertoast (0.0.2):
- Flutter - Flutter
- gal (1.0.0):
- Flutter
- FlutterMacOS
- image_picker_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.4.8):
- Flutter
- photo_manager (3.9.0):
- Flutter
- FlutterMacOS
- platform_metadata (0.0.1): - platform_metadata (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- video_player_avfoundation (0.0.1): - video_player_avfoundation (0.0.1):
@@ -34,10 +47,15 @@ DEPENDENCIES:
- flutter_paid (from `.symlinks/plugins/flutter_paid/ios`) - flutter_paid (from `.symlinks/plugins/flutter_paid/ios`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - 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`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - 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`) - platform_metadata (from `.symlinks/plugins/platform_metadata/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - 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`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
@@ -54,14 +72,24 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
fluttertoast: fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :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: platform_metadata:
:path: ".symlinks/plugins/platform_metadata/ios" :path: ".symlinks/plugins/platform_metadata/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation: video_player_avfoundation:
@@ -74,13 +102,18 @@ SPEC CHECKSUMS:
flutter_paid: 6055d15f439071fefb849e3cebc279915ed70db5 flutter_paid: 6055d15f439071fefb849e3cebc279915ed70db5
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
gal: baecd024ebfd13c441269ca7404792a7152fde89
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
platform_metadata: a4d1e9569219bb2dcbb54510554611df032ea023 platform_metadata: a4d1e9569219bb2dcbb54510554611df032ea023
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e PODFILE CHECKSUM: b6c30c22d7ac3adf21bf40601823fcf685f5b3d6
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2
+18
View File
@@ -198,6 +198,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
38380B0F0A71332BD02F1E6B /* [CP] Embed Pods Frameworks */, 38380B0F0A71332BD02F1E6B /* [CP] Embed Pods Frameworks */,
EA4FE8E3948A035FEE08B35D /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -361,6 +362,23 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 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 */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
+6
View File
@@ -56,5 +56,11 @@
<true/> <true/>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>NSCameraUsageDescription</key>
<string>用于为贵金属持仓添加发票、证书及实物照片。</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>用于为贵金属持仓添加发票、证书及实物照片。</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>用于为贵金属持仓添加发票、证书及实物照片。</string>
</dict> </dict>
</plist> </plist>
@@ -43,6 +43,8 @@ class DeviceHeaderService {
String _androidOaid = ''; String _androidOaid = '';
String _androidImei = ''; String _androidImei = '';
String _androidMac = ''; String _androidMac = '';
IosDeviceInfo? iosDeviceInfo;
AndroidDeviceInfo? androidDeviceInfo;
Future<void>? _baseInitialization; Future<void>? _baseInitialization;
Future<void>? _identityInitialization; Future<void>? _identityInitialization;
@@ -120,9 +122,23 @@ class DeviceHeaderService {
_iosIdfv = await _resolveIdfv(); _iosIdfv = await _resolveIdfv();
await _collectIosPaidIfMissing(); await _collectIosPaidIfMissing();
} }
await getDeviceInfo();
} catch (_) {} } catch (_) {}
} }
Future<void> 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<void> _collectAndroidIdIfAgreed() async { Future<void> _collectAndroidIdIfAgreed() async {
if (_androidId.isNotEmpty) return; if (_androidId.isNotEmpty) return;
if (!StorageService.to.getBool(storagePrivacyAgreed)) return; // 隐私同意后才采集 if (!StorageService.to.getBool(storagePrivacyAgreed)) return; // 隐私同意后才采集
+20
View File
@@ -0,0 +1,20 @@
import '../../features/assets/data/asset_models.dart';
class UploadRepository {
const UploadRepository();
Future<UploadResult> 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;
}
+13
View File
@@ -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<UploadResult> uploadAssetImage(AssetImage image) async {
// Placeholder: future implementation will upload the file and return URLs.
return repository.uploadAssetImage(image);
}
}
+119
View File
@@ -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<FileResponse> downloadImage(String url, String key) {
return DefaultCacheManager().getFileStream(
url,
key: key,
withProgress: true,
);
}
static Future<String> downloadImageInFile(String url, String key) async {
final file = await DefaultCacheManager().getSingleFile(url, key: key);
return file.path;
}
static Future<bool> 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<void> saveImage(FileInfo fileInfo) async {
await Gal.putImage(fileInfo.file.path, album: _albumName);
}
static Future<void> saveImageData(ByteData byteData) async {
Uint8List imageBytes = byteData.buffer.asUint8List();
await Gal.putImageBytes(
imageBytes,
name: 'XWallpaper_share',
album: _albumName,
);
}
static Future<void> saveImageUint8List(Uint8List imageBytes) async {
await Gal.putImageBytes(
imageBytes,
name: 'XWallpaper_share',
album: _albumName,
);
}
static Future<void> 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<Uint8List> 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<Uint8List?> 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;
}
}
+163
View File
@@ -1,5 +1,58 @@
import '../../../common/domain/metal_type.dart'; 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<String, dynamic> toJson() {
return {
'localId': localId,
'localPath': localPath,
'remoteUrl': remoteUrl,
'thumbnailUrl': thumbnailUrl,
'createdAt': createdAt.toIso8601String(),
};
}
factory AssetImage.fromJson(Map<String, dynamic> 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 { enum AssetCategory {
bar('金条'), bar('金条'),
necklace('项链'), necklace('项链'),
@@ -28,6 +81,7 @@ class GoldAssetHolding {
required this.weightGram, required this.weightGram,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
this.images = const [],
this.costAmount, this.costAmount,
this.purchaseDate, this.purchaseDate,
this.channel, this.channel,
@@ -42,6 +96,7 @@ class GoldAssetHolding {
final double purity; final double purity;
final String purityLabel; final String purityLabel;
final double weightGram; final double weightGram;
final List<AssetImage> images;
final double? costAmount; final double? costAmount;
final DateTime? purchaseDate; final DateTime? purchaseDate;
final String? channel; final String? channel;
@@ -50,7 +105,92 @@ class GoldAssetHolding {
final DateTime updatedAt; final DateTime updatedAt;
final HoldingStatus status; final HoldingStatus status;
GoldAssetHolding copyWith({
String? id,
String? name,
MetalType? metal,
AssetCategory? category,
double? purity,
String? purityLabel,
double? weightGram,
List<AssetImage>? 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; double materialValue(double spotPrice) => weightGram * purity * spotPrice;
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic>.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 { class HoldingValuation {
@@ -100,3 +240,26 @@ class PortfolioSummary {
final List<MetalBreakdown> breakdowns; final List<MetalBreakdown> breakdowns;
final List<HoldingValuation> holdings; final List<HoldingValuation> 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,
);
}
@@ -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: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: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/metal_type.dart';
import '../../../common/domain/money.dart'; import '../../../common/domain/money.dart';
@@ -43,6 +50,16 @@ class _AssetAddPageState extends ConsumerState<AssetAddPage> {
final _noteCtrl = TextEditingController(); final _noteCtrl = TextEditingController();
final _dateCtrl = TextEditingController(text: '2025-08-12'); final _dateCtrl = TextEditingController(text: '2025-08-12');
final _channelCtrl = TextEditingController(text: '银行'); final _channelCtrl = TextEditingController(text: '银行');
final _picker = ImagePicker();
final _images = <AssetImage>[];
final _uuid = const Uuid();
static const _maxImages = 9;
int get _remainingImageSlots => _maxImages - _images.length;
bool get _isImageLimitReached => _remainingImageSlots <= 0;
String get _imageLimitText => '最多可添加 $_maxImages 张图片';
@override @override
void dispose() { void dispose() {
@@ -91,6 +108,345 @@ class _AssetAddPageState extends ConsumerState<AssetAddPage> {
}; };
} }
Future<void> 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<void> _showImageSourceSheet() async {
if (!mounted) return;
showModalBottomSheet<void>(
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<void> _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<void> _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 = <AssetImage>[];
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<AssetImage> _assetImageFromXFile(XFile file) {
return _assetImageFromSourceFile(File(file.path));
}
Future<AssetImage> _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<bool> _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<void>(
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<void> _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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cs = ShadTheme.of(context).colorScheme; final cs = ShadTheme.of(context).colorScheme;
@@ -404,48 +760,9 @@ class _AssetAddPageState extends ConsumerState<AssetAddPage> {
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Container( _buildImagesSection(),
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),
),
),
),
],
),
),
],
const SizedBox(height: 16), const SizedBox(height: 16),
],
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: FilledButton( child: FilledButton(
@@ -468,6 +785,7 @@ class _AssetAddPageState extends ConsumerState<AssetAddPage> {
note: _noteCtrl.text.trim().isEmpty note: _noteCtrl.text.trim().isEmpty
? null ? null
: _noteCtrl.text.trim(), : _noteCtrl.text.trim(),
images: List<AssetImage>.unmodifiable(_images),
createdAt: DateTime.now(), createdAt: DateTime.now(),
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
@@ -277,6 +277,7 @@ class _AssetEditPageState extends ConsumerState<AssetEditPage> {
purity: _holding.purity, purity: _holding.purity,
purityLabel: _holding.purityLabel, purityLabel: _holding.purityLabel,
weightGram: grams ?? _holding.weightGram, weightGram: grams ?? _holding.weightGram,
images: _holding.images,
costAmount: cost, costAmount: cost,
purchaseDate: parsedDate, purchaseDate: parsedDate,
channel: _channelCtrl.text.trim().isEmpty channel: _channelCtrl.text.trim().isEmpty
@@ -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<AssetImage> images;
final int initialIndex;
@override
State<AssetImagePreviewPage> createState() => _AssetImagePreviewPageState();
}
class _AssetImagePreviewPageState extends State<AssetImagePreviewPage> {
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),
),
),
),
),
],
),
),
);
}
}
@@ -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:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_ui/shadcn_ui.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 '../../profile/application/profile_settings_controller.dart';
import '../application/asset_portfolio_controller.dart'; import '../application/asset_portfolio_controller.dart';
import '../data/asset_models.dart'; import '../data/asset_models.dart';
import 'asset_image_preview_page.dart';
class HoldingDetailPage extends ConsumerWidget { class HoldingDetailPage extends ConsumerWidget {
const HoldingDetailPage({super.key, this.id}); const HoldingDetailPage({super.key, this.id});
@@ -240,6 +243,8 @@ class HoldingDetailPage extends ConsumerWidget {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildImageSection(context, ref, holding),
const SizedBox(height: 12),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -362,6 +367,135 @@ class HoldingDetailPage extends ConsumerWidget {
).showSnackBar(const SnackBar(content: Text('已标记卖出 / 转赠'))); ).showSnackBar(const SnackBar(content: Text('已标记卖出 / 转赠')));
Navigator.of(context).pop(); 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<AssetImage> images,
int initialIndex,
) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => AssetImagePreviewPage(
images: images,
initialIndex: initialIndex,
),
),
);
}
void _confirmDeleteImage(
BuildContext context,
WidgetRef ref,
GoldAssetHolding holding,
AssetImage image,
) {
showDialog<void>(
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<AssetImage>.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 { class _ActionMenuTile extends StatelessWidget {
+289 -1
View File
@@ -215,6 +215,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" 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: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -359,6 +367,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" 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: fixnum:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -380,6 +420,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.2" 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: flutter_dotenv:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -440,6 +488,14 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.1" 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: flutter_remix:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -554,6 +610,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
gal:
dependency: "direct main"
description:
name: gal
sha256: "969598f986789127fd407a750413249e1352116d4c2be66e81837ffeeaafdfee"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@@ -658,6 +722,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" 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: intl:
dependency: transitive dependency: transitive
description: description:
@@ -802,6 +930,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -843,7 +979,7 @@ packages:
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
path_provider: path_provider:
dependency: transitive dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@@ -890,6 +1026,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" 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: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -898,6 +1082,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.2" 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: platform:
dependency: transitive dependency: transitive
description: description:
@@ -947,6 +1155,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.0" 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: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -1176,6 +1392,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.2" 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: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -1216,6 +1472,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -1424,6 +1688,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" 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: vm_service:
dependency: transitive dependency: transitive
description: description:
@@ -1464,6 +1736,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" 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: win32:
dependency: transitive dependency: transitive
description: description:
+7
View File
@@ -40,6 +40,13 @@ dependencies:
video_player: ^2.9.2 # 启动页视频背景 video_player: ^2.9.2 # 启动页视频背景
# 工具 # 工具
url_launcher: ^6.3.1 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 fancy_dio_inspector: ^2.0.1
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
android_id: android_id: