improved UX

This commit is contained in:
Myzel394 2022-08-13 21:54:37 +02:00
parent b0d0d709dd
commit 3904c4dbef
6 changed files with 255 additions and 77 deletions

View File

@ -0,0 +1 @@
const DURATION_INFINITY = Duration(days: 999);

View File

@ -1,20 +1,49 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:share_location/constants/values.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client; final supabase = Supabase.instance.client;
extension ShowSnackBar on BuildContext { extension ShowSnackBar on BuildContext {
void showSnackBar({ static ScaffoldFeatureController<SnackBar, SnackBarClosedReason>?
required String message, pendingSnackBar;
Color backgroundColor = Colors.white,
ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar({
required final String message,
final Color backgroundColor = Colors.white,
final Duration duration = const Duration(seconds: 4),
}) { }) {
ScaffoldMessenger.of(this).showSnackBar(SnackBar( pendingSnackBar?.close();
content: Text(message), pendingSnackBar = null;
backgroundColor: backgroundColor,
)); return ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: backgroundColor,
duration: duration,
),
);
} }
void showErrorSnackBar({required String message}) { void showErrorSnackBar({required final String message}) {
showSnackBar(message: message, backgroundColor: Colors.red); showSnackBar(
message: message,
backgroundColor: Colors.red,
);
}
void showSuccessSnackBar({required final String message}) {
showSnackBar(
message: message,
backgroundColor: Colors.green,
);
}
void showPendingSnackBar({required final String message}) {
pendingSnackBar = showSnackBar(
message: message,
backgroundColor: Colors.yellow,
duration: DURATION_INFINITY,
);
} }
} }

View File

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -12,6 +13,7 @@ import 'package:share_location/utils/loadable.dart';
import 'package:share_location/widgets/camera_button.dart'; import 'package:share_location/widgets/camera_button.dart';
import 'package:share_location/widgets/change_camera_button.dart'; import 'package:share_location/widgets/change_camera_button.dart';
import 'package:share_location/widgets/today_photo_button.dart'; import 'package:share_location/widgets/today_photo_button.dart';
import 'package:share_location/widgets/uploading_photo.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
class MainScreen extends StatefulWidget { class MainScreen extends StatefulWidget {
@ -27,6 +29,7 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
bool isRecording = false; bool isRecording = false;
bool hasGrantedPermissions = false; bool hasGrantedPermissions = false;
List? lastPhoto; List? lastPhoto;
Uint8List? uploadingPhotoAnimation;
late User _user; late User _user;
@ -114,26 +117,27 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
return; return;
} }
context.showPendingSnackBar(message: 'Taking photo...');
controller!.setFlashMode(FlashMode.off); controller!.setFlashMode(FlashMode.off);
final file = File((await controller!.takePicture()).path); final file = File((await controller!.takePicture()).path);
setState(() {
uploadingPhotoAnimation = file.readAsBytesSync();
});
context.showPendingSnackBar(message: 'Uploading photo...');
try { try {
await FileManager.uploadFile(_user, file); await FileManager.uploadFile(_user, file);
} catch (error) { } catch (error) {
if (mounted) { context.showErrorSnackBar(message: error.toString());
context.showErrorSnackBar(message: error.toString());
}
return; return;
} }
if (mounted) { context.showSuccessSnackBar(message: 'Photo uploaded!');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Photo saved.'),
backgroundColor: Colors.green,
),
);
if (mounted) {
await getLastPhoto(); await getLastPhoto();
} }
} }
@ -148,8 +152,12 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
isRecording = false; isRecording = false;
}); });
context.showPendingSnackBar(message: 'Saving video...');
final file = File((await controller!.stopVideoRecording()).path); final file = File((await controller!.stopVideoRecording()).path);
context.showPendingSnackBar(message: 'Uploading video...');
try { try {
await FileManager.uploadFile(_user, file); await FileManager.uploadFile(_user, file);
} catch (error) { } catch (error) {
@ -159,13 +167,9 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
return; return;
} }
context.showSuccessSnackBar(message: 'Video uploaded!');
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video saved.'),
backgroundColor: Colors.green,
),
);
await getLastPhoto(); await getLastPhoto();
} }
} }
@ -211,18 +215,21 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
ChangeCameraButton(onChangeCamera: () { ChangeCameraButton(
final currentCameraIndex = GlobalValuesManager onChangeCamera: () {
.cameras final currentCameraIndex = GlobalValuesManager
.indexOf(controller!.description); .cameras
final availableCameras = .indexOf(controller!.description);
GlobalValuesManager.cameras.length; final availableCameras =
GlobalValuesManager.cameras.length;
onNewCameraSelected( onNewCameraSelected(
GlobalValuesManager.cameras[ GlobalValuesManager.cameras[
(currentCameraIndex + 1) % availableCameras], (currentCameraIndex + 1) %
); availableCameras],
}), );
},
),
CameraButton( CameraButton(
active: isRecording, active: isRecording,
onVideoBegin: () async { onVideoBegin: () async {
@ -251,6 +258,15 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
) )
], ],
), ),
if (uploadingPhotoAnimation != null)
UploadingPhoto(
data: uploadingPhotoAnimation!,
onDone: () {
setState(() {
uploadingPhotoAnimation = null;
});
},
)
], ],
), ),
); );

