import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:mini_tias/providers/camera_provider.dart'; import 'package:mini_tias/widgets/camera_preview.dart'; import 'package:mini_tias/widgets/shutter_button.dart'; /// 撮影画面.カメラプレビューとシャッターボタンを表示する. class CaptureScreen extends StatefulWidget { const CaptureScreen({super.key}); @override State createState() => _CaptureScreenState(); } class _CaptureScreenState extends State with WidgetsBindingObserver { String? _previousFileName; bool _timerEnabled = false; int? _countdown; Timer? _countdownTimer; Uint8List? _snapshotBytes; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().initialize(); }); } @override void dispose() { _countdownTimer?.cancel(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final cameraProvider = context.read(); if (state == AppLifecycleState.paused) { _cancelCountdown(); cameraProvider.disposeCamera(); } else if (state == AppLifecycleState.resumed) { cameraProvider.initialize(); } } void _onShutterPressed() { final cameraProvider = context.read(); if (cameraProvider.isSaving || _countdown != null) return; if (_timerEnabled) { _startCountdown(); } else { _captureWithSnapshot(); } } Future _captureWithSnapshot() async { final cameraProvider = context.read(); // カメラの生フレームからスナップショットを取得(オーバーレイなし,瞬時) Uint8List? bytes; try { bytes = await cameraProvider.capturePreviewSnapshot(); if (bytes != null && mounted) { setState(() => _snapshotBytes = bytes); } // シャッター音を再生(撮影の瞬間を知らせる) cameraProvider.playShutterSound(); } catch (_) { // 失敗しても撮影は続行 } if (!mounted) return; // snapshot bytes を takePicture に渡し,preview.jpg として保存する cameraProvider.takePicture(previewBytes: bytes); } void _startCountdown() { setState(() => _countdown = 3); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_countdown == null) { timer.cancel(); return; } if (_countdown! <= 1) { timer.cancel(); setState(() => _countdown = null); _captureWithSnapshot(); } else { setState(() => _countdown = _countdown! - 1); } }); } void _cancelCountdown() { _countdownTimer?.cancel(); _countdownTimer = null; if (mounted) { setState(() => _countdown = null); } } void _showSaveResult(CameraProvider cameraProvider) { final fileName = cameraProvider.lastSavedFileName; if (fileName != null && fileName != _previousFileName) { _previousFileName = fileName; _snapshotBytes = null; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('保存しました: $fileName'), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, margin: EdgeInsets.only( bottom: MediaQuery.of(context).size.height / 2, left: 16, right: 16, ), ), ); }); } final error = cameraProvider.errorMessage; if (error != null && !cameraProvider.isInitialized) return; if (error != null && !cameraProvider.isSaving) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(error), backgroundColor: Colors.red), ); }); } } @override Widget build(BuildContext context) { final cameraProvider = context.watch(); _showSaveResult(cameraProvider); if (cameraProvider.permissionDenied) { return _buildPermissionDenied(cameraProvider); } if (cameraProvider.errorMessage != null && !cameraProvider.isInitialized) { return Center(child: Text(cameraProvider.errorMessage!)); } if (!cameraProvider.isInitialized && !cameraProvider.isSaving && _snapshotBytes == null) { return const Center(child: CircularProgressIndicator()); } return Stack( children: [ // 保存中はスナップショットを表示 if (_snapshotBytes != null && (!cameraProvider.isInitialized || cameraProvider.isSaving)) Positioned.fill( child: Image.memory(_snapshotBytes!, fit: BoxFit.cover), ) // カメラプレビュー else if (cameraProvider.isInitialized && cameraProvider.controller != null) Positioned.fill( child: CameraPreviewWidget(controller: cameraProvider.controller!), ), // 保存中インジケーター if (cameraProvider.isSaving) _buildSavingIndicator(), // カウントダウン表示 if (_countdown != null) _buildCountdownOverlay(_countdown!), // タイマー切り替えボタン(左側) if (!cameraProvider.isSaving) Positioned( left: 24, bottom: 36, child: GestureDetector( onTap: () { _cancelCountdown(); setState(() => _timerEnabled = !_timerEnabled); }, child: _buildToggleIcon( icon: _timerEnabled ? Icons.timer : Icons.timer_off, enabled: _timerEnabled, ), ), ), // サウンド切り替えボタン(右側) if (!cameraProvider.isSaving) Positioned( right: 24, bottom: 36, child: GestureDetector( onTap: () { cameraProvider.toggleSound(); }, child: _buildToggleIcon( icon: cameraProvider.isSoundEnabled ? Icons.volume_up : Icons.volume_off, enabled: cameraProvider.isSoundEnabled, ), ), ), // シャッターボタン(中央) if (!cameraProvider.isSaving) Positioned( left: 0, right: 0, bottom: 24, child: Center( child: ShutterButton( onPressed: _countdown != null ? () {} : _onShutterPressed, ), ), ), ], ); } /// 保存処理中を示すローディングインジケーター(中央配置). Widget _buildSavingIndicator() { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: Colors.white), SizedBox(height: 12), Text('保存中...', style: TextStyle(color: Colors.white, fontSize: 16)), ], ), ); } /// セルフタイマーのカウントダウン数字を中央に表示するオーバーレイ. /// /// 黒いアウトラインと白い塗りを重ねることで視認性を確保する. Widget _buildCountdownOverlay(int countdown) { return Center( child: Stack( children: [ Text( '$countdown', style: TextStyle( fontSize: 96, fontWeight: FontWeight.bold, foreground: Paint() ..style = PaintingStyle.stroke ..strokeWidth = 6 ..color = Colors.black, ), ), Text( '$countdown', style: const TextStyle( color: Colors.white, fontSize: 96, fontWeight: FontWeight.bold, ), ), ], ), ); } /// アウトライン(黒・40px)とカラー(有効:黄/無効:灰・32px)を重ねたトグルアイコン. Widget _buildToggleIcon({required IconData icon, required bool enabled}) { return Stack( alignment: Alignment.center, children: [ Icon(icon, color: Colors.black, size: 40), Icon(icon, color: enabled ? Colors.yellow : Colors.grey, size: 32), ], ); } Widget _buildPermissionDenied(CameraProvider cameraProvider) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(cameraProvider.errorMessage ?? 'カメラの権限が必要です'), const SizedBox(height: 16), if (cameraProvider.permissionPermanentlyDenied) ElevatedButton( onPressed: () => cameraProvider.openSettings(), child: const Text('設定画面を開く'), ) else ElevatedButton( onPressed: () => cameraProvider.initialize(), child: const Text('権限を許可する'), ), ], ), ); } }