diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 5a9aff2..86b0d66 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:camera/camera.dart'; +import 'package:expandable_bottom_sheet/expandable_bottom_sheet.dart'; import 'package:flutter/material.dart'; import 'package:share_location/constants/spacing.dart'; import 'package:share_location/constants/values.dart'; @@ -30,6 +31,7 @@ class MainScreen extends StatefulWidget { class _MainScreenState extends AuthRequiredState with Loadable { bool isRecording = false; bool lockCamera = false; + bool isTorchEnabled = false; List? lastPhoto; Uint8List? uploadingPhotoAnimation; @@ -208,123 +210,154 @@ class _MainScreenState extends AuthRequiredState with Loadable { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, - body: SafeArea( - child: () { - if (isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } + bottomSheet: () { + if (isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } - return SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Stack( - alignment: Alignment.center, - children: [ - Column( + return Container( + color: Colors.black, + child: ExpandableBottomSheet( + background: SafeArea( + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Stack( + alignment: Alignment.center, children: [ - AnimateInBuilder( - builder: (showPreview) => AnimatedOpacity( - opacity: showPreview ? 1.0 : 0.0, - duration: const Duration(milliseconds: 1100), - curve: Curves.easeOutQuad, - child: ClipRRect( - borderRadius: BorderRadius.circular(SMALL_SPACE), - child: AspectRatio( - aspectRatio: 1 / controller!.value.aspectRatio, - child: controller!.buildPreview(), + Align( + alignment: Alignment.topCenter, + child: AnimateInBuilder( + builder: (showPreview) => AnimatedOpacity( + opacity: showPreview ? 1.0 : 0.0, + duration: const Duration(milliseconds: 1100), + curve: Curves.easeOutQuad, + child: ClipRRect( + borderRadius: BorderRadius.circular(SMALL_SPACE), + child: AspectRatio( + aspectRatio: 1 / controller!.value.aspectRatio, + child: controller!.buildPreview(), + ), ), ), ), ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: LARGE_SPACE), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FadeAndMoveInAnimation( - translationDuration: - DEFAULT_TRANSLATION_DURATION * - SECONDARY_BUTTONS_DURATION_MULTIPLIER, - opacityDuration: DEFAULT_OPACITY_DURATION * - SECONDARY_BUTTONS_DURATION_MULTIPLIER, - child: ChangeCameraButton( - onChangeCamera: () { - final currentCameraIndex = - GlobalValuesManager.cameras - .indexOf(controller!.description); - final availableCameras = - GlobalValuesManager.cameras.length; - - onNewCameraSelected( - GlobalValuesManager.cameras[ - (currentCameraIndex + 1) % - availableCameras], - ); - }, - ), - ), - FadeAndMoveInAnimation( - child: CameraButton( - disabled: lockCamera, - active: isRecording, - onVideoBegin: () async { - setState(() { - isRecording = true; - }); - - if (controller!.value.isRecordingVideo) { - // A recording has already started, do nothing. - return; - } - - await controller!.startVideoRecording(); - }, - onVideoEnd: takeVideo, - onPhotoShot: takePhoto, - ), - ), - FadeAndMoveInAnimation( - translationDuration: - DEFAULT_TRANSLATION_DURATION * - SECONDARY_BUTTONS_DURATION_MULTIPLIER, - opacityDuration: DEFAULT_OPACITY_DURATION * - SECONDARY_BUTTONS_DURATION_MULTIPLIER, - child: lastPhoto == null - ? const TodayPhotoButton() - : TodayPhotoButton( - data: lastPhoto![0], - type: lastPhoto![1], - ), - ), - ], - ), - ) - ], + if (uploadingPhotoAnimation != null) + UploadingPhoto( + data: uploadingPhotoAnimation!, + onDone: () { + setState(() { + uploadingPhotoAnimation = null; + }); + }, ), + ], + ), + ), + ), + persistentHeader: Container( + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(LARGE_SPACE), + topRight: Radius.circular(LARGE_SPACE), + ), + ), + child: Padding( + padding: const EdgeInsets.all(LARGE_SPACE), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FadeAndMoveInAnimation( + translationDuration: DEFAULT_TRANSLATION_DURATION * + SECONDARY_BUTTONS_DURATION_MULTIPLIER, + opacityDuration: DEFAULT_OPACITY_DURATION * + SECONDARY_BUTTONS_DURATION_MULTIPLIER, + child: ChangeCameraButton( + onChangeCamera: () { + final currentCameraIndex = GlobalValuesManager.cameras + .indexOf(controller!.description); + final availableCameras = + GlobalValuesManager.cameras.length; + + onNewCameraSelected( + GlobalValuesManager.cameras[ + (currentCameraIndex + 1) % availableCameras], + ); + }, + ), + ), + FadeAndMoveInAnimation( + child: CameraButton( + disabled: lockCamera, + active: isRecording, + onVideoBegin: () async { + setState(() { + isRecording = true; + }); + + if (controller!.value.isRecordingVideo) { + // A recording has already started, do nothing. + return; + } + + await controller!.startVideoRecording(); + }, + onVideoEnd: takeVideo, + onPhotoShot: takePhoto, + ), + ), + FadeAndMoveInAnimation( + translationDuration: DEFAULT_TRANSLATION_DURATION * + SECONDARY_BUTTONS_DURATION_MULTIPLIER, + opacityDuration: DEFAULT_OPACITY_DURATION * + SECONDARY_BUTTONS_DURATION_MULTIPLIER, + child: lastPhoto == null + ? const TodayPhotoButton() + : TodayPhotoButton( + data: lastPhoto![0], + type: lastPhoto![1], + ), ), ], ), - if (uploadingPhotoAnimation != null) - UploadingPhoto( - data: uploadingPhotoAnimation!, - onDone: () { + ), + ), + expandableContent: Padding( + padding: const EdgeInsets.all(LARGE_SPACE), + child: Row( + children: [ + ElevatedButton.icon( + icon: const Icon(Icons.flashlight_on_rounded), + label: const Text('Torch'), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith( + (_) => isTorchEnabled ? Colors.white : Colors.black, + ), + foregroundColor: MaterialStateProperty.resolveWith( + (_) => isTorchEnabled ? Colors.black : Colors.white, + ), + ), + onPressed: () { setState(() { - uploadingPhotoAnimation = null; + isTorchEnabled = !isTorchEnabled; + + if (isTorchEnabled) { + controller!.setFlashMode(FlashMode.torch); + } else { + controller!.setFlashMode(FlashMode.off); + } }); }, ), - ], + ], + ), ), - ); - }(), - ), + ), + ); + }(), ); } } diff --git a/pubspec.lock b/pubspec.lock index 44d56ce..b21896a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -106,6 +106,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + expandable_bottom_sheet: + dependency: "direct main" + description: + name: expandable_bottom_sheet + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1+1" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e492c1..a71e72d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: provider: ^6.0.3 gallery_saver: ^2.3.2 fluttertoast: ^8.0.9 + expandable_bottom_sheet: ^1.1.1+1 dev_dependencies: flutter_test: