463 lines
14 KiB
Dart
463 lines
14 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'package:camera/camera.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||
import 'package:image_picker/image_picker.dart';
|
||
import 'package:gal/gal.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
import 'package:path/path.dart' as path;
|
||
import 'package:permission_handler/permission_handler.dart';
|
||
import 'package:camera_app/services/yolo_service.dart';
|
||
import 'package:camera_app/screens/result_screen.dart';
|
||
|
||
class CameraScreen extends StatefulWidget {
|
||
const CameraScreen({super.key});
|
||
|
||
@override
|
||
State<CameraScreen> createState() => _CameraScreenState();
|
||
}
|
||
|
||
class _CameraScreenState extends State<CameraScreen> {
|
||
CameraController? _controller;
|
||
List<CameraDescription>? _cameras;
|
||
bool _isInitialized = false;
|
||
bool _isCapturing = false;
|
||
int _photoCount = 0;
|
||
Timer? _timer;
|
||
List<String> _savedPhotoPaths = [];
|
||
bool _isWebOrDesktop = false;
|
||
final YOLOService _yoloService = YOLOService();
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_initializeCamera();
|
||
}
|
||
|
||
Future<void> _initializeCamera() async {
|
||
try {
|
||
// Web 和桌面平台使用 image_picker
|
||
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_isWebOrDesktop = true;
|
||
_isInitialized = true; // Web/桌面平台直接显示界面
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 移动平台使用 camera 包
|
||
await _requestPermissions();
|
||
|
||
_cameras = await availableCameras();
|
||
if (_cameras == null || _cameras!.isEmpty) {
|
||
_showError('未找到可用的相机');
|
||
return;
|
||
}
|
||
|
||
_controller = CameraController(
|
||
_cameras![0],
|
||
ResolutionPreset.high,
|
||
enableAudio: false,
|
||
);
|
||
|
||
await _controller!.initialize();
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
_isInitialized = true;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
_showError('初始化相机失败: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _requestPermissions() async {
|
||
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) {
|
||
return; // Web/桌面平台权限由浏览器/系统处理
|
||
}
|
||
|
||
final cameraStatus = await Permission.camera.request();
|
||
if (!cameraStatus.isGranted) {
|
||
_showError('需要相机权限才能使用此功能');
|
||
return;
|
||
}
|
||
|
||
if (Platform.isAndroid) {
|
||
final photosStatus = await Permission.photos.request();
|
||
if (photosStatus.isDenied || photosStatus.isPermanentlyDenied) {
|
||
final storageStatus = await Permission.storage.request();
|
||
if (!storageStatus.isGranted) {
|
||
_showError('需要存储权限才能保存图片');
|
||
return;
|
||
}
|
||
}
|
||
} else if (Platform.isIOS) {
|
||
final photosStatus = await Permission.photos.request();
|
||
if (!photosStatus.isGranted) {
|
||
_showError('需要照片权限才能保存图片');
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _startCapturing() async {
|
||
if (_isWebOrDesktop) {
|
||
_showError('Web/桌面平台不支持自动连续拍照,请使用移动设备');
|
||
return;
|
||
}
|
||
|
||
// 移动平台:自动拍照模式
|
||
if (!_isInitialized || _controller == null || !_controller!.value.isInitialized) {
|
||
_showError('相机未初始化');
|
||
return;
|
||
}
|
||
|
||
setState(() {
|
||
_isCapturing = true;
|
||
_photoCount = 0;
|
||
_savedPhotoPaths.clear();
|
||
});
|
||
|
||
// 立即拍摄第一张
|
||
await _takePhoto();
|
||
|
||
// 然后每2秒拍摄一张,直到点击停止
|
||
_timer = Timer.periodic(const Duration(seconds: 2), (timer) async {
|
||
if (_isCapturing) {
|
||
await _takePhoto();
|
||
} else {
|
||
timer.cancel();
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> _stopCapturing() async {
|
||
setState(() {
|
||
_isCapturing = false;
|
||
});
|
||
_timer?.cancel();
|
||
|
||
if (_savedPhotoPaths.isEmpty) {
|
||
_showError('没有拍摄照片');
|
||
return;
|
||
}
|
||
|
||
// 显示分析中对话框
|
||
if (!mounted) return;
|
||
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (BuildContext context) {
|
||
return const AlertDialog(
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
CircularProgressIndicator(),
|
||
SizedBox(height: 16),
|
||
Text('正在分析照片...'),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
|
||
try {
|
||
// 使用YOLO分析照片
|
||
final analysisResults = await _yoloService.analyzeImages(_savedPhotoPaths);
|
||
|
||
if (!mounted) return;
|
||
|
||
Navigator.of(context).pop(); // 关闭分析中对话框
|
||
|
||
// 导航到结果页面
|
||
Navigator.of(context).pushReplacement(
|
||
MaterialPageRoute(
|
||
builder: (context) => ResultScreen(
|
||
photoPaths: _savedPhotoPaths,
|
||
analysisResults: analysisResults,
|
||
),
|
||
),
|
||
);
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
|
||
Navigator.of(context).pop(); // 关闭分析中对话框
|
||
_showError('分析照片失败: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _takePhoto() async {
|
||
if (_controller == null || !_controller!.value.isInitialized) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
final XFile photo = await _controller!.takePicture();
|
||
await _savePhoto(photo.path);
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
_photoCount++;
|
||
});
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
_showError('拍照失败: $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _savePhoto(String photoPath) async {
|
||
try {
|
||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||
// 移动平台:保存到相册
|
||
await Gal.putImage(photoPath);
|
||
} else {
|
||
// Web/桌面平台:保存到下载目录
|
||
final appDir = await getApplicationDocumentsDirectory();
|
||
final fileName = 'photo_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||
final savedPath = path.join(appDir.path, fileName);
|
||
await File(photoPath).copy(savedPath);
|
||
}
|
||
_savedPhotoPaths.add(photoPath);
|
||
} catch (e) {
|
||
// 保存失败不影响流程
|
||
_savedPhotoPaths.add(photoPath);
|
||
}
|
||
}
|
||
|
||
void _showError(String message) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(message),
|
||
backgroundColor: Colors.red,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_timer?.cancel();
|
||
_controller?.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (!_isInitialized) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.black,
|
||
body: const Center(
|
||
child: CircularProgressIndicator(color: Colors.white),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Web/桌面平台界面
|
||
if (_isWebOrDesktop) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.black,
|
||
body: Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24.0),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(
|
||
Icons.camera_alt,
|
||
size: 100,
|
||
color: Colors.white,
|
||
),
|
||
const SizedBox(height: 32),
|
||
Text(
|
||
'$_photoCount 张',
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 48,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
'Web/桌面平台不支持自动连续拍照',
|
||
style: TextStyle(
|
||
color: Colors.white70,
|
||
fontSize: 18,
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
const SizedBox(height: 48),
|
||
IconButton(
|
||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
tooltip: '返回',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// 移动平台:显示相机预览
|
||
if (_controller == null || !_controller!.value.isInitialized) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.black,
|
||
body: const Center(
|
||
child: Text(
|
||
'相机初始化中...',
|
||
style: TextStyle(color: Colors.white),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return Scaffold(
|
||
backgroundColor: Colors.black,
|
||
body: Stack(
|
||
children: [
|
||
SizedBox.expand(child: CameraPreview(_controller!)),
|
||
|
||
SafeArea(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [
|
||
Colors.black.withOpacity(0.7),
|
||
Colors.transparent,
|
||
],
|
||
),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
IconButton(
|
||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||
onPressed: () {
|
||
if (_isCapturing) {
|
||
_stopCapturing();
|
||
}
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
if (_isCapturing)
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 16,
|
||
vertical: 8,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Colors.red.withOpacity(0.8),
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 12,
|
||
height: 12,
|
||
decoration: const BoxDecoration(
|
||
color: Colors.white,
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'$_photoCount 张',
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
Align(
|
||
alignment: Alignment.bottomCenter,
|
||
child: Container(
|
||
padding: const EdgeInsets.all(32),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.bottomCenter,
|
||
end: Alignment.topCenter,
|
||
colors: [
|
||
Colors.black.withOpacity(0.7),
|
||
Colors.transparent,
|
||
],
|
||
),
|
||
),
|
||
child: _isCapturing
|
||
? Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
ElevatedButton.icon(
|
||
onPressed: _stopCapturing,
|
||
icon: const Icon(Icons.stop, size: 28),
|
||
label: const Text(
|
||
'停止拍照',
|
||
style: TextStyle(fontSize: 18),
|
||
),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.red,
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 32,
|
||
vertical: 16,
|
||
),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(30),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'已拍摄 $_photoCount 张照片',
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 16,
|
||
),
|
||
),
|
||
],
|
||
)
|
||
: ElevatedButton.icon(
|
||
onPressed: _startCapturing,
|
||
icon: const Icon(Icons.camera_alt, size: 28),
|
||
label: const Text(
|
||
'开始拍照',
|
||
style: TextStyle(fontSize: 18),
|
||
),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.white,
|
||
foregroundColor: Colors.black,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 32,
|
||
vertical: 16,
|
||
),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(30),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|