Newer
Older
MiniTias / lib / screens / capture_screen.dart
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);
      }
      // シャッター音を再生(撮影の瞬間を知らせる)
      cameraProvider.playShutterSound();
    } 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: _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,
              ),
            ),
          ),
      ],
    );
  }

  /// アウトライン(黒・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('権限を許可する'),
            ),
        ],
      ),
    );
  }
}