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<CaptureScreen> createState() => _CaptureScreenState();
}
class _CaptureScreenState extends State<CaptureScreen>
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<CameraProvider>().initialize();
});
}
@override
void dispose() {
_countdownTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final cameraProvider = context.read<CameraProvider>();
if (state == AppLifecycleState.paused) {
_cancelCountdown();
cameraProvider.disposeCamera();
} else if (state == AppLifecycleState.resumed) {
cameraProvider.initialize();
}
}
void _onShutterPressed() {
final cameraProvider = context.read<CameraProvider>();
if (cameraProvider.isSaving || _countdown != null) return;
if (_timerEnabled) {
_startCountdown();
} else {
_captureWithSnapshot();
}
}
Future<void> _captureWithSnapshot() async {
final cameraProvider = context.read<CameraProvider>();
// カメラの生フレームからスナップショットを取得(オーバーレイなし,瞬時)
try {
final bytes = await cameraProvider.capturePreviewSnapshot();
if (bytes != null && mounted) {
setState(() => _snapshotBytes = bytes);
}
} catch (_) {
// 失敗しても撮影は続行
}
if (!mounted) return;
cameraProvider.takePicture();
}
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<CameraProvider>();
_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)
const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 12),
Text(
'保存中...',
style: TextStyle(color: Colors.white, fontSize: 16),
),
],
),
),
// カウントダウン表示
if (_countdown != null)
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,
),
),
],
),
),
// タイマー切り替えボタン(左側)
if (!cameraProvider.isSaving)
Positioned(
left: 24,
bottom: 36,
child: GestureDetector(
onTap: () {
_cancelCountdown();
setState(() => _timerEnabled = !_timerEnabled);
},
child: Stack(
alignment: Alignment.center,
children: [
Icon(
_timerEnabled ? Icons.timer : Icons.timer_off,
color: Colors.black,
size: 40,
),
Icon(
_timerEnabled ? Icons.timer : Icons.timer_off,
color: _timerEnabled ? Colors.yellow : Colors.grey,
size: 32,
),
],
),
),
),
// シャッターボタン(中央)
if (!cameraProvider.isSaving)
Positioned(
left: 0,
right: 0,
bottom: 24,
child: Center(
child: ShutterButton(
onPressed: _countdown != null ? () {} : _onShutterPressed,
),
),
),
],
);
}
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('権限を許可する'),
),
],
),
);
}
}