View File

@ -1,11 +1,13 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class CameraButton extends StatelessWidget { class CameraButton extends StatefulWidget {
final bool active; final bool active;
final VoidCallback onPhotoShot; final VoidCallback onPhotoShot;
final VoidCallback onVideoBegin; final VoidCallback onVideoBegin;
final VoidCallback onVideoEnd; final VoidCallback onVideoEnd;
final bool disabled;
const CameraButton({ const CameraButton({
Key? key, Key? key,
@ -13,61 +15,111 @@ class CameraButton extends StatelessWidget {
required this.onVideoBegin, required this.onVideoBegin,
required this.onVideoEnd, required this.onVideoEnd,
this.active = false, this.active = false,
this.disabled = false,
}) : super(key: key); }) : super(key: key);
@override
State<CameraButton> createState() => _CameraButtonState();
}
class _CameraButtonState extends State<CameraButton> {
bool shrinkIcon = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return GestureDetector(
onTap: () { onTap: () {
if (widget.disabled) {
return;
}
setState(() {
shrinkIcon = false;
});
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
if (active) { if (widget.active) {
onVideoEnd(); widget.onVideoEnd();
} else { } else {
onPhotoShot(); widget.onPhotoShot();
}
},
onLongPressDown: (_) {
if (widget.disabled) {
return;
}
setState(() {
shrinkIcon = true;
});
},
onLongPressUp: () {
if (widget.disabled) {
return;
}
setState(() {
shrinkIcon = false;
});
if (widget.active) {
widget.onVideoEnd();
} }
}, },
onLongPress: () { onLongPress: () {
if (widget.disabled) {
return;
}
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
if (active) { if (widget.active) {
onVideoEnd(); widget.onVideoEnd();
} else { } else {
onVideoBegin(); widget.onVideoBegin();
} }
}, },
child: Stack( child: Opacity(
alignment: Alignment.center, opacity: widget.disabled ? 0.5 : 1.0,
children: active child: Stack(
? const <Widget>[ alignment: Alignment.center,
Icon( children: widget.active
Icons.circle, ? const <Widget>[
size: 75, Icon(
color: Colors.white, Icons.circle,
), size: 75,
Icon( color: Colors.white,
Icons.circle, ),
size: 65, Icon(
color: Colors.red, Icons.circle,
), size: 65,
Icon( color: Colors.red,
Icons.stop, ),
size: 45, Icon(
color: Colors.white, Icons.stop,
), size: 45,
] color: Colors.white,
: <Widget>[ ),
Icon( ]
Icons.circle, : <Widget>[
size: 75, Icon(
color: Colors.white.withOpacity(.2), Icons.circle,
), size: 75,
const Icon( color: Colors.white.withOpacity(.2),
Icons.circle, ),
size: 50, AnimatedScale(
color: Colors.white, duration: kLongPressTimeout,
), curve: Curves.easeInOut,
], scale: shrinkIcon ? .8 : 1,
child: const Icon(
Icons.circle,
size: 50,
color: Colors.white,
),
),
],
),
), ),
); );
} }

View File

@ -9,12 +9,14 @@ import 'package:video_player/video_player.dart';
class RawMemoryDisplay extends StatefulWidget { class RawMemoryDisplay extends StatefulWidget {
final Uint8List data; final Uint8List data;
final MemoryType type; final MemoryType type;
final bool loopVideo;
final String? filename; final String? filename;
const RawMemoryDisplay({ const RawMemoryDisplay({
Key? key, Key? key,
required this.data, required this.data,
required this.type, required this.type,
this.loopVideo = false,
this.filename, this.filename,
}) : super(key: key); }) : super(key: key);
@ -56,7 +58,7 @@ class _RawMemoryDisplayState extends State<RawMemoryDisplay> {
videoController = VideoPlayerController.file(file); videoController = VideoPlayerController.file(file);
videoController!.initialize().then((value) { videoController!.initialize().then((value) {
setState(() {}); setState(() {});
videoController!.setLooping(true); videoController!.setLooping(widget.loopVideo);
videoController!.play(); videoController!.play();
}); });
} }

View File

@ -0,0 +1,78 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:share_location/constants/spacing.dart';
class UploadingPhoto extends StatefulWidget {
final Uint8List data;
final VoidCallback onDone;
const UploadingPhoto({
Key? key,
required this.data,
required this.onDone,
}) : super(key: key);
@override
State<UploadingPhoto> createState() => _UploadingPhotoState();
}
class _UploadingPhotoState extends State<UploadingPhoto>
with TickerProviderStateMixin {
late final AnimationController controller;
late final Animation<double> animation;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
animation = Tween(begin: 1.0, end: 0.8).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOutQuad,
),
);
controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
widget.onDone();
}
});
}
});
controller.forward();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: animation,
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 15,
),
borderRadius: BorderRadius.circular(SMALL_SPACE),
),
child: Image.memory(widget.data, fit: BoxFit.cover),
),
);
}
}