petsHero-AI/lib/features/generate_video/in_app_camera_page.dart
2026-03-29 23:53:24 +08:00

220 lines
7.0 KiB
Dart
Raw Permalink 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 'package:flutter_lucide/flutter_lucide.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../core/log/app_logger.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_spacing.dart';
/// 应用内拍照:不跳出系统相机 Activity避免与 Flutter Surface / 截屏防护等叠加导致黑屏卡死。
class InAppCameraPage extends StatefulWidget {
const InAppCameraPage({super.key});
static final _log = AppLogger('InAppCamera');
@override
State<InAppCameraPage> createState() => _InAppCameraPageState();
}
class _InAppCameraPageState extends State<InAppCameraPage> {
CameraController? _controller;
bool _initializing = true;
bool _capturing = false;
String? _error;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _prepareCamera());
}
Future<void> _prepareCamera() async {
final status = await Permission.camera.request();
if (!status.isGranted) {
if (mounted) {
setState(() {
_initializing = false;
_error = 'Camera permission is required';
});
}
return;
}
try {
final cameras = await availableCameras();
if (cameras.isEmpty) {
if (mounted) {
setState(() {
_initializing = false;
_error = 'No camera available';
});
}
return;
}
final back = cameras.where((c) => c.lensDirection == CameraLensDirection.back);
final selected = back.isEmpty ? cameras.first : back.first;
final c = CameraController(
selected,
ResolutionPreset.medium,
enableAudio: false,
);
await c.initialize();
if (!mounted) {
await c.dispose();
return;
}
setState(() {
_controller = c;
_initializing = false;
});
} catch (e, st) {
InAppCameraPage._log.e('Camera init failed', e, st);
if (mounted) {
setState(() {
_initializing = false;
_error = 'Could not open camera';
});
}
}
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
Future<void> _onShutter() async {
final c = _controller;
if (c == null || !c.value.isInitialized || _capturing) return;
setState(() => _capturing = true);
try {
final shot = await c.takePicture();
if (!mounted) return;
Navigator.of(context, rootNavigator: true).pop<String>(shot.path);
} catch (e, st) {
InAppCameraPage._log.e('takePicture failed', e, st);
if (mounted) setState(() => _capturing = false);
}
}
void _onClose() {
Navigator.of(context, rootNavigator: true).pop<String>(null);
}
@override
Widget build(BuildContext context) {
final c = _controller;
final showPreview =
!_initializing && _error == null && c != null && c.value.isInitialized;
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
fit: StackFit.expand,
children: [
if (showPreview)
Positioned.fill(
child: ColoredBox(
color: Colors.black,
// 勿再包一层自定义 aspect 的 SizedBox/FittedBox[CameraPreview] 已在竖屏下使用
// 1/value.aspectRatio + Android [RotatedBox],外层乱算比例会把它压扁。
child: Center(
child: CameraPreview(c),
),
),
),
SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
if (_initializing)
const Center(
child:
CircularProgressIndicator(color: AppColors.primary),
)
else if (_error != null)
Center(
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
LucideIcons.camera_off,
size: 48,
color: AppColors.surface.withValues(alpha: 0.6),
),
const SizedBox(height: AppSpacing.md),
Text(
_error!,
textAlign: TextAlign.center,
style: TextStyle(
color: AppColors.surface.withValues(alpha: 0.85),
fontSize: 15,
),
),
const SizedBox(height: AppSpacing.lg),
TextButton(
onPressed: _onClose,
child: const Text('Close'),
),
],
),
),
),
if (showPreview)
Positioned(
left: 0,
right: 0,
bottom: 24,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: _capturing ? null : _onClose,
icon: Icon(
LucideIcons.x,
color: AppColors.surface.withValues(alpha: 0.9),
size: 28,
),
),
GestureDetector(
onTap: _capturing ? null : _onShutter,
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppColors.surface,
width: 4,
),
color: AppColors.surface.withValues(alpha: 0.2),
),
child: _capturing
? const Padding(
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.surface,
),
)
: null,
),
),
const SizedBox(width: 48),
],
),
),
],
),
),
],
),
);
}
}