diff --git a/lib/locale/l10n/app_de.arb b/lib/locale/l10n/app_de.arb index 2775ffc..ea36eee 100644 --- a/lib/locale/l10n/app_de.arb +++ b/lib/locale/l10n/app_de.arb @@ -21,6 +21,8 @@ "mainScreenTakeVideoActionSaveVideo": "Video wird aufgenommen, halte still...", "mainScreenTakeVideoActionUploadingVideo": "Video wird hochgeladen...", + "recordingOverlayIsRecording": "Aufnahme", + "loginScreenTitle": "Anmelden", "loginScreenLoginError": "E-Mail oder Passwort inkorrekt", diff --git a/lib/locale/l10n/app_en.arb b/lib/locale/l10n/app_en.arb index f13f61e..253614a 100644 --- a/lib/locale/l10n/app_en.arb +++ b/lib/locale/l10n/app_en.arb @@ -21,6 +21,8 @@ "mainScreenTakeVideoActionSaveVideo": "Taking video, please hold still...", "mainScreenTakeVideoActionUploadingVideo": "Uploading video...", + "recordingOverlayIsRecording": "Recording", + "loginScreenTitle": "Login", "loginScreenLoginError": "Invalid password or email", diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 08c23b1..6d88eed 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -15,11 +15,13 @@ import 'package:quid_faciam_hodie/utils/auth_required.dart'; import 'package:quid_faciam_hodie/utils/loadable.dart'; import 'package:quid_faciam_hodie/widgets/animate_in_builder.dart'; import 'package:quid_faciam_hodie/widgets/fade_and_move_in_animation.dart'; +import 'package:quid_faciam_hodie/widgets/icon_button_child.dart'; import 'package:quid_faciam_hodie/widgets/sheet_indicator.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'main_screen/change_camera_button.dart'; import 'main_screen/record_button.dart'; +import 'main_screen/recording_overlay.dart'; import 'main_screen/today_photo_button.dart'; import 'main_screen/uploading_photo.dart'; @@ -178,6 +180,12 @@ class _MainScreenState extends AuthRequiredState with Loadable { message: localizations.mainScreenTakePhotoActionTakingPhoto, ); + if (isTorchEnabled) { + await controller!.setFlashMode(FlashMode.torch); + } else { + await controller!.setFlashMode(FlashMode.off); + } + final file = File((await controller!.takePicture()).path); setState(() { @@ -302,8 +310,11 @@ class _MainScreenState extends AuthRequiredState with Loadable { child: AspectRatio( aspectRatio: 1 / controller!.value.aspectRatio, child: Stack( + fit: StackFit.expand, children: [ controller!.buildPreview(), + if (isRecording) + RecordingOverlay(controller: controller!), if (uploadingPhotoAnimation != null) UploadingPhoto( data: uploadingPhotoAnimation!, @@ -347,7 +358,7 @@ class _MainScreenState extends AuthRequiredState with Loadable { opacityDuration: DEFAULT_OPACITY_DURATION * SECONDARY_BUTTONS_DURATION_MULTIPLIER, child: ChangeCameraButton( - disabled: lockCamera, + disabled: lockCamera || isRecording, onChangeCamera: () { final currentCameraIndex = GlobalValuesManager .cameras @@ -421,10 +432,7 @@ class _MainScreenState extends AuthRequiredState with Loadable { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - ElevatedButton.icon( - icon: const Icon(Icons.flashlight_on_rounded), - label: Text(AppLocalizations.of(context)! - .mainScreenActionsTorchButton), + ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith( @@ -446,6 +454,10 @@ class _MainScreenState extends AuthRequiredState with Loadable { } }); }, + child: IconButtonChild( + icon: const Icon(Icons.flashlight_on_rounded), + label: Text(localizations.mainScreenActionsTorchButton), + ), ), ElevatedButton( style: ButtonStyle( diff --git a/lib/screens/main_screen/record_button.dart b/lib/screens/main_screen/record_button.dart index 297a432..2256379 100644 --- a/lib/screens/main_screen/record_button.dart +++ b/lib/screens/main_screen/record_button.dart @@ -29,7 +29,7 @@ class _RecordButtonState extends State { bool videoInAnimationActive = false; void cancelAnimation() { - if (videoInAnimationActive || animateToVideoIcon) { + if (videoInAnimationActive) { return; } @@ -168,7 +168,7 @@ class _RecordButtonState extends State { decoration: BoxDecoration( color: videoInAnimationActive ? Colors.red : Colors.white, borderRadius: videoInAnimationActive - ? BorderRadius.circular(4) + ? BorderRadius.circular(8) : BorderRadius.circular(50), ), ), diff --git a/lib/screens/main_screen/recording_overlay.dart b/lib/screens/main_screen/recording_overlay.dart new file mode 100644 index 0000000..056712e --- /dev/null +++ b/lib/screens/main_screen/recording_overlay.dart @@ -0,0 +1,115 @@ +import 'dart:async'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; + +class RecordingOverlay extends StatefulWidget { + final CameraController controller; + + const RecordingOverlay({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _RecordingOverlayState(); +} + +class _RecordingOverlayState extends State { + late final Timer _timer; + bool animateIn = false; + bool initialAnimateIn = false; + int recordingTime = 0; + + @override + void initState() { + super.initState(); + + _timer = Timer.periodic(const Duration(seconds: 1), (_) { + setState(() { + if (!mounted) { + return; + } + + recordingTime++; + + animateIn = !animateIn; + }); + }); + + WidgetsBinding.instance.addPostFrameCallback((_) { + initialAnimateIn = true; + }); + } + + @override + void dispose() { + _timer.cancel(); + + super.dispose(); + } + + String getFormattedTime() { + final minutes = recordingTime ~/ 60; + final seconds = recordingTime % 60; + + return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Positioned( + left: 0, + top: SMALL_SPACE, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: initialAnimateIn ? 1.0 : 0.0, + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedOpacity( + curve: Curves.linear, + opacity: animateIn ? 1.0 : 0.0, + duration: const Duration(seconds: 1), + child: Icon( + Icons.circle, + size: platformThemeData( + context, + material: (data) => data.textTheme.subtitle1!.fontSize, + cupertino: (data) => data.textTheme.textStyle.fontSize, + ), + color: Colors.red, + ), + ), + const SizedBox(width: SMALL_SPACE), + Text( + localizations.recordingOverlayIsRecording, + style: platformThemeData( + context, + material: (data) => data.textTheme.bodyLarge, + cupertino: (data) => data.textTheme.textStyle, + ), + ), + const SizedBox(width: SMALL_SPACE), + Text( + getFormattedTime(), + style: platformThemeData( + context, + material: (data) => data.textTheme.bodyLarge, + cupertino: (data) => data.textTheme.textStyle, + ), + ), + ], + ), + ), + ), + ); + } +}