diff --git a/assets/images/home.svg b/assets/images/home.svg new file mode 100644 index 0000000..fc25f0b --- /dev/null +++ b/assets/images/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/stats.svg b/assets/images/stats.svg new file mode 100644 index 0000000..6191e14 --- /dev/null +++ b/assets/images/stats.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/images/success.svg b/assets/images/success.svg new file mode 100644 index 0000000..07c6cc4 --- /dev/null +++ b/assets/images/success.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/sounds/point.mp3 b/assets/sounds/point.mp3 new file mode 100644 index 0000000..d130cce Binary files /dev/null and b/assets/sounds/point.mp3 differ diff --git a/assets/sounds/wind.mp3 b/assets/sounds/wind.mp3 new file mode 100644 index 0000000..e20ee10 Binary files /dev/null and b/assets/sounds/wind.mp3 differ diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..9959c24 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,365 @@ +SF:lib/utils/animation/scale_on_press_widget.dart +DA:7,3 +DA:14,3 +DA:22,4 +DA:23,4 +DA:29,0 +DA:31,2 +DA:33,1 +DA:34,2 +DA:35,1 +DA:39,4 +DA:41,4 +DA:43,2 +DA:44,0 +DA:45,2 +DA:46,0 +DA:47,4 +DA:48,4 +DA:49,4 +DA:50,8 +DA:51,4 +DA:52,8 +DA:53,4 +DA:54,8 +DA:55,6 +DA:57,4 +DA:58,8 +DA:60,8 +DA:61,4 +DA:62,4 +DA:63,4 +DA:65,4 +DA:66,4 +DA:67,4 +DA:70,4 +DA:72,8 +LF:35 +LH:32 +end_of_record +SF:lib/utils/animation/animated_scale.dart +DA:5,4 +DA:13,8 +DA:14,4 +DA:22,4 +DA:23,4 +DA:30,4 +DA:32,20 +DA:33,8 +DA:37,4 +DA:39,16 +DA:42,4 +DA:43,4 +DA:44,4 +DA:45,8 +DA:46,8 +LF:15 +LH:15 +end_of_record +SF:lib/common/resources/r.dart +DA:2,9 +DA:3,9 +DA:4,6 +DA:5,9 +DA:6,3 +DA:7,6 +LF:6 +LH:6 +end_of_record +SF:lib/features/home/widgets/home_app_bar.dart +DA:9,0 +DA:11,1 +DA:13,1 +DA:15,1 +DA:16,1 +DA:23,1 +DA:24,2 +DA:25,1 +DA:27,3 +DA:34,0 +DA:39,1 +DA:41,1 +DA:42,1 +DA:43,2 +DA:44,0 +DA:45,1 +DA:46,2 +DA:47,1 +DA:48,1 +DA:49,1 +LF:20 +LH:17 +end_of_record +SF:lib/features/home/widgets/home_bottom_nav_bar.dart +DA:7,0 +DA:9,1 +DA:10,1 +DA:17,0 +DA:18,0 +DA:19,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:29,0 +DA:31,0 +DA:33,0 +DA:35,0 +DA:37,0 +DA:39,1 +DA:41,1 +DA:49,1 +DA:54,1 +DA:62,1 +DA:64,1 +DA:65,1 +DA:66,1 +DA:67,1 +DA:68,1 +DA:69,1 +DA:70,1 +DA:72,2 +DA:73,1 +DA:76,1 +DA:77,1 +DA:78,1 +DA:79,1 +DA:80,1 +DA:81,1 +DA:83,2 +DA:84,1 +LF:36 +LH:24 +end_of_record +SF:lib/features/home/widgets/home_fab.dart +DA:8,0 +DA:10,1 +DA:12,1 +DA:15,0 +DA:16,0 +DA:18,2 +DA:20,1 +DA:22,1 +DA:23,2 +DA:25,1 +DA:28,1 +LF:11 +LH:8 +end_of_record +SF:lib/features/home/widgets/home_screen.dart +DA:10,0 +DA:12,1 +DA:14,1 +DA:16,1 +LF:4 +LH:3 +end_of_record +SF:lib/features/home/widgets/animated_circle_progress.dart +DA:9,0 +DA:14,0 +DA:15,0 +DA:21,1 +DA:23,1 +DA:32,1 +DA:34,1 +DA:35,5 +DA:36,2 +DA:38,2 +DA:39,3 +DA:40,2 +DA:41,4 +DA:42,2 +DA:43,2 +DA:45,2 +DA:48,1 +DA:50,1 +DA:52,5 +DA:53,5 +DA:57,1 +DA:59,2 +DA:60,2 +DA:61,1 +DA:66,1 +DA:67,9 +DA:77,1 +DA:79,2 +DA:80,1 +DA:81,1 +DA:82,1 +DA:83,2 +DA:85,1 +DA:86,1 +DA:87,2 +DA:88,1 +DA:89,1 +DA:90,5 +DA:91,1 +DA:92,1 +DA:94,1 +DA:95,1 +DA:96,5 +DA:97,1 +DA:98,1 +DA:99,1 +DA:100,1 +DA:102,1 +DA:105,0 +LF:49 +LH:45 +end_of_record +SF:lib/utils/extensions/gradient_extensions.dart +DA:4,3 +DA:5,6 +DA:8,1 +DA:9,2 +DA:12,4 +DA:13,4 +DA:15,4 +DA:16,4 +DA:22,2 +DA:25,2 +DA:26,2 +DA:27,2 +DA:35,12 +DA:36,6 +DA:38,4 +DA:39,4 +LF:16 +LH:16 +end_of_record +SF:lib/features/home/widgets/animated_nav_button.dart +DA:7,1 +DA:14,1 +DA:21,1 +DA:23,1 +DA:25,1 +DA:26,1 +DA:27,1 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,1 +DA:37,1 +DA:42,1 +DA:48,1 +DA:50,1 +DA:52,1 +DA:54,3 +DA:55,1 +DA:56,1 +DA:58,4 +DA:64,1 +DA:65,2 +DA:66,1 +DA:67,1 +DA:70,1 +DA:71,2 +DA:72,0 +DA:73,2 +DA:74,2 +DA:75,2 +DA:80,0 +DA:85,0 +DA:90,0 +DA:91,0 +DA:95,0 +DA:96,0 +DA:97,0 +LF:37 +LH:26 +end_of_record +SF:lib/features/home/widgets/home_body.dart +DA:5,0 +DA:7,1 +DA:9,1 +DA:10,3 +LF:4 +LH:3 +end_of_record +SF:lib/features/splash/splash_screen.dart +DA:12,2 +DA:14,2 +DA:15,2 +DA:21,2 +DA:23,2 +DA:24,10 +DA:31,2 +DA:38,2 +DA:40,2 +DA:41,4 +DA:46,2 +DA:48,2 +DA:50,2 +DA:53,2 +DA:55,6 +DA:57,2 +DA:58,4 +DA:59,2 +DA:60,2 +DA:70,1 +DA:72,1 +DA:74,2 +DA:76,2 +DA:78,0 +DA:79,0 +DA:84,2 +DA:86,2 +DA:87,6 +DA:89,2 +DA:90,4 +DA:91,2 +LF:31 +LH:29 +end_of_record +SF:lib/utils/extensions/navigation_extension.dart +DA:5,1 +DA:10,2 +DA:11,1 +DA:14,0 +DA:16,2 +DA:20,1 +DA:21,1 +DA:22,1 +DA:23,1 +DA:29,3 +DA:31,2 +LF:11 +LH:10 +end_of_record +SF:lib/main.dart +DA:5,2 +DA:8,2 +DA:11,1 +DA:13,1 +DA:14,1 +DA:17,1 +LF:6 +LH:6 +end_of_record +SF:lib/playground/test_main.dart +DA:7,1 +DA:9,2 +DA:14,1 +DA:16,1 +DA:17,1 +DA:18,1 +DA:19,1 +DA:20,1 +DA:22,1 +DA:23,2 +DA:27,1 +DA:29,1 +DA:30,2 +DA:42,1 +DA:44,1 +DA:45,1 +DA:46,1 +DA:47,1 +DA:48,1 +DA:50,1 +DA:51,2 +DA:57,1 +DA:59,1 +DA:60,2 +LF:24 +LH:24 +end_of_record diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index ca61192..b8994f1 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -263,13 +263,21 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", "${PODS_ROOT}/../Flutter/Flutter.framework", + "${BUILT_PRODUCTS_DIR}/assets_audio_player/assets_audio_player.framework", + "${BUILT_PRODUCTS_DIR}/assets_audio_player_web/assets_audio_player_web.framework", "${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework", + "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/assets_audio_player.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/assets_audio_player_web.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/lib/common/resources/r.dart b/lib/common/resources/r.dart new file mode 100644 index 0000000..c488906 --- /dev/null +++ b/lib/common/resources/r.dart @@ -0,0 +1,40 @@ +class R { + static Strings string = Strings(); + static SVGImages svg = SVGImages(); + static LottieFiles lottie = LottieFiles(); + static AudioFiles audio = AudioFiles(); + static Placeholders placeholders = Placeholders(); + static BuildMode buildMode = BuildMode(); +} + +class Placeholders { + String johDoeImage = + "https://pbs.twimg.com/profile_images/1057989852942270464/bt45DHmR.jpg"; +} + +class Strings { + String appName = "Productive"; + String loginWithGoogle = "Continue With Google"; + String todo = "Todo"; + String addTodo = "+ Add Todo"; +} + +class SVGImages { + String homeIcon = "assets/images/home.svg"; + String statsIcon = "assets/images/stats.svg"; + String googleIcon = "assets/images/google.svg"; + String successIcon = "assets/images/success.svg"; +} + +class LottieFiles { + String paperPlane = "assets/lottie/paper_plane.json"; +} + +class AudioFiles { + String splashBreeze = "assets/sounds/wind.mp3"; + String welcomeTone = "assets/sounds/point.mp3"; +} + +class BuildMode{ + bool isTesting = false; +} diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart deleted file mode 100644 index d0d708b..0000000 --- a/lib/features/home/home_screen.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -class HomeScreen extends StatelessWidget { - const HomeScreen(); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Text(Name(abc: "Laxman").abc), - ), - ); - } -} - -class Name { - Name({this.abc = "Hello World"}); - - String abc; -} diff --git a/lib/features/home/widgets/animated_circle_progress.dart b/lib/features/home/widgets/animated_circle_progress.dart new file mode 100644 index 0000000..cf68d12 --- /dev/null +++ b/lib/features/home/widgets/animated_circle_progress.dart @@ -0,0 +1,107 @@ +import 'dart:math'; + +import 'package:assets_audio_player/assets_audio_player.dart'; +import 'package:flutter/material.dart'; +import 'package:productive/common/resources/r.dart'; +import 'package:productive/utils/extensions/gradient_extensions.dart'; + +class AnimatedMotivationMeter extends StatefulWidget { + const AnimatedMotivationMeter({ + this.motivation, + this.height, + this.width, + Key key, + }) : assert(motivation <= 1.0 && motivation >= 0), + super(key: key); + + final double motivation; + final double height; + final double width; + + @override + _AnimatedMotivationMeterState createState() => + _AnimatedMotivationMeterState(); +} + +class _AnimatedMotivationMeterState extends State + with SingleTickerProviderStateMixin { + Animation animation; + AnimationController controller; + final _audioPlayer = AssetsAudioPlayer(); + + @override + void initState() { + super.initState(); + _audioPlayer.open(Audio(R.audio.welcomeTone), volume: 0.5); + controller = AnimationController( + vsync: this, duration: const Duration(milliseconds: 2000)); + animation = Tween( + begin: widget.motivation * 0.5, + end: widget.motivation, + ).chain(CurveTween(curve: Curves.decelerate)).animate(controller) + ..addListener(() { + setState(() {}); + }); + controller.forward(); + } + + @override + Widget build(BuildContext context) { + return CustomPaint( + willChange: true, + painter: CircleProgressPainter(widget.motivation, animation.value), + size: Size(widget.width, widget.height), + ); + } + + @override + void dispose() { + _audioPlayer.dispose(); + controller.dispose(); + super.dispose(); + } +} + +class CircleProgressPainter extends CustomPainter { + CircleProgressPainter(this.maxMotivation, this.animatingMotivation) { + endAngle = pi * 2 * min(animatingMotivation, 0.95); + } + + final double startAngle = -pi / 2; + final double gradientRotationAngle = -pi / 2 - 0.14; + double endAngle; + final double maxMotivation; + final double animatingMotivation; + Paint _ink; + + @override + void paint(Canvas canvas, Size size) { + _ink ??= Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 6 + ..strokeCap = StrokeCap.round + ..shader = SweepGradient( + colors: blueGradientColors, + startAngle: startAngle, + endAngle: endAngle, + transform: GradientRotation(gradientRotationAngle)) + .createShader( + Rect.fromCenter( + center: Offset(size.height / 2, size.width / 2), + width: size.width, + height: size.height), + ); + canvas.drawArc( + Rect.fromCenter( + center: Offset(size.height / 2, size.width / 2), + width: size.width, + height: size.height), + startAngle, + endAngle, + false, + _ink); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => true; +} diff --git a/lib/features/home/widgets/animated_nav_button.dart b/lib/features/home/widgets/animated_nav_button.dart new file mode 100644 index 0000000..9d6e871 --- /dev/null +++ b/lib/features/home/widgets/animated_nav_button.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:productive/utils/animation/animated_scale.dart'; +import 'package:productive/utils/extensions/gradient_extensions.dart'; + +class AnimatedNavIcon extends StatelessWidget { + const AnimatedNavIcon({ + Key key, + @required this.navState, + @required this.select, + @required this.touch, + @required this.unTouch, + @required this.child, + }) : super(key: key); + final NavButtonsState navState; + final Function() select; + final Function() touch; + final Function() unTouch; + final Widget child; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: select, + onLongPress: touch, + onLongPressUp: unTouch, + onTapUp: (_) => unTouch, + onLongPressStart: (_) => touch, + onLongPressEnd: (_) => unTouch, + child: child, + ); + } +} + +class NavIcon extends StatelessWidget { + const NavIcon( + {Key key, + @required this.navState, + @required this.svgPath, + @required this.position}) + : super(key: key); + + final NavButtonsState navState; + final String svgPath; + final int position; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(32.0), + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: _getScale(position, navState), + child: SvgPicture.asset( + svgPath, + height: 32, + ).getShadedWidget(_getGradient(position, navState)), + ), + ); + } +} + +LinearGradient _getGradient(int buttonIndex, NavButtonsState navState) { + return buttonIndex == navState.selectedIndex + ? blueLinearGradient + : greyLinearGradient; +} + +double _getScale(int buttonIndex, NavButtonsState navState) { + if (buttonIndex == navState.pressedIndex && + buttonIndex == navState.selectedIndex) return 0.8; + if (buttonIndex == navState.pressedIndex) return 0.6; + if (buttonIndex != navState.pressedIndex && + buttonIndex != navState.selectedIndex) return 0.8; + return 1.0; +} + +class NavButtonsState { + const NavButtonsState({this.selectedIndex, this.pressedIndex}); + + final int selectedIndex; + final int pressedIndex; + + NavButtonsState copyWith({ + int selectedIndex, + int pressedIndex, + }) { + if ((selectedIndex == null || + identical(selectedIndex, this.selectedIndex)) && + (pressedIndex == null || identical(pressedIndex, this.pressedIndex))) { + return this; + } + + return NavButtonsState( + selectedIndex: selectedIndex ?? this.selectedIndex, + pressedIndex: pressedIndex ?? this.pressedIndex, + ); + } +} diff --git a/lib/features/home/widgets/home_app_bar.dart b/lib/features/home/widgets/home_app_bar.dart new file mode 100644 index 0000000..0a79d4c --- /dev/null +++ b/lib/features/home/widgets/home_app_bar.dart @@ -0,0 +1,53 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:productive/common/resources/r.dart'; + +import 'animated_circle_progress.dart'; + +class AppbarContent extends StatelessWidget { + const AppbarContent({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Stack( + alignment: Alignment.center, + children: const [ + AnimatedMotivationMeter(motivation: 0.6, height: 42, width: 42), + DisplayPic(height: 36, width: 36), + ], + ), + Text( + R.string.todo, + style: TextStyle(fontSize: 28, fontWeight: FontWeight.w600), + ), + SvgPicture.asset(R.svg.successIcon, height: 28, width: 28) + ], + ); + } +} + +class DisplayPic extends StatelessWidget { + const DisplayPic({this.height, this.width, Key key}) : super(key: key); + + final double height; + final double width; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(200), + child: R.buildMode.isTesting + ? Container() + : CachedNetworkImage( + imageUrl: R.placeholders.johDoeImage, + placeholder: (context, url) => const CircularProgressIndicator(), + height: height, + width: width, + ), + ); + } +} diff --git a/lib/features/home/widgets/home_body.dart b/lib/features/home/widgets/home_body.dart new file mode 100644 index 0000000..42a6eb3 --- /dev/null +++ b/lib/features/home/widgets/home_body.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:productive/common/resources/r.dart'; + +class HomeBody extends StatelessWidget { + const HomeBody({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Text(R.string.appName), + ); + } +} diff --git a/lib/features/home/widgets/home_bottom_nav_bar.dart b/lib/features/home/widgets/home_bottom_nav_bar.dart new file mode 100644 index 0000000..3d92289 --- /dev/null +++ b/lib/features/home/widgets/home_bottom_nav_bar.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:productive/common/resources/r.dart'; + +import 'animated_nav_button.dart'; + +class BottomNav extends StatefulWidget { + const BottomNav({Key key}) : super(key: key); + + @override + _BottomNavState createState() => _BottomNavState(); +} + +class _BottomNavState extends State { + NavButtonsState navState = + const NavButtonsState(selectedIndex: 0, pressedIndex: -1); + + void _selectPage(int index) { + setState(() { + navState = navState.copyWith(selectedIndex: index); + }); + } + + void _touchIcon(int index) { + setState(() { + navState = navState.copyWith(pressedIndex: index); + }); + } + + void _touchHomeIcon() => _touchIcon(0); + + void _touchStatsIcon() => _touchIcon(1); + + void _unTouch() => _touchIcon(-1); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration(boxShadow: [ + BoxShadow( + color: Color(0x33CCCCCC), + offset: Offset(-4, -4), + blurRadius: 8, + spreadRadius: 8) + ]), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + child: BottomAppBar( + elevation: 20, + shape: const AutomaticNotchedShape( + RoundedRectangleBorder(), + StadiumBorder( + side: BorderSide(), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AnimatedNavIcon( + key: const Key("first_icon"), + navState: navState, + select: () => _selectPage(0), + unTouch: _unTouch, + touch: _touchHomeIcon, + child: NavIcon( + position: 0, + svgPath: R.svg.homeIcon, + navState: navState, + ), + ), + AnimatedNavIcon( + key: const Key("second_icon"), + navState: navState, + select: () => _selectPage(1), + unTouch: _unTouch, + touch: _touchStatsIcon, + child: NavIcon( + position: 1, + svgPath: R.svg.statsIcon, + navState: navState, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/home/widgets/home_fab.dart b/lib/features/home/widgets/home_fab.dart new file mode 100644 index 0000000..3f917fa --- /dev/null +++ b/lib/features/home/widgets/home_fab.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:productive/common/resources/r.dart'; +import 'package:productive/features/splash/splash_screen.dart'; +import 'package:productive/utils/extensions/gradient_extensions.dart'; +import 'package:productive/utils/extensions/navigation_extension.dart'; + +class Fab extends StatelessWidget { + const Fab({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return RaisedButton( + key: const Key("home_fab"), + color: Colors.transparent, + splashColor: Colors.transparent, + onPressed: () { + context.navigateTo(const SplashScreen()); + }, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(80.0)), + padding: const EdgeInsets.all(0.0), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), + child: Text( + R.string.addTodo, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, color: Colors.white, fontWeight: FontWeight.w600), + ), + ).withBlueGradientBg(), + ); + } +} diff --git a/lib/features/home/widgets/home_screen.dart b/lib/features/home/widgets/home_screen.dart new file mode 100644 index 0000000..fa94f94 --- /dev/null +++ b/lib/features/home/widgets/home_screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:productive/features/home/widgets/home_body.dart'; +import 'package:productive/utils/animation/scale_on_press_widget.dart'; + +import 'home_app_bar.dart'; +import 'home_bottom_nav_bar.dart'; +import 'home_fab.dart'; + +class HomeScreen extends StatelessWidget { + + @override + Widget build(BuildContext context) { + return Scaffold( + body: const HomeBody(), + appBar: AppBar( + title: const AppbarContent(), automaticallyImplyLeading: false), + floatingActionButton: const Padding( + padding: EdgeInsets.symmetric(horizontal: 2, vertical: 2), + child: ScaleOnPressWidget( + child: Fab(), + ), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + bottomNavigationBar: const BottomNav(), + ); + } +} diff --git a/lib/features/splash/spalash_screen.dart b/lib/features/splash/spalash_screen.dart deleted file mode 100644 index 09b64d8..0000000 --- a/lib/features/splash/spalash_screen.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:lottie/lottie.dart'; -import 'package:dartx/dartx.dart'; -import 'package:productive/features/home/home_screen.dart'; -import 'package:productive/utils/extensions/navigation_extension.dart'; - -class SplashScreen extends StatefulWidget { - const SplashScreen(); - - @override - _SplashScreenState createState() => _SplashScreenState(); -} - -class _SplashScreenState extends State { - @override - void initState() { - super.initState(); - context.navigateTo(const HomeScreen(), delay: 2.seconds); - } - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: SplashScreenBody(), - ); - } -} - -class SplashScreenBody extends StatelessWidget { - const SplashScreenBody(); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Spacer( - flex: 3, - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Lottie.asset( - 'assets/lottie/paper_plane.json', - height: 300, - width: 300, - ), - ], - ), - const Spacer( - flex: 3, - ), - ShaderMask( - blendMode: BlendMode.srcIn, - shaderCallback: (Rect bound) { - return const LinearGradient( - colors: [ - Color(0xFF01A4FF), - Color(0xFF0186FF), - ], - begin: Alignment(-1.0, -8.0), - end: Alignment(1.0, 4.0), - ).createShader(bound); - }, - child: Text( - "Productive", - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 64, - color: Colors.blue, - ), - ), - ), - const Spacer( - flex: 5, - ), - InkWell( - onTap: () {}, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 48.0), - child: Container( - padding: const EdgeInsets.only( - top: 12, bottom: 8, left: 12, right: 12), - decoration: BoxDecoration( - boxShadow: const [ - BoxShadow( - color: Color(0x33CCCCCC), - blurRadius: 4, - offset: Offset(4, 4), - spreadRadius: 4) - ], - color: Colors.white, - borderRadius: BorderRadius.circular(200), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset("assets/images/google.svg"), - const SizedBox( - width: 16, - ), - Text( - "Continue with google", - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), - ), - ), - const Spacer( - flex: 2, - ) - ], - ); - } -} diff --git a/lib/features/splash/splash_screen.dart b/lib/features/splash/splash_screen.dart new file mode 100644 index 0000000..411f580 --- /dev/null +++ b/lib/features/splash/splash_screen.dart @@ -0,0 +1,92 @@ +import 'package:assets_audio_player/assets_audio_player.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:lottie/lottie.dart'; +import 'package:productive/common/resources/r.dart'; +import 'package:productive/features/home/widgets/home_screen.dart'; +import 'package:productive/utils/animation/scale_on_press_widget.dart'; +import 'package:productive/utils/extensions/gradient_extensions.dart'; +import 'package:productive/utils/extensions/navigation_extension.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({Key key}) : super(key: key); + + @override + _SplashScreenState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + final _audioPlayer = AssetsAudioPlayer(); + + @override + void initState() { + super.initState(); + _audioPlayer.open(Audio(R.audio.splashBreeze), + loopMode: LoopMode.single, + playInBackground: PlayInBackground.disabledRestoreOnForeground, + seek: const Duration(seconds: 2), + volume: 0.5); + } + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: SplashScreenBody(), + ); + } + + @override + void dispose() { + super.dispose(); + _audioPlayer.dispose(); + } +} + +class SplashScreenBody extends StatelessWidget { + const SplashScreenBody({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(flex: 3), + Lottie.asset(R.lottie.paperPlane, height: 300, width: 300), + const Spacer(flex: 3), + Text( + R.string.appName, + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 64), + ).withBlueGradientFg(), + const Spacer(flex: 5), + GoogleSignInButton(), + const Spacer(flex: 2) + ], + ); + } +} + +class GoogleSignInButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return ScaleOnPressWidget( + key: const Key("login"), + onTap: () { + context.navigateTo(HomeScreen(), replace: true); + }, + outerPadding: const EdgeInsets.symmetric(horizontal: 48.0), + innerPadding: + const EdgeInsets.only(top: 12, bottom: 8, left: 12, right: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(R.svg.googleIcon), + const SizedBox(width: 16), + Text( + R.string.loginWithGoogle, + style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700), + ), + ], + )); + } +} diff --git a/lib/main.dart b/lib/main.dart index 598676a..5d6c1c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,18 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:productive/features/splash/spalash_screen.dart'; -void main() { - runApp(ProductiveApp()); -} +import 'package:productive/features/splash/splash_screen.dart'; +import 'package:productive/utils/design/colors.dart'; + +void main() => runApp(const ProductiveApp()); class ProductiveApp extends StatelessWidget { + const ProductiveApp({Key key, this.testChild}) : super(key: key); + final Widget testChild; + @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( - fontFamily: 'Product Sans', - ), + fontFamily: 'Product Sans', primarySwatch: whiteMaterialColor), debugShowCheckedModeBanner: false, - home: const SplashScreen(), + home: testChild ?? const SplashScreen(), ); } } diff --git a/lib/playground/test_main.dart b/lib/playground/test_main.dart new file mode 100644 index 0000000..a28d0cd --- /dev/null +++ b/lib/playground/test_main.dart @@ -0,0 +1,69 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../utils/extensions/navigation_extension.dart'; + +class DummyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp(home: Screen1(),); + } +} + +class Screen1 extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + children: [ + MaterialButton( + key: const Key("go_screen_2"), + onPressed: () { + context.navigateTo(Screen2()); + }, + child: const Text("Go to screen2"), + ), + GestureDetector( + key: const Key("gd1"), + onTap: () { + context.navigateTo(Screen2()); + }, + child: const Text("Jump to screen2"), + ), + ], + ), + ), + ); + } +} + +class Screen2 extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + children: [ + MaterialButton( + key: const Key("go_screen_1"), + onPressed: () { + context.navigateTo(Screen1()); +// Navigator.of(context) +// .push(MaterialPageRoute(builder: (context) => Screen1())); + }, + child: const Text("Go to screen1"), + ), + GestureDetector( + key: const Key("gd2"), + onTap: () { + context.navigateTo(Screen1()); + }, + child: const Text("Jump to screen1"), + ) + ], + ), + ), + ); + } +} diff --git a/lib/utils/animation/animated_scale.dart b/lib/utils/animation/animated_scale.dart new file mode 100644 index 0000000..d30bc3e --- /dev/null +++ b/lib/utils/animation/animated_scale.dart @@ -0,0 +1,48 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class AnimatedScale extends ImplicitlyAnimatedWidget { + const AnimatedScale({ + Key key, + this.child, + @required this.scale, + Curve curve = Curves.linear, + @required Duration duration, + VoidCallback onEnd, + this.alignment = Alignment.center, + }) : assert(scale != null && scale >= 0.0 && scale <= 1.0), + super(key: key, curve: curve, duration: duration, onEnd: onEnd); + + final Widget child; + + final double scale; + + final Alignment alignment; + + @override + _AnimatedScaleState createState() => _AnimatedScaleState(); +} + +class _AnimatedScaleState extends ImplicitlyAnimatedWidgetState { + Tween _scale; + Animation _scaleAnimation; + + @override + void forEachTween(TweenVisitor visitor) { + _scale = visitor(_scale, widget.scale, + (dynamic value) => Tween(begin: value as double)) + as Tween; + } + + @override + void didUpdateTweens() { + _scaleAnimation = animation.drive(_scale); + } + + @override + Widget build(BuildContext context) => ScaleTransition( + scale: _scaleAnimation, + alignment: widget.alignment, + child: widget.child, + ); +} \ No newline at end of file diff --git a/lib/utils/animation/scale_on_press_widget.dart b/lib/utils/animation/scale_on_press_widget.dart new file mode 100644 index 0000000..e2ace3f --- /dev/null +++ b/lib/utils/animation/scale_on_press_widget.dart @@ -0,0 +1,70 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; + +import 'animated_scale.dart'; + +class ScaleOnPressWidget extends StatefulWidget { + const ScaleOnPressWidget( + {this.child, + this.onTap, + this.outerPadding = EdgeInsets.zero, + this.innerPadding = EdgeInsets.zero, + this.scaleFactor = 0.95, + Key key}) + : super(key: key); + + final Widget child; + final GestureTapCallback onTap; + final EdgeInsets outerPadding; + final EdgeInsets innerPadding; + final double scaleFactor; + + @override + _ScaleOnPressWidgetState createState() => _ScaleOnPressWidgetState(); +} + +class _ScaleOnPressWidgetState extends State { + bool isPressed = false; + + void _updatePressed(bool pressed) { + setState(() { + isPressed = pressed; + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPress: () => _updatePressed(true), + onTapDown: (_) => _updatePressed(true), + onTapUp: (_) => _updatePressed(false), + onTap: widget.onTap, + child: Padding( + padding: widget.outerPadding, + child: AnimatedScale( + duration: (isPressed ? 50 : 200).milliseconds, + scale: isPressed ? widget.scaleFactor : 1.0, + curve: Curves.decelerate, + child: AnimatedContainer( + duration: (isPressed ? 50 : 200).milliseconds, + curve: Curves.decelerate, + padding: widget.innerPadding, + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: const Color(0x33CCCCCC), + blurRadius: isPressed ? 0 : 4, + offset: isPressed ? const Offset(0, 0) : const Offset(4, 4), + spreadRadius: isPressed ? 0 : 4) + ], + color: Colors.white, + borderRadius: BorderRadius.circular(200), + ), + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/utils/design/colors.dart b/lib/utils/design/colors.dart new file mode 100644 index 0000000..7e3aae0 --- /dev/null +++ b/lib/utils/design/colors.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +const MaterialColor whiteMaterialColor = MaterialColor( + 0xFFFFFFFF, + { + 50: Color(0xFFFFFFFF), + 100: Color(0xFFFFFFFF), + 200: Color(0xFFFFFFFF), + 300: Color(0xFFFFFFFF), + 400: Color(0xFFFFFFFF), + 500: Color(0xFFFFFFFF), + 600: Color(0xFFFFFFFF), + 700: Color(0xFFFFFFFF), + 800: Color(0xFFFFFFFF), + 900: Color(0xFFFFFFFF), + }, +); diff --git a/lib/utils/extensions/gradient_extensions.dart b/lib/utils/extensions/gradient_extensions.dart new file mode 100644 index 0000000..0df611f --- /dev/null +++ b/lib/utils/extensions/gradient_extensions.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +extension WidgetUnderGradient on Widget { + Widget withBlueGradientFg() { + return getShadedWidget(blueLinearGradient); + } + + Widget withGreyGradientFg() { + return getShadedWidget(greyLinearGradient); + } + + Widget getShadedWidget(LinearGradient gradient) { + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (Rect bound) { + return gradient.createShader(bound); + }, + child: this, + ); + } + + Widget withBlueGradientBg( + {BorderRadiusGeometry borderRadius = + const BorderRadius.all(Radius.circular(200.0))}) { + return Ink( + decoration: BoxDecoration( + gradient: blueLinearGradient, + borderRadius: borderRadius, + ), + child: this, + ); + } +} + +LinearGradient blueLinearGradient = getLinearGradient(blueGradientColors); +LinearGradient greyLinearGradient = getLinearGradient(_greyGradientColors); + +LinearGradient getLinearGradient(List colors) { + return LinearGradient( + colors: colors, + begin: const Alignment(-1.0, -1.0), + end: const Alignment(0.7, 0.7), + transform: const GradientRotation(-3.14 / 3), + ); +} + +const List blueGradientColors = [Color(0xFF0186FF), Color(0xFF00C2FF)]; +const List _greyGradientColors = [Color(0xFFD0D1D1), Color(0xFF8E9496)]; diff --git a/lib/utils/extensions/navigation_extension.dart b/lib/utils/extensions/navigation_extension.dart index 14a7ec6..0537fad 100644 --- a/lib/utils/extensions/navigation_extension.dart +++ b/lib/utils/extensions/navigation_extension.dart @@ -1,13 +1,35 @@ -import 'package:flutter/material.dart'; import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; extension NavigationExtension on BuildContext { Future navigateTo( Widget destination, { Duration delay = const Duration(), + bool replace = false, }) async { await delay.delay; - Navigator.of(this) - .push(MaterialPageRoute(builder: (context) => destination)); + final route = _createRoute(destination); + + if (replace) { + Navigator.of(this).pushReplacement(route); + } else { + Navigator.of(this).push(route); + } + } + + PageRouteBuilder _createRoute(Widget destination) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => destination, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.decelerate; + + final tween = + Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + + return SlideTransition(position: animation.drive(tween), child: child); + }, + ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 6ae2cd3..1e99684 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,8 @@ dependencies: google_fonts: ^1.1.0 flutter_svg: ^0.17.4 lottie: ^0.4.0+1 + preview: ^0.0.8 + cached_network_image: ^2.2.0+1 #Architecture rxdart: ^0.24.1 @@ -22,6 +24,7 @@ dependencies: #Utils dartx: ^0.4.2 + assets_audio_player: ^2.0.6+4 dev_dependencies: flutter_test: @@ -33,6 +36,7 @@ flutter: assets: - assets/lottie/ - assets/images/ + - assets/sounds/ fonts: - family: Product Sans @@ -43,10 +47,3 @@ flutter: style: italic - asset: assets/fonts/Product_Sans_Bold_Italic.ttf style: italic - - - -# fonts Balsamiq+Sans, Poppins, Tenali, Rubik, Patrick Hand, Lobster two, Handlee, El Messiri, Alice, -# Amaranth, Comic neue, Jost, Capriola, Combo, Reem Kufi, Cabin Sketch,Caladea,Cinzel, Cousard,Sen, -# Delius, Metrophobic,Pompiere, Gabriela,Kurale,Vioada Libre,Amarante,Soloway, - diff --git a/test/features/home/home_screen_components_test.dart b/test/features/home/home_screen_components_test.dart new file mode 100644 index 0000000..6a370e4 --- /dev/null +++ b/test/features/home/home_screen_components_test.dart @@ -0,0 +1,87 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:productive/common/resources/r.dart'; +import 'package:productive/features/home/widgets/animated_circle_progress.dart'; +import 'package:productive/features/home/widgets/home_app_bar.dart'; +import 'package:productive/features/home/widgets/home_body.dart'; +import 'package:productive/features/home/widgets/home_bottom_nav_bar.dart'; +import 'package:productive/features/home/widgets/home_screen.dart'; + +import '../../test_utils/test_utils.dart'; + +void main() { + group("Test Animated Motivation Meter", () { + setUpAll(() { + R.buildMode.isTesting = true; + }); + + tearDownAll(() { + R.buildMode.isTesting = false; + }); + + testWidgets("test animated progressbar", (WidgetTester tester) async { + await tester.pumpWidget( + // ignore: prefer_const_constructors + AnimatedMotivationMeter(height: 200, width: 200, motivation: 0.5)); + expect(find.byType(CustomPaint), findsNWidgets(1)); + }); + }); + + group("Test Animated Nav Bar", () { + setUpAll(() { + R.buildMode.isTesting = true; + }); + + tearDownAll(() { + R.buildMode.isTesting = false; + }); + + testWidgets("gestures are working", (WidgetTester tester) async { + // ignore: prefer_const_constructors + await tester.pumpWidget(BottomNav().insideMaterialApp); + expect(find.byType(GestureDetector), findsNWidgets(2)); + + await tester.press(find.byKey(const Key("first_icon"))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("first_icon"))); + await tester.pumpAndSettle(); + await tester.longPress(find.byKey(const Key("second_icon"))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("second_icon"))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key("first_icon"))); + await tester.pumpAndSettle(); + }); + }); + + group("Test home Appbar", () { + testWidgets("HomeAppbar contains DisplayPic Widget", + (WidgetTester tester) async { + // ignore: prefer_const_constructors + await tester.pumpWidget(AppbarContent().asScaffold); + expect(find.byType(DisplayPic), findsOneWidget); + + // ignore: prefer_const_constructors + await tester.pumpWidget(DisplayPic().asScaffold); + expect(find.byType(ClipRRect), findsOneWidget); + expect(find.byType(CachedNetworkImage), findsOneWidget); + }); + }); + + group("Test Home Body", () { + testWidgets("Home Body contains App Name", (WidgetTester tester) async { + // ignore: prefer_const_constructors + await tester.pumpWidget(HomeBody().asScaffold); + expect(find.text(R.string.appName), findsOneWidget); + }); + }); + + group("Test Home Fab", () { + testWidgets("Home Body contains App Name", (WidgetTester tester) async { + // ignore: prefer_const_constructors + await tester.pumpWidget(HomeScreen().asScaffold); + await tester.press(find.byKey(const Key("home_fab"))); + }); + }); +} diff --git a/test/features/home/home_screen_test.dart b/test/features/home/home_screen_test.dart new file mode 100644 index 0000000..7619986 --- /dev/null +++ b/test/features/home/home_screen_test.dart @@ -0,0 +1,29 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:productive/common/resources/r.dart'; +import 'package:productive/features/home/widgets/home_app_bar.dart'; +import 'package:productive/features/home/widgets/home_bottom_nav_bar.dart'; +import 'package:productive/features/home/widgets/home_fab.dart'; +import 'package:productive/features/home/widgets/home_screen.dart'; + +import '../../test_utils/test_utils.dart'; + +void main() { + group("All required widgets are on the Home screen", () { + testWidgets( + "Fab,BottomNav and AppbarContent exist", + (WidgetTester tester) async { + await tester.pumpWidget(HomeScreen().asScaffold); + expect(find.byType(Fab), findsOneWidget); + expect(find.byType(RaisedButton), findsOneWidget); + expect(find.byType(BottomNav), findsOneWidget); + expect(find.byType(AppbarContent), findsOneWidget); + expect(find.byType(CachedNetworkImage), findsOneWidget); + + await tester.pumpWidget(const Fab().testWidget); + expect(find.text(R.string.addTodo), findsOneWidget); + }, + ); + }); +} diff --git a/test/features/splash/splash_screen_test.dart b/test/features/splash/splash_screen_test.dart new file mode 100644 index 0000000..63e2646 --- /dev/null +++ b/test/features/splash/splash_screen_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lottie/lottie.dart'; +import 'package:productive/common/resources/r.dart'; +import 'package:productive/features/home/widgets/home_body.dart'; +import 'package:productive/features/home/widgets/home_fab.dart'; +import 'package:productive/features/splash/splash_screen.dart'; +import 'package:productive/utils/animation/animated_scale.dart'; + +import '../../test_utils/test_utils.dart'; + +void main() { + group("All required widgets are on the Splash screen", () { + setUpAll(() { + R.buildMode.isTesting = true; + }); + + tearDownAll(() { + R.buildMode.isTesting = false; + }); + testWidgets( + "There is Lottie Widget", + (WidgetTester tester) async { + // ignore: prefer_const_constructors + await tester.pumpWidget(SplashScreen().asScaffold); + expect(find.byType(LottieBuilder), findsOneWidget); + }, + ); + + testWidgets( + "There is a Productive Text Logo", + (WidgetTester tester) async { + await tester.pumpWidget(const SplashScreen().asScaffold); + expect(find.byType(ShaderMask), findsOneWidget); + expect(find.text(R.string.appName), findsOneWidget); + }, + ); + + testWidgets( + "There is a Google Sign in Button", + (WidgetTester tester) async { + // ignore: prefer_const_constructors + await tester.pumpWidget(SplashScreenBody().asScaffold); + expect(find.byType(AnimatedScale), findsOneWidget); + expect(find.text(R.string.loginWithGoogle), findsOneWidget); + }, + ); + + testWidgets("Can navigate to HomeScreen", (WidgetTester tester) async { + // ignore: prefer_const_constructors + await tester.pumpWidget( SplashScreenBody().insideMaterialApp); + final loginButton = find.byKey(const Key("login")); + expect(loginButton, findsOneWidget); + await tester.tap(loginButton); + await tester.pumpAndSettle(); + expect(loginButton, findsNothing); + expect(find.byType(HomeBody), findsOneWidget); + expect(find.byType(Fab), findsOneWidget); + }); + }); +} diff --git a/test/main_test.dart b/test/main_test.dart new file mode 100644 index 0000000..159c68d --- /dev/null +++ b/test/main_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:productive/main.dart' as app; +import 'package:productive/main.dart'; + +void main() { + group("Basic App Setup test", () { + testWidgets('App is MaterialApp', (WidgetTester tester) async { + await tester.pumpWidget(ProductiveApp( + testChild: Container(), + )); + expect(find.byType(MaterialApp), findsOneWidget); + }); + + testWidgets("main method runs successfully", (WidgetTester tester) async { + app.main(); + }); + }); +} diff --git a/test/playgorund/testmain_test.dart b/test/playgorund/testmain_test.dart new file mode 100644 index 0000000..efb60fd --- /dev/null +++ b/test/playgorund/testmain_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:productive/playground/test_main.dart'; + +void main() { + group("Dummy widget test", () { + testWidgets( + "Screen contains a button to go to page2", + (WidgetTester tester) async { + await tester.pumpWidget(DummyApp()); + final buttonFinder = find.byType(MaterialButton); + expect(buttonFinder, findsOneWidget); + expect(find.text("Go to screen2"), findsOneWidget); + await tester.tap(buttonFinder); + await tester.pumpAndSettle(); + expect(buttonFinder, findsOneWidget); + expect(find.text("Go to screen1"), findsOneWidget); + expect(find.text("Go to screen2"), findsNothing); + await tester.tap(buttonFinder); + await tester.pumpAndSettle(); + expect(find.text("Go to screen2"), findsOneWidget); + }, + ); + + testWidgets( + "Gesture Detector also works like button", + (WidgetTester tester) async { + await tester.pumpWidget(DummyApp()); + expect(find.byKey(const Key("gd1")), findsOneWidget); + expect(find.text("Jump to screen2"), findsOneWidget); + await tester.tap(find.byKey(const Key("gd1"))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key("gd2")), findsOneWidget); + expect(find.text("Jump to screen1"), findsOneWidget); + expect(find.text("Jump to screen2"), findsNothing); + await tester.tap(find.byKey(const Key("gd2"))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key("gd1")), findsOneWidget); + expect(find.text("Jump to screen2"), findsOneWidget); + }, + ); + }); +} diff --git a/test/test_utils/test_utils.dart b/test/test_utils/test_utils.dart new file mode 100644 index 0000000..ef7394a --- /dev/null +++ b/test/test_utils/test_utils.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +extension TestWidget on Widget { + Widget get testWidget => + Directionality(textDirection: TextDirection.ltr, child: this); + + Widget get asScaffold => MaterialApp(home: this); + + Widget get insideMaterialApp =>MaterialApp(home: Scaffold(body: this,)); +} diff --git a/test/unit_test.dart b/test/unit_test.dart deleted file mode 100644 index 13e9bf3..0000000 --- a/test/unit_test.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group("blablabla", () { - test("unit tests", () { - expect(5, 4 + 1); - }); - }); -} diff --git a/test/utils/animation/scale_on_press_widget_test.dart b/test/utils/animation/scale_on_press_widget_test.dart new file mode 100644 index 0000000..e133007 --- /dev/null +++ b/test/utils/animation/scale_on_press_widget_test.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:productive/utils/animation/scale_on_press_widget.dart'; + +import '../../test_utils/test_utils.dart'; + +void main() { + group("Gradient Extensions Test", () { + testWidgets("Gradient on Foreground Test", (WidgetTester tester) async { + final Widget scaledIcon = + ScaleOnPressWidget(child: Icon(Icons.ac_unit)).testWidget; + await tester.pumpWidget(scaledIcon); + expect(find.byType(GestureDetector), findsOneWidget); + await tester.press(find.byType(GestureDetector)); + await tester.pumpAndSettle(); + await tester.longPress(find.byType(GestureDetector)); + await tester.pumpAndSettle(); + }); + }); +} diff --git a/test/utils/extensions/gradient_extensions_test.dart b/test/utils/extensions/gradient_extensions_test.dart new file mode 100644 index 0000000..cddf01f --- /dev/null +++ b/test/utils/extensions/gradient_extensions_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:productive/utils/extensions/gradient_extensions.dart'; + +import '../../test_utils/test_utils.dart'; + +void main() { + group("Gradient Extensions Test", () { + testWidgets( + "Gradient on Foreground Test", + (WidgetTester tester) async { + final Widget blueFg = const Text("").withBlueGradientFg().testWidget; + await tester.pumpWidget(blueFg); + expect(find.byType(ShaderMask), findsOneWidget); + final Widget greyFg = const Text("").withGreyGradientFg().testWidget; + await tester.pumpWidget(greyFg); + expect(find.byType(ShaderMask), findsOneWidget); + }, + ); + + testWidgets( + "Gradient on Background Test", + (WidgetTester tester) async { + final Widget blueBg = + const Text("").withBlueGradientBg().insideMaterialApp; + await tester.pumpWidget(blueBg); + expect(find.byType(Ink), findsOneWidget); + }, + ); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index c9ce1fd..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:productive/main.dart'; -void main() { - testWidgets('App Follows MaterialTheme', (WidgetTester tester) async { - await tester.pumpWidget(ProductiveApp()); - expect(find.byType(MaterialApp), findsOneWidget); - }); -} \ No newline at end of file