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/services/raw_capture_service.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;
  final _rawCaptureService = RawCaptureService();

  @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 {
    // PixelCopy でカメラ領域のスナップショットを瞬時に取得
    try {
      if (!mounted) return;
      final renderBox = context.findRenderObject() as RenderBox?;
      if (renderBox != null) {
        final position = renderBox.localToGlobal(Offset.zero);
        final size = renderBox.size;
        final ratio = View.of(context).devicePixelRatio;

        // localToGlobal は回転後の座標(右下)を返すため,左上を計算する
        final topLeftX = position.dx - size.width;
        final topLeftY = position.dy - size.height;

        final bytes = await _rawCaptureService.capturePreviewFrame(
          x: (topLeftX * ratio).round(),
          y: (topLeftY * ratio).round(),
          width: (size.width * ratio).round(),
          height: (size.height * ratio).round(),
        );
        setState(() => _snapshotBytes = bytes);
      }
    } catch (_) {
      // 失敗しても撮影は続行
    }

    if (!mounted) return;
    context.read<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: [
        // 保存中はスナップショットを表示(Transform なし,Kotlin 側で回転済み)
        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,
                  ),
                ),
              ],
            ),
          ),
        // タイマー切り替えボタン(左側)
        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,
                ),
              ],
            ),
          ),
        ),
        // シャッターボタン(中央)
        Positioned(
          left: 0,
          right: 0,
          bottom: 24,
          child: Center(
            child: ShutterButton(
              onPressed: cameraProvider.isSaving || _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('権限を許可する'),
            ),
        ],
      ),
    );
  }
}