import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../services/coin_scan_gate.dart'; import '../widgets/guide_overlay.dart'; import 'preview_screen.dart'; /// Capture only — blur + detect run after shutter (CoinSnap flow). class CaptureScreen extends StatefulWidget { const CaptureScreen({super.key}); @override State createState() => _CaptureScreenState(); } class _CaptureScreenState extends State with WidgetsBindingObserver { final _picker = ImagePicker(); CameraController? _camera; List _cameras = const []; bool _ready = false; bool _busy = false; String? _error; Size? _previewSize; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _initCamera(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _camera?.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final cam = _camera; if (cam == null || !cam.value.isInitialized) return; if (state == AppLifecycleState.inactive) { cam.dispose(); _camera = null; _ready = false; } else if (state == AppLifecycleState.resumed) { _initCamera(); } } Future _initCamera() async { try { _cameras = await availableCameras(); if (_cameras.isEmpty) { setState(() => _error = '没有可用相机'); return; } final back = _cameras.firstWhere( (c) => c.lensDirection == CameraLensDirection.back, orElse: () => _cameras.first, ); final controller = CameraController( back, ResolutionPreset.high, enableAudio: false, ); await controller.initialize(); if (!mounted) { await controller.dispose(); return; } _camera = controller; setState(() { _ready = true; _error = null; }); } catch (e) { setState(() => _error = '相机初始化失败: $e'); } } Future _openPreviewIfOk(String path) async { final result = await CoinScanGate.evaluate( imagePath: path, captureViewport: _previewSize, ); if (!mounted) return; if (!result.ok) { final msg = switch (result.fail) { CoinGateFail.blurry => '图像过模糊(清晰度 ${(result.blur!.clearScore * 100).toStringAsFixed(0)}%),请重拍', CoinGateFail.noCoin => '未检测到硬币,请对准圆框后重拍', CoinGateFail.none => '无法处理该图片', }; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg), duration: const Duration(seconds: 2)), ); return; } await Navigator.of(context).push( MaterialPageRoute( builder: (_) => PreviewScreen( imagePath: path, captureViewport: _previewSize, ), ), ); } Future _capture() async { if (_busy) return; final cam = _camera; if (cam == null || !cam.value.isInitialized) return; setState(() => _busy = true); try { final shot = await cam.takePicture(); await _openPreviewIfOk(shot.path); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('拍照失败: $e')), ); } } finally { if (mounted) setState(() => _busy = false); } } Future _pickGallery() async { if (_busy) return; setState(() => _busy = true); try { final file = await _picker.pickImage( source: ImageSource.gallery, imageQuality: 100, requestFullMetadata: true, ); if (file == null) return; await _openPreviewIfOk(file.path); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('选图失败: $e')), ); } } finally { if (mounted) setState(() => _busy = false); } } Future _switchCamera() async { if (_cameras.length < 2 || _busy) return; final current = _camera?.description; final next = _cameras.firstWhere( (c) => c.lensDirection != current?.lensDirection, orElse: () => _cameras.first, ); setState(() => _ready = false); await _camera?.dispose(); final controller = CameraController( next, ResolutionPreset.high, enableAudio: false, ); await controller.initialize(); if (!mounted) { await controller.dispose(); return; } _camera = controller; setState(() => _ready = true); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( fit: StackFit.expand, children: [ _buildPreview(), SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '硬币扫描', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text( '将硬币放入圆框 → blur 清晰度检测 → detect 轮廓锁定', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white70, ), ), ], ), ), ), if (_busy) const ColoredBox( color: Color(0x66000000), child: Center(child: CircularProgressIndicator()), ), Positioned( left: 0, right: 0, bottom: 0, child: _buildControls(), ), ], ), ); } Widget _buildPreview() { if (_error != null) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text(_error!, textAlign: TextAlign.center), ), ); } final cam = _camera; if (!_ready || cam == null || !cam.value.isInitialized) { return const Center(child: CircularProgressIndicator()); } return LayoutBuilder( builder: (context, constraints) { final previewSize = Size(constraints.maxWidth, constraints.maxHeight); _previewSize = previewSize; return Stack( fit: StackFit.expand, children: [ FittedBox( fit: BoxFit.cover, child: SizedBox( width: cam.value.previewSize?.height ?? previewSize.width, height: cam.value.previewSize?.width ?? previewSize.height, child: CameraPreview(cam), ), ), GuideOverlay(size: previewSize), ], ); }, ); } Widget _buildControls() { return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(28, 12, 28, 24), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _RoundButton( icon: Icons.photo_library_outlined, onTap: _busy ? null : _pickGallery, ), GestureDetector( onTap: _busy ? null : _capture, child: Container( width: 76, height: 76, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 4), ), padding: const EdgeInsets.all(5), child: Container( decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ), ), _RoundButton( icon: Icons.flip_camera_ios_outlined, onTap: _busy ? null : _switchCamera, ), ], ), ), ); } } class _RoundButton extends StatelessWidget { const _RoundButton({required this.icon, this.onTap}); final IconData icon; final VoidCallback? onTap; @override Widget build(BuildContext context) { return Material( color: Colors.white12, shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), onTap: onTap, child: SizedBox( width: 52, height: 52, child: Icon(icon, color: Colors.white), ), ), ); } }