From da524533718d1a87dd2c1e38244f880f032491c2 Mon Sep 17 00:00:00 2001 From: Nils Weber Date: Thu, 13 Nov 2025 19:33:58 +0100 Subject: [PATCH] First draft of Onboarding page --- android/tutorialtest_android.iml | 2 +- lib/core/const/color_constants.dart | 19 +++ lib/core/const/data_constants.dart | 25 ++++ lib/core/const/path_constants.dart | 20 +++ lib/core/const/text_constants.dart | 12 ++ lib/core/extensions/exceptions.dart | 0 lib/core/extensions/list_extension.dart | 0 lib/core/service/auth_service.dart | 0 lib/core/service/date_service.dart | 0 lib/core/service/notification_service.dart | 0 lib/core/service/user_service.dart | 0 lib/core/service/user_storage_service.dart | 0 lib/core/service/validation_service.dart | 0 lib/main.dart | 12 +- .../onboarding/bloc/onboarding_bloc.dart | 37 ++++++ .../onboarding/bloc/onboarding_event.dart | 16 +++ .../onboarding/bloc/onboarding_state.dart | 18 +++ .../onboarding/page/onboarding_page.dart | 38 ++++++ .../onboarding/widget/onboarding_content.dart | 114 ++++++++++++++++++ .../onboarding/widget/onboarding_tile.dart | 46 +++++++ pubspec.yaml | 24 +++- test/widget_test.dart | 2 +- 22 files changed, 371 insertions(+), 14 deletions(-) create mode 100644 lib/core/const/color_constants.dart create mode 100644 lib/core/const/data_constants.dart create mode 100644 lib/core/const/path_constants.dart create mode 100644 lib/core/const/text_constants.dart create mode 100644 lib/core/extensions/exceptions.dart create mode 100644 lib/core/extensions/list_extension.dart create mode 100644 lib/core/service/auth_service.dart create mode 100644 lib/core/service/date_service.dart create mode 100644 lib/core/service/notification_service.dart create mode 100644 lib/core/service/user_service.dart create mode 100644 lib/core/service/user_storage_service.dart create mode 100644 lib/core/service/validation_service.dart create mode 100644 lib/screens/onboarding/bloc/onboarding_bloc.dart create mode 100644 lib/screens/onboarding/bloc/onboarding_event.dart create mode 100644 lib/screens/onboarding/bloc/onboarding_state.dart create mode 100644 lib/screens/onboarding/page/onboarding_page.dart create mode 100644 lib/screens/onboarding/widget/onboarding_content.dart create mode 100644 lib/screens/onboarding/widget/onboarding_tile.dart diff --git a/android/tutorialtest_android.iml b/android/tutorialtest_android.iml index 1899969..3e44773 100644 --- a/android/tutorialtest_android.iml +++ b/android/tutorialtest_android.iml @@ -26,4 +26,4 @@ - + \ No newline at end of file diff --git a/lib/core/const/color_constants.dart b/lib/core/const/color_constants.dart new file mode 100644 index 0000000..1a77da0 --- /dev/null +++ b/lib/core/const/color_constants.dart @@ -0,0 +1,19 @@ +import 'dart:ui'; + +class ColorConstants { + static const textColor = Color(0xFF1F2022); + static const primaryColor = Color(0xFF6358E1); + static const textBlack = Color(0xFF1F2022); + static const white = Color(0xFFFFFFFF); + static const grey = Color(0xFFB6BDC6); + static const loadingBlack = Color(0x80000000); + + static const textFieldBackground = Color(0xFFFBFCFF); + static const textFieldBorder = Color (0xFFB9BBC5); + static const disabledColor = Color(0xFFE1E1E5); + static const errorColor = Color (0xFFF25252); + static const homeBackgroundColor = Color.fromRGBO(252, 252, 252, 1); + static const textGrey = Color(0xFF8F98A3); + static const cardioColor = Color(0xFFFCB74F); + static const armsColor = Color(0xFF5C9BA4); +} \ No newline at end of file diff --git a/lib/core/const/data_constants.dart b/lib/core/const/data_constants.dart new file mode 100644 index 0000000..6f0d42a --- /dev/null +++ b/lib/core/const/data_constants.dart @@ -0,0 +1,25 @@ +import 'package:tutorialtest/core/const/path_constants.dart'; +import 'package:tutorialtest/core/const/text_constants.dart'; + +import '../../screens/onboarding/widget/onboarding_tile.dart'; + +class DataConstants { + // Onboarding + static final onboardingTiles = [ + OnboardingTile( + title: TextConstants.onboarding1Title, + mainText: TextConstants.onboardingDescription, + imagePath: PathConstants.onboarding1 + ), + OnboardingTile( + title: TextConstants.onboardingTitle, + mainText: TextConstants.onboardingDescription, + imagePath: PathConstants.onboarding2 + ), + OnboardingTile( + title: TextConstants.onboarding3Title, + mainText: TextConstants.onboarding3Description, + imagePath: PathConstants.onboarding3 + ) + ]; +} \ No newline at end of file diff --git a/lib/core/const/path_constants.dart b/lib/core/const/path_constants.dart new file mode 100644 index 0000000..6c7ba6a --- /dev/null +++ b/lib/core/const/path_constants.dart @@ -0,0 +1,20 @@ +class PathConstants{ + // Onboarding + static const String onboarding1 = 'assets/images/onboarding/onboarding.png'; + static const String onboarding2 = 'assets/images/onboarding/onboarding_2.png'; + static const String onboarding3 = 'assets/images/onboarding/onboarding_3.png'; + // Auth + static const String eye = 'assets/images/auth/eye_icon.png'; + // Tabbar + static const String home = 'assets/icons/home/home_icon.png'; + static const String workouts = 'assets/icons/home/workouts_icon.png'; + static const String settings = 'assets/icons/home/settings_icon.png'; + // Home + static const String profile = 'assets/images/home/profile.png'; + static const String finished = 'assets/images/home/finished.png'; + static const String inProgress = 'assets/icons/home/inProgress.png'; + static const String timeSent = 'assets/icons/home/time.png'; + static const String cardio = 'assets/images/home/cardio.png'; + static const String arms = 'assets/images/home/arms.png'; + static const String progress = 'assets/icons/home/progress.png'; +} \ No newline at end of file diff --git a/lib/core/const/text_constants.dart b/lib/core/const/text_constants.dart new file mode 100644 index 0000000..1710975 --- /dev/null +++ b/lib/core/const/text_constants.dart @@ -0,0 +1,12 @@ +class TextConstants { + // Onboarding + static const String onboarding1Title = "Workout anywhere"; + static const String onboardingTitle = "Learn techniques"; + static const String onboarding3Title = "Stay strong & healthy"; + static const String onboardingDescription = + "You can do your workout at home without any equipment, outside or at the gym."; + static const String onboarding2Description = + "Our workout programs are made by professionals."; + static const String onboarding3Description = + "We want you to fully enjoy the program and stay healthy and positive."; +} \ No newline at end of file diff --git a/lib/core/extensions/exceptions.dart b/lib/core/extensions/exceptions.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/extensions/list_extension.dart b/lib/core/extensions/list_extension.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/service/auth_service.dart b/lib/core/service/auth_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/service/date_service.dart b/lib/core/service/date_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/service/notification_service.dart b/lib/core/service/notification_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/service/user_service.dart b/lib/core/service/user_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/service/user_storage_service.dart b/lib/core/service/user_storage_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/service/validation_service.dart b/lib/core/service/validation_service.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/main.dart b/lib/main.dart index ac22c56..c6542ec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,29 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:tutorialtest/screens/onboarding/page/onboarding_page.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, ]); - await Firebase.initializeApp(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - final isLoggedIn = FirebaseAuth.instance.currentUser != null; - return MaterialApp( debugShowCheckedModeBanner: false, title: 'Fitness', theme: ThemeData( - textTheme: - TextTheme(bodyText1: TextStyle(color: ColorConstants.textColor)), fontFamily: 'NotoSansKR', scaffoldBackgroundColor: Colors.white, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: isLoggedIn ? TabBarPage() : OnboardingPage(), + //home: isLoggedIn ? TabBarPage() : OnboardingPage(), + home: OnboardingPage(), ); } } \ No newline at end of file diff --git a/lib/screens/onboarding/bloc/onboarding_bloc.dart b/lib/screens/onboarding/bloc/onboarding_bloc.dart new file mode 100644 index 0000000..e953583 --- /dev/null +++ b/lib/screens/onboarding/bloc/onboarding_bloc.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'onboarding_event.dart'; +import 'onboarding_state.dart'; + +class OnboardingBloc extends Bloc { + OnboardingBloc() : super(OnboardingInitial()); + + int pageIndex = 0; + + final pageController = PageController(initialPage: 0); + + @override + Stream mapEventToState( + OnboardingEvent event, + ) async* { + if (event is PageChangedEvent) { + if (pageIndex == 2) { + yield NextScreenState(); + return; + } + pageIndex += 1; + + pageController.animateToPage( + pageIndex, + duration: Duration(milliseconds: 500), + curve: Curves.ease, + ); + + yield PageChangedState(counter: pageIndex); + } else if (event is PageSwipedEvent) { + pageIndex = event.index; + yield PageChangedState(counter: pageIndex); + } + } +} \ No newline at end of file diff --git a/lib/screens/onboarding/bloc/onboarding_event.dart b/lib/screens/onboarding/bloc/onboarding_event.dart new file mode 100644 index 0000000..9843cbc --- /dev/null +++ b/lib/screens/onboarding/bloc/onboarding_event.dart @@ -0,0 +1,16 @@ +//part of 'onboarding_bloc.dart'; + +import 'package:flutter/material.dart'; + +@immutable +abstract class OnboardingEvent {} + +class PageChangedEvent extends OnboardingEvent { + +} + +class PageSwipedEvent extends OnboardingEvent { + final int index; + + PageSwipedEvent({required this.index}); +} \ No newline at end of file diff --git a/lib/screens/onboarding/bloc/onboarding_state.dart b/lib/screens/onboarding/bloc/onboarding_state.dart new file mode 100644 index 0000000..50a16bc --- /dev/null +++ b/lib/screens/onboarding/bloc/onboarding_state.dart @@ -0,0 +1,18 @@ +//part of 'onboarding_bloc.dart'; + +import 'package:flutter/material.dart'; + +@immutable +abstract class OnboardingState {} + +class OnboardingInitial extends OnboardingState {} + +class PageChangedState extends OnboardingState { + final int counter; + + PageChangedState({ + required this.counter, + }); +} + +class NextScreenState extends OnboardingState {} \ No newline at end of file diff --git a/lib/screens/onboarding/page/onboarding_page.dart b/lib/screens/onboarding/page/onboarding_page.dart new file mode 100644 index 0000000..003e8f2 --- /dev/null +++ b/lib/screens/onboarding/page/onboarding_page.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../bloc/onboarding_bloc.dart'; +import '../bloc/onboarding_state.dart'; +import '../widget/onboarding_content.dart'; + +class OnboardingPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: _buildBody(context), + ); + } + + BlocProvider _buildBody(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => OnboardingBloc(), + child: BlocConsumer( + listenWhen: (_, currState) => currState is NextScreenState, + listener: (context, state) { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) { + //return SignUpPage(); + return Text('Test'); + }, + ), + ); + }, + buildWhen: (_, currState) => currState is OnboardingInitial, + builder: (context, state) { + return OnboardingContent(); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/onboarding/widget/onboarding_content.dart b/lib/screens/onboarding/widget/onboarding_content.dart new file mode 100644 index 0000000..1fefe46 --- /dev/null +++ b/lib/screens/onboarding/widget/onboarding_content.dart @@ -0,0 +1,114 @@ +import 'package:dots_indicator/dots_indicator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:percent_indicator/circular_percent_indicator.dart'; + +import '../../../core/const/color_constants.dart'; +import '../../../core/const/data_constants.dart'; +import '../bloc/onboarding_bloc.dart'; +import '../bloc/onboarding_event.dart'; +import '../bloc/onboarding_state.dart'; + +class OnboardingContent extends StatelessWidget { + @override + Widget build(BuildContext context) { + final bloc = BlocProvider.of(context); + return SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 4, + child: _createPageView(bloc.pageController, bloc), + ), + Expanded( + flex: 2, + child: _createStatic(bloc), + ), + ], + ), + ); + } + + Widget _createPageView(PageController controller, OnboardingBloc bloc) { + return PageView( + scrollDirection: Axis.horizontal, + controller: controller, + children: DataConstants.onboardingTiles, + onPageChanged: (index) { + bloc.add(PageSwipedEvent(index: index)); + }, + ); + } + + Widget _createStatic(OnboardingBloc bloc) { + return Column( + children: [ + SizedBox( + height: 30, + ), + BlocBuilder( + buildWhen: (_, currState) => currState is PageChangedState, + builder: (context, state) { + return DotsIndicator( + dotsCount: 3, + position: bloc.pageIndex.toDouble(), + decorator: DotsDecorator( + color: Colors.grey, + activeColor: ColorConstants.primaryColor, + ), + ); + }, + ), + Spacer(), + BlocBuilder( + buildWhen: (_, currState) => currState is PageChangedState, + builder: (context, state) { + final percent = _getPercent(bloc.pageIndex); + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: percent), + duration: Duration(seconds: 1), + builder: (context, value, _) => CircularPercentIndicator( + radius: 110, + backgroundColor: ColorConstants.primaryColor, + progressColor: Colors.white, + percent: 1 - value, + center: Material( + shape: CircleBorder(), + color: ColorConstants.primaryColor, + child: RawMaterialButton( + shape: CircleBorder(), + onPressed: () { + bloc.add(PageChangedEvent()); + }, + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Icon( + Icons.east_rounded, + size: 38.0, + color: Colors.white, + ), + ), + ), + ), + )); + }, + ), + SizedBox(height: 30), + ], + ); + } + + double _getPercent(int pageIndex) { + switch (pageIndex) { + case 0: + return 0.25; + case 1: + return 0.65; + case 2: + return 1; + default: + return 0; + } + } +} \ No newline at end of file diff --git a/lib/screens/onboarding/widget/onboarding_tile.dart b/lib/screens/onboarding/widget/onboarding_tile.dart new file mode 100644 index 0000000..9beac1a --- /dev/null +++ b/lib/screens/onboarding/widget/onboarding_tile.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class OnboardingTile extends StatelessWidget { + final title, imagePath, mainText; + + OnboardingTile({this.imagePath, this.mainText, this.title}); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + children: [ + const SizedBox(height: 34), + Expanded( + child: Image.asset( + imagePath, + ), + ), + const SizedBox(height: 65), + Text( + title, + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24.0, + ), + ), + const SizedBox(height: 15), + Padding( + padding: EdgeInsets.symmetric( + horizontal: screenWidth / 100, + ), + child: Text( + mainText, + style: TextStyle( + fontSize: 16.0, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index df52a27..6333c38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + dots_indicator: ^4.0.1 + percent_indicator: ^4.2.5 dev_dependencies: flutter_test: @@ -53,12 +55,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - fonts: - - family: NotoSansKR - fonts: - - asset: assets/fonts/NotoSansKR/NotoSansKR-Regular.ttf - - asset: assets/fonts/NotoSansKR/NotoSansKR-Bold.ttf - weight: 700 + # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. @@ -66,6 +63,21 @@ flutter: # To add assets to your application, add an assets section, like this: assets: + - assets/images/onboarding/ + - assets/images/auth/ + - assets/images/home/ + - assets/images/exercises/ + - assets/icons/home/ + - assets/icons/workouts/ + - assets/icons/social_networks/ + - assets/videos/workouts/ + + fonts: + - family: NotoSansKR + fonts: + - asset: assets/fonts/NotoSansKR/NotoSansKR-Regular.ttf + - asset: assets/fonts/NotoSansKR/NotoSansKR-Bold.ttf + weight: 700 # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg diff --git a/test/widget_test.dart b/test/widget_test.dart index e0a41ae..748f4cb 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:tutorialtest/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);