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: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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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