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:share_location/constants/values.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
extension ShowSnackBar on BuildContext {
void showSnackBar({
required String message,
Color backgroundColor = Colors.white,
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason>?
pendingSnackBar;
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();
pendingSnackBar = null;
return ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: backgroundColor,
));
duration: duration,
),
);
}
void showErrorSnackBar({required String message}) {
showSnackBar(message: message, backgroundColor: Colors.red);
void showErrorSnackBar({required final String message}) {
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:typed_data';
import 'package:camera/camera.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/change_camera_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';
class MainScreen extends StatefulWidget {
@ -27,6 +29,7 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
bool isRecording = false;
bool hasGrantedPermissions = false;
List? lastPhoto;
Uint8List? uploadingPhotoAnimation;
late User _user;
@ -114,26 +117,27 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
return;
}
context.showPendingSnackBar(message: 'Taking photo...');
controller!.setFlashMode(FlashMode.off);
final file = File((await controller!.takePicture()).path);
setState(() {
uploadingPhotoAnimation = file.readAsBytesSync();
});
context.showPendingSnackBar(message: 'Uploading photo...');
try {
await FileManager.uploadFile(_user, file);
} catch (error) {
if (mounted) {
context.showErrorSnackBar(message: error.toString());
}
return;
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Photo saved.'),
backgroundColor: Colors.green,
),
);
context.showSuccessSnackBar(message: 'Photo uploaded!');
if (mounted) {
await getLastPhoto();
}
}
@ -148,8 +152,12 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
isRecording = false;
});
context.showPendingSnackBar(message: 'Saving video...');
final file = File((await controller!.stopVideoRecording()).path);
context.showPendingSnackBar(message: 'Uploading video...');
try {
await FileManager.uploadFile(_user, file);
} catch (error) {
@ -159,13 +167,9 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
return;
}
context.showSuccessSnackBar(message: 'Video uploaded!');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Video saved.'),
backgroundColor: Colors.green,
),
);
await getLastPhoto();
}
}
@ -211,7 +215,8 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
ChangeCameraButton(onChangeCamera: () {
ChangeCameraButton(
onChangeCamera: () {
final currentCameraIndex = GlobalValuesManager
.cameras
.indexOf(controller!.description);
@ -220,9 +225,11 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
onNewCameraSelected(
GlobalValuesManager.cameras[
(currentCameraIndex + 1) % availableCameras],
(currentCameraIndex + 1) %
availableCameras],
);
}),
},
),
CameraButton(
active: isRecording,
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/services.dart';
class CameraButton extends StatelessWidget {
class CameraButton extends StatefulWidget {
final bool active;
final VoidCallback onPhotoShot;
final VoidCallback onVideoBegin;
final VoidCallback onVideoEnd;
final bool disabled;
const CameraButton({
Key? key,
@ -13,32 +15,76 @@ class CameraButton extends StatelessWidget {
required this.onVideoBegin,
required this.onVideoEnd,
this.active = false,
this.disabled = false,
}) : super(key: key);
@override
State<CameraButton> createState() => _CameraButtonState();
}
class _CameraButtonState extends State<CameraButton> {
bool shrinkIcon = false;
@override
Widget build(BuildContext context) {
return InkWell(
return GestureDetector(
onTap: () {
if (widget.disabled) {
return;
}
setState(() {
shrinkIcon = false;
});
HapticFeedback.heavyImpact();
if (active) {
onVideoEnd();
if (widget.active) {
widget.onVideoEnd();
} 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: () {
if (widget.disabled) {
return;
}
HapticFeedback.heavyImpact();
if (active) {
onVideoEnd();
if (widget.active) {
widget.onVideoEnd();
} else {
onVideoBegin();
widget.onVideoBegin();
}
},
child: Opacity(
opacity: widget.disabled ? 0.5 : 1.0,
child: Stack(
alignment: Alignment.center,
children: active
children: widget.active
? const <Widget>[
Icon(
Icons.circle,
@ -62,13 +108,19 @@ class CameraButton extends StatelessWidget {
size: 75,
color: Colors.white.withOpacity(.2),
),
const Icon(
AnimatedScale(
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 {
final Uint8List data;
final MemoryType type;
final bool loopVideo;
final String? filename;
const RawMemoryDisplay({
Key? key,
required this.data,
required this.type,
this.loopVideo = false,
this.filename,
}) : super(key: key);
@ -56,7 +58,7 @@ class _RawMemoryDisplayState extends State<RawMemoryDisplay> {
videoController = VideoPlayerController.file(file);
videoController!.initialize().then((value) {
setState(() {});
videoController!.setLooping(true);
videoController!.setLooping(widget.loopVideo);
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),
),
);
}
}