Chepuhagram/lib/presentation/screens/camera_screen.dart

484 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<CameraScreen> createState() => _CameraScreenState();
}
enum FlashModeType { off, autoCapture, alwaysCapture, torch }
class _CameraScreenState extends State<CameraScreen> {
CameraController? _controller;
List<CameraDescription> _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<void>? _initFuture;
@override
void initState() {
super.initState();
_initFuture = _init();
}
Future<void> _init() async {
_cameras = await availableCameras();
await _initCamera();
}
Future<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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,
),
),
),
],
),
),
),
),
],
);
},
),
);
}
}