Chepuhagram/lib/presentation/screens/camera_screen.dart

413 lines
12 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;
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;
await _controller?.dispose();
_cameraIndex = (_cameraIndex + 1) % _cameras.length;
setState(() => _controller = null);
await _initCamera();
}
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) {
await _controller!.setFlashMode(FlashMode.torch);
usedTorch = true;
await Future.delayed(const Duration(milliseconds: 120));
}
if (_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) {
await _controller!.setFlashMode(FlashMode.torch);
usedTorch = true;
await Future.delayed(const Duration(milliseconds: 120));
}
if (_flashMode == FlashModeType.autoCapture) {
await _controller!.setFlashMode(FlashMode.torch);
usedTorch = true;
await Future.delayed(const Duration(milliseconds: 120));
}
await _controller!.startVideoRecording();
setState(() => _isRecording = true);
}
Future<void> _stopVideo() async {
if (_controller == null || !_isRecording) return;
if (usedTorch) {
await _controller!.setFlashMode(FlashMode.off);
}
final file = await _controller!.stopVideoRecording();
setState(() => _isRecording = 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());
}
return Stack(
children: [
// 📷 Camera preview (full screen, Telegram style crop)
Positioned.fill(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _controller!.value.previewSize!.height,
height: _controller!.value.previewSize!.width,
child: GestureDetector(
onScaleStart: (_) {
setState(() {
_showZoomSlider = true;
});
},
onScaleUpdate: (details) {
final zoom = (_currentZoom * details.scale).clamp(
_minZoom,
_maxZoom,
);
_setZoom(zoom);
},
child: CameraPreview(_controller!),
),
),
),
),
// 🌑 top gradient (Telegram feel)
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: 90,
left: 0,
right: 0,
child: Column(
children: [
GestureDetector(
onTap: _takePhoto,
onLongPressStart: (_) => _startVideo(),
onLongPressEnd: (_) => _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),
),
),
),
const SizedBox(height: 16),
const Text(
"Нажмите для фото, удерживайте для съемки",
style: TextStyle(color: Colors.white70, fontSize: 13),
),
],
),
),
// 🔴 recording indicator
if (_isRecording)
const Positioned(
top: 50,
left: 0,
right: 0,
child: Center(
child: Text(
"REC",
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
),
if (_showZoomSlider)
Positioned(
bottom: 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,
),
),
),
],
),
),
),
),
],
);
},
),
);
}
}