mirror of
https://github.com/Myzel394/quid_faciam_hodie.git
synced 2025-06-19 15:45:26 +02:00
improved UX
This commit is contained in:
parent
b0d0d709dd
commit
3904c4dbef
1
lib/constants/values.dart
Normal file
1
lib/constants/values.dart
Normal file
@ -0,0 +1 @@
|
||||
const DURATION_INFINITY = Duration(days: 999);
|
@ -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(
|
||||
content: Text(message),
|
||||
backgroundColor: backgroundColor,
|
||||
));
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
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,18 +215,21 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
ChangeCameraButton(onChangeCamera: () {
|
||||
final currentCameraIndex = GlobalValuesManager
|
||||
.cameras
|
||||
.indexOf(controller!.description);
|
||||
final availableCameras =
|
||||
GlobalValuesManager.cameras.length;
|
||||
ChangeCameraButton(
|
||||
onChangeCamera: () {
|
||||
final currentCameraIndex = GlobalValuesManager
|
||||
.cameras
|
||||
.indexOf(controller!.description);
|
||||
final availableCameras =
|
||||
GlobalValuesManager.cameras.length;
|
||||
|
||||
onNewCameraSelected(
|
||||
GlobalValuesManager.cameras[
|
||||
(currentCameraIndex + 1) % availableCameras],
|
||||
);
|
||||
}),
|
||||
onNewCameraSelected(
|
||||
GlobalValuesManager.cameras[
|
||||
(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;
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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,61 +15,111 @@ 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: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: active
|
||||
? const <Widget>[
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 75,
|
||||
color: Colors.white,
|
||||
),
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 65,
|
||||
color: Colors.red,
|
||||
),
|
||||
Icon(
|
||||
Icons.stop,
|
||||
size: 45,
|
||||
color: Colors.white,
|
||||
),
|
||||
]
|
||||
: <Widget>[
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 75,
|
||||
color: Colors.white.withOpacity(.2),
|
||||
),
|
||||
const Icon(
|
||||
Icons.circle,
|
||||
size: 50,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
child: Opacity(
|
||||
opacity: widget.disabled ? 0.5 : 1.0,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: widget.active
|
||||
? const <Widget>[
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 75,
|
||||
color: Colors.white,
|
||||
),
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 65,
|
||||
color: Colors.red,
|
||||
),
|
||||
Icon(
|
||||
Icons.stop,
|
||||
size: 45,
|
||||
color: Colors.white,
|
||||
),
|
||||
]
|
||||
: <Widget>[
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 75,
|
||||
color: Colors.white.withOpacity(.2),
|
||||
),
|
||||
AnimatedScale(
|
||||
duration: kLongPressTimeout,
|
||||
curve: Curves.easeInOut,
|
||||
scale: shrinkIcon ? .8 : 1,
|
||||
child: const Icon(
|
||||
Icons.circle,
|
||||
size: 50,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
78
lib/widgets/uploading_photo.dart
Normal file
78
lib/widgets/uploading_photo.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user