Files
sight-identification/lib/screens/camera_screen.dart
2026-01-07 16:14:34 +08:00

463 lines
14 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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),
),
),
),
),
),
],
),
);
}
}