import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'dart:async'; import 'media_preview_screen.dart'; class CameraScreen extends StatefulWidget { const CameraScreen({super.key}); @override State createState() => _CameraScreenState(); } enum FlashModeType { off, autoCapture, alwaysCapture, torch } class _CameraScreenState extends State { CameraController? _controller; List _cameras = []; int _cameraIndex = 0; bool _isRecording = false; bool _isLockedRecording = false; FlashModeType _flashMode = FlashModeType.off; double _minZoom = 1.0; double _maxZoom = 1.0; double _currentZoom = 1.0; bool _showZoomSlider = false; Future? _initFuture; @override void initState() { super.initState(); _initFuture = _init(); } Future _init() async { _cameras = await availableCameras(); await _initCamera(); } Future _initCamera() async { final camera = _cameras[_cameraIndex]; final controller = CameraController( camera, ResolutionPreset.high, enableAudio: true, ); await controller.initialize(); _minZoom = await controller.getMinZoomLevel(); _maxZoom = await controller.getMaxZoomLevel(); _currentZoom = _minZoom; if (!mounted) return; setState(() { _controller = controller; }); } // Обновленный метод смены камеры (поддерживает переключение во время записи) Future _switchCamera() async { if (_cameras.length < 2) return; _cameraIndex = (_cameraIndex + 1) % _cameras.length; final newCamera = _cameras[_cameraIndex]; if (_controller != null && _controller!.value.isInitialized) { try { // Меняем описание камеры «на лету» без dispose контроллера await _controller!.setDescription(newCamera); // Обновляем параметры зума для новой линзы _minZoom = await _controller!.getMinZoomLevel(); _maxZoom = await _controller!.getMaxZoomLevel(); _currentZoom = _currentZoom.clamp(_minZoom, _maxZoom); } catch (e) { // Если динамическая смена не удалась, делаем полный перезапуск await _initCamera(); } } else { await _initCamera(); } setState(() {}); } Future _cycleFlashMode() async { if (_controller == null) return; switch (_flashMode) { case FlashModeType.off: _flashMode = FlashModeType.autoCapture; await _controller!.setFlashMode(FlashMode.off); break; case FlashModeType.autoCapture: _flashMode = FlashModeType.alwaysCapture; await _controller!.setFlashMode(FlashMode.off); break; case FlashModeType.alwaysCapture: _flashMode = FlashModeType.torch; await _controller!.setFlashMode(FlashMode.torch); break; case FlashModeType.torch: _flashMode = FlashModeType.off; await _controller!.setFlashMode(FlashMode.off); break; } setState(() {}); } Future _takePhoto() async { if (_controller == null) return; bool usedTorch = false; if (_flashMode == FlashModeType.alwaysCapture || _flashMode == FlashModeType.autoCapture) { await _controller!.setFlashMode(FlashMode.torch); usedTorch = true; await Future.delayed(const Duration(milliseconds: 120)); } final file = await _controller!.takePicture(); if (usedTorch) { await _controller!.setFlashMode(FlashMode.off); } WidgetsBinding.instance.addPostFrameCallback((_) async { final result = await Navigator.push( context, MaterialPageRoute( builder: (_) => MediaPreviewScreen(path: file.path, isVideo: false), ), ); if (result == true && mounted) { Navigator.pop(context, (file, 'image')); } }); } bool usedTorch = false; Future _startVideo() async { if (_controller == null || _isRecording) return; if (_flashMode == FlashModeType.alwaysCapture || _flashMode == FlashModeType.autoCapture) { await _controller!.setFlashMode(FlashMode.torch); usedTorch = true; await Future.delayed(const Duration(milliseconds: 120)); } await _controller!.startVideoRecording(); setState(() { _isRecording = true; _isLockedRecording = false; // Сбрасываем фиксацию при новом старте }); } Future _stopVideo() async { if (_controller == null || !_isRecording) return; if (usedTorch) { await _controller!.setFlashMode(FlashMode.off); } final file = await _controller!.stopVideoRecording(); setState(() { _isRecording = false; _isLockedRecording = false; }); final result = await Navigator.push( context, MaterialPageRoute( builder: (_) => MediaPreviewScreen(path: file.path, isVideo: true), ), ); if (result == true && mounted) { Navigator.pop(context, (file, 'video')); } } Future _setZoom(double zoom) async { if (_controller == null) return; final clamped = zoom.clamp(_minZoom, _maxZoom); await _controller!.setZoomLevel(clamped); setState(() { _currentZoom = clamped; }); } @override void dispose() { _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: FutureBuilder( future: _initFuture, builder: (context, snapshot) { if (_controller == null || !_controller!.value.isInitialized) { return const Center(child: CircularProgressIndicator()); } final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape; final previewSize = _controller!.value.previewSize!; final previewWidth = isLandscape ? previewSize.width : previewSize.height; final previewHeight = isLandscape ? previewSize.height : previewSize.width; return Stack( children: [ // 📷 Camera preview Positioned.fill( child: FittedBox( fit: BoxFit.cover, child: SizedBox( width: previewWidth, height: previewHeight, child: GestureDetector( onScaleStart: (_) { setState(() { _showZoomSlider = true; }); }, onScaleUpdate: (details) { final zoom = (_currentZoom * details.scale).clamp( _minZoom, _maxZoom, ); _setZoom(zoom); }, child: CameraPreview(_controller!), ), ), ), ), // 🌑 top gradient Positioned( top: 0, left: 0, right: 0, height: 120, child: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.black87, Colors.transparent], ), ), ), ), // 🔘 top controls Positioned( top: 50, left: 20, right: 20, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Flash (left) IconButton( onPressed: _cycleFlashMode, icon: Icon(switch (_flashMode) { FlashModeType.off => Icons.flash_off, FlashModeType.autoCapture => Icons.flash_auto, FlashModeType.alwaysCapture => Icons.flash_on, FlashModeType.torch => Icons.highlight, }, color: Colors.white), ), // Camera switch (right) IconButton( onPressed: _switchCamera, icon: const Icon(Icons.cameraswitch, color: Colors.white), ), ], ), ), // 🔘 capture button (center bottom) Positioned( bottom: isLandscape ? 30 : 90, left: 0, right: 0, child: Column( children: [ GestureDetector( onTap: () { if (_isLockedRecording) { // Если запись зафиксирована, повторный тап останавливает её _stopVideo(); } else { // Иначе обычное фото _takePhoto(); } }, onLongPressStart: (_) => _startVideo(), onLongPressMoveUpdate: (details) { if (_isRecording && !_isLockedRecording) { // Если ведем запись и тянем палец вверх (значение dy уменьшается) if (details.localOffsetFromOrigin.dy < -60) { setState(() { _isLockedRecording = true; }); } } }, onLongPressEnd: (_) { // Если запись зафиксирована, при отпускании пальца НЕ останавливаем съемку if (!_isLockedRecording) { _stopVideo(); } }, child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: _isRecording ? 80 : 72, height: _isRecording ? 80 : 72, decoration: BoxDecoration( shape: BoxShape.circle, color: _isRecording ? Colors.red : Colors.white, border: Border.all(color: Colors.white, width: 4), ), child: _isLockedRecording ? const Icon( Icons.stop, color: Colors.white, size: 32, ) : null, // Показываем иконку стоп, когда запись зафиксирована ), ), const SizedBox(height: 16), Text( _isLockedRecording ? "Запись зафиксирована. Нажмите для остановки." : "Нажмите для фото, удерживайте для съемки", style: const TextStyle( color: Colors.white70, fontSize: 13, ), ), ], ), ), // 🔴 recording indicator if (_isRecording) Positioned( top: 50, left: 0, right: 0, child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), ), const SizedBox(width: 6), Text( _isLockedRecording ? "REC (LOCK)" : "REC", style: const TextStyle( color: Colors.red, fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ), ), ), if (_showZoomSlider) Positioned( bottom: isLandscape ? 120 : 200, left: 20, right: 20, child: Center( child: Container( child: Row( children: [ GestureDetector( onTap: () { final newZoom = (_currentZoom - 0.5).clamp( _minZoom, _maxZoom, ); _setZoom(newZoom); }, child: const Text( '−', style: TextStyle( color: Colors.white, fontSize: 18, ), ), ), const SizedBox(width: 8), Expanded( child: SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 2, activeTrackColor: Colors.white, inactiveTrackColor: Colors.white24, thumbColor: Colors.white, overlayColor: Colors.white24, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6, ), ), child: Slider( value: _currentZoom, min: _minZoom, max: _maxZoom, onChanged: (value) { _setZoom(value); }, ), ), ), const SizedBox(width: 8), GestureDetector( onTap: () { final newZoom = (_currentZoom + 0.5).clamp( _minZoom, _maxZoom, ); _setZoom(newZoom); }, child: const Text( '+', style: TextStyle( color: Colors.white, fontSize: 18, ), ), ), ], ), ), ), ), ], ); }, ), ); } }