mirror of
https://github.com/Myzel394/quid_faciam_hodie.git
synced 2025-06-19 15:45:26 +02:00
improved UI & UX & DX
This commit is contained in:
parent
b146957c69
commit
4bcafd991d
@ -1 +1,3 @@
|
||||
const DURATION_INFINITY = Duration(days: 999);
|
||||
const SECONDARY_BUTTONS_DURATION_MULTIPLIER = 1.8;
|
||||
const PHOTO_SHOW_AFTER_CREATION_DURATION = Duration(milliseconds: 500);
|
||||
|
@ -2,6 +2,7 @@ import 'package:camera/camera.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share_location/constants/apis.dart';
|
||||
import 'package:share_location/screens/grant_permission_screen.dart';
|
||||
import 'package:share_location/screens/login_screen.dart';
|
||||
import 'package:share_location/screens/main_screen.dart';
|
||||
import 'package:share_location/screens/timeline_screen.dart';
|
||||
@ -60,6 +61,7 @@ class MyApp extends StatelessWidget {
|
||||
MainScreen.ID: (context) => const MainScreen(),
|
||||
LoginScreen.ID: (context) => const LoginScreen(),
|
||||
TimelineScreen.ID: (context) => const TimelineScreen(),
|
||||
GrantPermissionScreen.ID: (context) => const GrantPermissionScreen(),
|
||||
},
|
||||
initialRoute: initialPage,
|
||||
);
|
||||
|
26
lib/screens/grant_permission_screen.dart
Normal file
26
lib/screens/grant_permission_screen.dart
Normal file
@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share_location/screens/main_screen.dart';
|
||||
|
||||
import 'main_screen/permissions_required_page.dart';
|
||||
|
||||
class GrantPermissionScreen extends StatelessWidget {
|
||||
static const ID = 'grant_permission';
|
||||
|
||||
const GrantPermissionScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Grant Permission'),
|
||||
),
|
||||
body: Center(
|
||||
child: PermissionsRequiredPage(
|
||||
onPermissionsGranted: () {
|
||||
Navigator.pushReplacementNamed(context, MainScreen.ID);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -4,14 +4,16 @@ import 'dart:typed_data';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share_location/constants/spacing.dart';
|
||||
import 'package:share_location/constants/values.dart';
|
||||
import 'package:share_location/extensions/snackbar.dart';
|
||||
import 'package:share_location/managers/file_manager.dart';
|
||||
import 'package:share_location/managers/global_values_manager.dart';
|
||||
import 'package:share_location/screens/main_screen/permissions_required_page.dart';
|
||||
import 'package:share_location/utils/auth_required.dart';
|
||||
import 'package:share_location/utils/loadable.dart';
|
||||
import 'package:share_location/widgets/animate_in_builder.dart';
|
||||
import 'package:share_location/widgets/camera_button.dart';
|
||||
import 'package:share_location/widgets/change_camera_button.dart';
|
||||
import 'package:share_location/widgets/fade_and_move_in_animation.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';
|
||||
@ -27,7 +29,6 @@ class MainScreen extends StatefulWidget {
|
||||
|
||||
class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
||||
bool isRecording = false;
|
||||
bool hasGrantedPermissions = false;
|
||||
bool lockCamera = false;
|
||||
List? lastPhoto;
|
||||
Uint8List? uploadingPhotoAnimation;
|
||||
@ -45,6 +46,7 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
||||
super.initState();
|
||||
|
||||
callWithLoading(getLastPhoto);
|
||||
onNewCameraSelected(GlobalValuesManager.cameras[0]);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -55,6 +57,10 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
_updateCamera(state);
|
||||
}
|
||||
|
||||
void _updateCamera(final AppLifecycleState state) {
|
||||
final CameraController? cameraController = controller;
|
||||
|
||||
// App state changed before we got the chance to initialize.
|
||||
@ -109,6 +115,7 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
@ -203,20 +210,6 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: () {
|
||||
if (!hasGrantedPermissions) {
|
||||
return Center(
|
||||
child: PermissionsRequiredPage(
|
||||
onPermissionsGranted: () {
|
||||
onNewCameraSelected(GlobalValuesManager.cameras[0]);
|
||||
|
||||
setState(() {
|
||||
hasGrantedPermissions = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
@ -229,57 +222,93 @@ class _MainScreenState extends AuthRequiredState<MainScreen> with Loadable {
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
controller!.buildPreview(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(LARGE_SPACE),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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: <Widget>[
|
||||
ChangeCameraButton(
|
||||
onChangeCamera: () {
|
||||
final currentCameraIndex = GlobalValuesManager
|
||||
.cameras
|
||||
.indexOf(controller!.description);
|
||||
final availableCameras =
|
||||
GlobalValuesManager.cameras.length;
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: LARGE_SPACE),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
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],
|
||||
);
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
lastPhoto == null
|
||||
? const TodayPhotoButton()
|
||||
: TodayPhotoButton(
|
||||
data: lastPhoto![0],
|
||||
type: lastPhoto![1],
|
||||
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)
|
||||
|
@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share_location/constants/spacing.dart';
|
||||
import 'package:share_location/managers/startup_page_manager.dart';
|
||||
import 'package:share_location/screens/main_screen.dart';
|
||||
import 'package:share_location/widgets/logo.dart';
|
||||
|
||||
import 'grant_permission_screen.dart';
|
||||
|
||||
class WelcomeScreen extends StatelessWidget {
|
||||
static const ID = 'welcome';
|
||||
|
||||
@ -43,7 +44,10 @@ class WelcomeScreen extends StatelessWidget {
|
||||
icon: const Icon(Icons.arrow_right),
|
||||
label: const Text('Start'),
|
||||
onPressed: () {
|
||||
StartupPageManager.navigateToNewPage(context, MainScreen.ID);
|
||||
StartupPageManager.navigateToNewPage(
|
||||
context,
|
||||
GrantPermissionScreen.ID,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
33
lib/widgets/animate_in_builder.dart
Normal file
33
lib/widgets/animate_in_builder.dart
Normal file
@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimateInBuilder extends StatefulWidget {
|
||||
final Widget Function(bool isActive) builder;
|
||||
|
||||
const AnimateInBuilder({
|
||||
Key? key,
|
||||
required this.builder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AnimateInBuilder> createState() => _AnimateInBuilderState();
|
||||
}
|
||||
|
||||
class _AnimateInBuilderState extends State<AnimateInBuilder> {
|
||||
bool isActive = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
isActive = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(isActive);
|
||||
}
|
||||
}
|
49
lib/widgets/delay_render.dart
Normal file
49
lib/widgets/delay_render.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DelayRender extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Widget placeholder;
|
||||
final Duration delay;
|
||||
|
||||
const DelayRender({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.placeholder = const SizedBox(),
|
||||
this.delay = const Duration(milliseconds: 120),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<DelayRender> createState() => _DelayRenderState();
|
||||
}
|
||||
|
||||
class _DelayRenderState extends State<DelayRender> {
|
||||
bool allowRendering = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
startTimer();
|
||||
}
|
||||
|
||||
Future<void> startTimer() async {
|
||||
await Future.delayed(widget.delay);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
allowRendering = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!allowRendering) {
|
||||
return widget.placeholder;
|
||||
}
|
||||
|
||||
return widget.child;
|
||||
}
|
||||
}
|
94
lib/widgets/fade_and_move_in_animation.dart
Normal file
94
lib/widgets/fade_and_move_in_animation.dart
Normal file
@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const DEFAULT_TRANSLATION_DURATION = Duration(milliseconds: 500);
|
||||
const DEFAULT_OPACITY_DURATION = Duration(milliseconds: 800);
|
||||
|
||||
class FadeAndMoveInAnimation extends StatefulWidget {
|
||||
final Widget child;
|
||||
final bool active;
|
||||
|
||||
final Offset translationOffset;
|
||||
final Duration translationDuration;
|
||||
final Curve translationCurve;
|
||||
|
||||
final Duration opacityDuration;
|
||||
final Curve opacityCurve;
|
||||
|
||||
const FadeAndMoveInAnimation({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.active = true,
|
||||
this.translationOffset = const Offset(0, 60),
|
||||
this.translationDuration = DEFAULT_TRANSLATION_DURATION,
|
||||
this.translationCurve = Curves.easeOutQuad,
|
||||
this.opacityDuration = DEFAULT_OPACITY_DURATION,
|
||||
this.opacityCurve = Curves.linearToEaseOut,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<FadeAndMoveInAnimation> createState() => _FadeAndMoveInAnimationState();
|
||||
}
|
||||
|
||||
class _FadeAndMoveInAnimationState extends State<FadeAndMoveInAnimation>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController translationController;
|
||||
late final Animation<double> translationAnimation;
|
||||
|
||||
bool opacityEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
translationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.translationDuration,
|
||||
);
|
||||
translationAnimation = Tween<double>(
|
||||
begin: 1,
|
||||
end: 0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: translationController,
|
||||
curve: widget.translationCurve,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
if (widget.active) {
|
||||
translationController.forward();
|
||||
} else {
|
||||
translationController.reverse();
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
opacityEnabled = widget.active;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: translationController,
|
||||
child: widget.child,
|
||||
builder: (context, child) => Transform.translate(
|
||||
offset: Offset(
|
||||
widget.translationOffset.dx * translationAnimation.value,
|
||||
widget.translationOffset.dy * translationAnimation.value,
|
||||
),
|
||||
child: AnimatedOpacity(
|
||||
duration: widget.opacityDuration,
|
||||
curve: widget.opacityCurve,
|
||||
opacity: opacityEnabled ? 1 : 0,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:share_location/constants/spacing.dart';
|
||||
import 'package:share_location/constants/values.dart';
|
||||
|
||||
class UploadingPhoto extends StatefulWidget {
|
||||
final Uint8List data;
|
||||
@ -39,7 +40,7 @@ class _UploadingPhotoState extends State<UploadingPhoto>
|
||||
|
||||
controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
Future.delayed(PHOTO_SHOW_AFTER_CREATION_DURATION, () {
|
||||
if (mounted) {
|
||||
widget.onDone();
|
||||
}
|
||||
@ -67,9 +68,9 @@ class _UploadingPhotoState extends State<UploadingPhoto>
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 15,
|
||||
width: 12,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(SMALL_SPACE),
|
||||
borderRadius: BorderRadius.circular(MEDIUM_SPACE),
|
||||
),
|
||||
child: Image.memory(widget.data, fit: BoxFit.cover),
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user