From a6003d4027975364486bda2df1e573811db2637d Mon Sep 17 00:00:00 2001 From: Oniel <51058739+0niel@users.noreply.github.com> Date: Sat, 3 Sep 2022 17:42:36 +0300 Subject: [PATCH 01/38] fix: Add SafeArea to profile bottom modal sheet --- .../profile/widgets/bottom_error_info.dart | 136 +++++++++--------- 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/lib/presentation/pages/profile/widgets/bottom_error_info.dart b/lib/presentation/pages/profile/widgets/bottom_error_info.dart index 27491e45..8d15de40 100644 --- a/lib/presentation/pages/profile/widgets/bottom_error_info.dart +++ b/lib/presentation/pages/profile/widgets/bottom_error_info.dart @@ -8,78 +8,82 @@ class BottomErrorInfo extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - height: MediaQuery.of(context).size.height * 0.95, - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - DarkThemeColors.secondary, - DarkThemeColors.deactive, - DarkThemeColors.background01 - ], - begin: Alignment(-1, -1), - end: Alignment(-1, 1), - ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(25.0), - topRight: Radius.circular(25.0), - ), - ), - child: Padding( - padding: const EdgeInsets.all(3.0), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24), - decoration: const BoxDecoration( - color: DarkThemeColors.background01, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(25.0), - topRight: Radius.circular(25.0)), + return SafeArea( + child: Container( + height: MediaQuery.of(context).size.height * 0.95, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + DarkThemeColors.secondary, + DarkThemeColors.deactive, + DarkThemeColors.background01 + ], + begin: Alignment(-1, -1), + end: Alignment(-1, 1), + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(25.0), + topRight: Radius.circular(25.0), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.symmetric(vertical: 40), - child: Image( - image: AssetImage('assets/images/Saly-39.png'), - height: 205.0, + ), + child: Padding( + padding: const EdgeInsets.all(3.0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 24), + decoration: const BoxDecoration( + color: DarkThemeColors.background01, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(25.0), + topRight: Radius.circular(25.0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Image( + image: AssetImage('assets/images/Saly-39.png'), + height: 205.0, + ), + ), + Text( + "Профиль теперь недоступен", + style: DarkTextTheme.h5, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + "Разработчики, отвечающие за API ЛКС, отключили возможность производить аутентификацию и получать данные своего аккаунта. Пожалуйста, используйте lk.mirea.ru", + style: DarkTextTheme.captionL + .copyWith(color: DarkThemeColors.deactive), + textAlign: TextAlign.center, ), - ), - Text( - "Профиль теперь недоступен", - style: DarkTextTheme.h5, - ), - const SizedBox(height: 8), - Text( - "Разработчики, отвечающие за API ЛКС, отключили возможность производить аутентификацию и получать данные своего аккаунта. Пожалуйста, используйте lk.mirea.ru", - style: DarkTextTheme.captionL - .copyWith(color: DarkThemeColors.deactive), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - ConstrainedBox( - constraints: const BoxConstraints.tightFor( - width: double.infinity, height: 48), - child: ElevatedButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(DarkThemeColors.primary), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24.0), + const SizedBox(height: 32), + ConstrainedBox( + constraints: const BoxConstraints.tightFor( + width: double.infinity, height: 48), + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(DarkThemeColors.primary), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24.0), + ), ), ), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - 'Понятно!', - style: DarkTextTheme.buttonS, + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Понятно!', + style: DarkTextTheme.buttonS, + ), ), ), - ), - ], + ], + ), ), ), ), From 3b37d724cad7aed0c86ba225b5336a5cccf889eb Mon Sep 17 00:00:00 2001 From: Oniel <51058739+0niel@users.noreply.github.com> Date: Sat, 3 Sep 2022 17:43:02 +0300 Subject: [PATCH 02/38] refactor: Update tabs router navbar builder --- lib/presentation/pages/home_page.dart | 34 +++++---------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index f74e30f1..1554159b 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -26,34 +26,12 @@ class HomePage extends StatelessWidget { builder: (context, child, animation) { final tabsRouter = AutoTabsRouter.of(context); - return Column( - children: [ - Expanded( - child: FadeTransition( - opacity: animation, - child: BlocListener( - child: child, - listener: (context, state) => - UpdateInfoDialog.checkAndShow(context, state), - ), - ), - ), - Container( - decoration: const BoxDecoration( - color: DarkThemeColors.background01, - ), - child: SafeArea( - left: false, - top: false, - right: false, - bottom: true, - child: AppBottomNavigationBar( - index: tabsRouter.activeIndex, - onClick: tabsRouter.setActiveIndex, - ), - ), - ), - ], + return Scaffold( + body: child, + bottomNavigationBar: AppBottomNavigationBar( + index: tabsRouter.activeIndex, + onClick: tabsRouter.setActiveIndex, + ), ); }, ); From 146dd5800cfacd8b17aed3af8b158bb28d893a39 Mon Sep 17 00:00:00 2001 From: Oniel <51058739+0niel@users.noreply.github.com> Date: Thu, 22 Sep 2022 18:05:21 +0300 Subject: [PATCH 03/38] refactor: Change android target sdk version --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5405dcf1..8af920d1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,7 +48,7 @@ android { multiDexEnabled true applicationId "ninja.mirea.mireaapp" minSdkVersion 19 - targetSdkVersion 29 + targetSdkVersion 32 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } From ef2393fd5bac301d62991d258cdad0a3dd5374ad Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Mon, 26 Sep 2022 12:50:13 +0300 Subject: [PATCH 04/38] refactor: Upgrade Android `compileSdkVersion` (#247) --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8af920d1..54c69630 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -28,7 +28,7 @@ apply plugin: 'com.google.gms.google-services' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 32 ndkVersion flutter.ndkVersion compileOptions { From e068d239acd2bb580d87eac7321d70c46bd142b5 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 27 Sep 2022 17:38:55 +0300 Subject: [PATCH 05/38] ui: Create news shimmer loading effect (#248) --- lib/presentation/pages/news/news_page.dart | 107 +++++++++++++++++++-- 1 file changed, 98 insertions(+), 9 deletions(-) diff --git a/lib/presentation/pages/news/news_page.dart b/lib/presentation/pages/news/news_page.dart index 6ea30343..1343b1e2 100644 --- a/lib/presentation/pages/news/news_page.dart +++ b/lib/presentation/pages/news/news_page.dart @@ -10,6 +10,7 @@ import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/app_settings_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_tab_button.dart'; +import 'package:shimmer/shimmer.dart'; import 'widgets/news_item.dart'; import 'widgets/story_item.dart'; import 'widgets/tags_widgets.dart'; @@ -36,6 +37,8 @@ class _NewsPageState extends State { ); } + /// Show iOS-style bottom sheet with tags. When user taps on tag, news will be filtered by it. + /// If user taps on "все", news filter will be reset. void _showTagsModalWindow(BuildContext context) { showCupertinoModalPopup( context: context, @@ -90,10 +93,13 @@ class _NewsPageState extends State { itemIndex: 1, notifier: _tabValueNotifier, onClick: () { - context.read().add(NewsLoadEvent( - refresh: true, - isImportant: _tabValueNotifier.value == 1, - tag: "все")); + context.read().add( + NewsLoadEvent( + refresh: true, + isImportant: _tabValueNotifier.value == 1, + tag: "все", + ), + ); }, ) ], @@ -101,9 +107,10 @@ class _NewsPageState extends State { Padding( padding: const EdgeInsets.only(right: 12), child: AppSettingsButton( - onClick: () => (context.read().state is NewsLoaded) - ? _showTagsModalWindow(context) - : null), + onClick: () => (context.read().state is NewsLoaded) + ? _showTagsModalWindow(context) + : null, + ), ), ], ), @@ -204,8 +211,20 @@ class _NewsPageState extends State { } else if (state is NewsLoaded) { news = state.news; } else if (state is NewsLoading && state.isFirstFetch) { - return const Center( - child: CircularProgressIndicator(), + return Expanded( + child: Column( + children: [ + const SizedBox(height: 12), + Expanded( + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: 3, + itemBuilder: (context, index) => + const _ShimmerNewsCardLoading(), + ), + ), + ], + ), ); } else if (state is NewsLoading) { news = state.oldNews; @@ -267,3 +286,73 @@ class _NewsPageState extends State { }); } } + +/// Widget with news card loading animation (shimmer effect). +/// Used for first-time loading. +class _ShimmerNewsCardLoading extends StatelessWidget { + const _ShimmerNewsCardLoading({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 24, left: 16, right: 16), + decoration: BoxDecoration( + color: DarkThemeColors.background02, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Shimmer.fromColors( + baseColor: DarkThemeColors.background01, + highlightColor: DarkThemeColors.background02, + child: Container( + height: 175, + decoration: BoxDecoration( + color: DarkThemeColors.background02, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16, bottom: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Shimmer.fromColors( + baseColor: DarkThemeColors.background01, + highlightColor: DarkThemeColors.background02, + child: Container( + height: 18, + decoration: BoxDecoration( + color: DarkThemeColors.background02, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + const SizedBox(height: 4), + Shimmer.fromColors( + baseColor: DarkThemeColors.background01, + highlightColor: DarkThemeColors.background02, + child: Container( + height: 18, + width: MediaQuery.of(context).size.width * 0.4, + decoration: BoxDecoration( + color: DarkThemeColors.background02, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ); + } +} From 933617c8a751da5f28d9f113cb359c9be6e4ed3d Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 27 Sep 2022 19:12:04 +0300 Subject: [PATCH 06/38] fix(android 12): Add explicit value for `android:exported` (#249) --- android/app/src/main/AndroidManifest.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9369b400..d0be17b8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -14,7 +14,8 @@ android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustResize" + android:exported="true"> - + From e17b71ebf0f8f2ddec68a58c418eb4e9fdc1ca00 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 27 Sep 2022 23:11:16 +0300 Subject: [PATCH 07/38] ui: Update Schedule page Drawer (#250) --- .../pages/onboarding/widgets/next_button.dart | 4 +- .../pages/schedule/schedule_page.dart | 495 +++++++++--------- .../schedule/widgets/schedule_page_view.dart | 4 +- .../widgets/schedule_settings_drawer.dart | 46 ++ pubspec.yaml | 10 +- 5 files changed, 301 insertions(+), 258 deletions(-) create mode 100644 lib/presentation/pages/schedule/widgets/schedule_settings_drawer.dart diff --git a/lib/presentation/pages/onboarding/widgets/next_button.dart b/lib/presentation/pages/onboarding/widgets/next_button.dart index 00457600..6c2618f7 100644 --- a/lib/presentation/pages/onboarding/widgets/next_button.dart +++ b/lib/presentation/pages/onboarding/widgets/next_button.dart @@ -28,10 +28,8 @@ class NextPageViewButton extends StatelessWidget { } }, style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), - onPrimary: DarkThemeColors.primary.withOpacity(0.25), + foregroundColor: DarkThemeColors.primary.withOpacity(0.25), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), backgroundColor: DarkThemeColors.primary, shadowColor: const Color(0x7f000000), - primary: DarkThemeColors.primary, elevation: 8.0, ), child: AnimatedContainer( diff --git a/lib/presentation/pages/schedule/schedule_page.dart b/lib/presentation/pages/schedule/schedule_page.dart index cdd12cc2..d2983f10 100644 --- a/lib/presentation/pages/schedule/schedule_page.dart +++ b/lib/presentation/pages/schedule/schedule_page.dart @@ -1,11 +1,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_inner_drawer/inner_drawer.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rtu_mirea_app/domain/entities/schedule.dart'; import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_drawer.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_modal.dart'; import 'package:rtu_mirea_app/presentation/widgets/settings_switch_button.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; @@ -21,17 +21,40 @@ class SchedulePage extends StatefulWidget { class _SchedulePageState extends State { bool _modalShown = false; - // Current State of InnerDrawerState - final GlobalKey _innerDrawerKey = - GlobalKey(); - @override void initState() { super.initState(); + + WidgetsBinding.instance + .addPostFrameCallback((_) => _showScheduleSettingsModal(context)); + } + + /// Shows schedule settings modal if it's not shown yet and if there is no + /// schedule data in the cache. + void _showScheduleSettingsModal(BuildContext context) { + final state = context.read().state; + if (state is ScheduleActiveGroupEmpty) { + if (!_modalShown) { + showModalBottomSheet( + useRootNavigator: false, + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => const ScheduleSettingsModal(isFirstRun: true), + ).whenComplete(() { + _modalShown = false; + }); + } + _modalShown = true; + } } Widget _buildGroupButton( - String group, String activeGroup, bool isActive, Schedule schedule) { + String group, + String activeGroup, + bool isActive, + Schedule schedule, + ) { if (isActive) { return Padding( padding: const EdgeInsets.only(bottom: 10), @@ -141,259 +164,241 @@ class _SchedulePageState extends State { @override Widget build(BuildContext context) { - return Material( - child: InnerDrawer( - key: _innerDrawerKey, - offset: IDOffset.horizontal( - (100 / (MediaQuery.of(context).size.width / 250)) / 100), - swipeChild: true, - onTapClose: true, - boxShadow: const [], - rightChild: SafeArea( - bottom: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 18), - child: Text( - 'Управление расписанием и группами', - style: DarkTextTheme.h6, - ), - ), - BlocBuilder( - buildWhen: (prevState, currentState) { - if (currentState is ScheduleLoaded && - prevState is ScheduleLoaded) { - if (prevState.activeGroup != currentState.activeGroup || - prevState.downloadedScheduleGroups != - currentState.downloadedScheduleGroups || - prevState.schedule.isRemote != - currentState.schedule.isRemote) return true; - } - if (currentState is ScheduleLoaded && - prevState.runtimeType != ScheduleLoaded) return true; - return false; - }, builder: (context, state) { - if (state is ScheduleLoaded || - state is ScheduleActiveGroupEmpty) { - return Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (state is ScheduleLoaded) - SettingsSwitchButton( - initialValue: - state.scheduleSettings.showEmptyLessons, - svgPicture: SvgPicture.asset( - 'assets/icons/lessons.svg', - height: 16, - width: 16, - ), - text: "Пустые пары", - onChanged: (value) { - context.read().add( - ScheduleUpdateSettingsEvent( - showEmptyLessons: value)); - }, + return Scaffold( + backgroundColor: DarkThemeColors.background01, + endDrawer: ScheduleSettingsDrawer( + builder: (_) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + BlocBuilder( + buildWhen: (prevState, currentState) { + if (currentState is ScheduleLoaded && + prevState is ScheduleLoaded) { + if (prevState.activeGroup != currentState.activeGroup || + prevState.downloadedScheduleGroups != + currentState.downloadedScheduleGroups || + prevState.schedule.isRemote != + currentState.schedule.isRemote) return true; + } + if (currentState is ScheduleLoaded && + prevState.runtimeType != ScheduleLoaded) return true; + return false; + }, builder: (context, state) { + if (state is ScheduleLoaded || + state is ScheduleActiveGroupEmpty) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state is ScheduleLoaded) + SettingsSwitchButton( + initialValue: + state.scheduleSettings.showEmptyLessons, + svgPicture: SvgPicture.asset( + 'assets/icons/lessons.svg', + height: 16, + width: 16, ), - // SizedBox(height: 10), - // SettingsSwitchButton( - // initialValue: - // state.scheduleSettings.showLessonsNumbers, - // svgPicture: SvgPicture.asset( - // 'assets/icons/number.svg', - // height: 16, - // width: 16, - // ), - // text: "Номера пар", - // onChanged: (value) { - // context.read().add( - // ScheduleUpdateSettingsEvent( - // showLesonsNums: value)); - // }, - // ), - Material( - color: Colors.transparent, - child: InkWell( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 20), - child: Row( - children: [ - SvgPicture.asset( - 'assets/icons/add_group.svg', - height: 16, - width: 16, - ), - const SizedBox(width: 20), - Text("Добавить группу", - style: DarkTextTheme.buttonL), - ], - ), + text: "Пустые пары", + onChanged: (value) { + context.read().add( + ScheduleUpdateSettingsEvent( + showEmptyLessons: value)); + }, + ), + // SizedBox(height: 10), + // SettingsSwitchButton( + // initialValue: + // state.scheduleSettings.showLessonsNumbers, + // svgPicture: SvgPicture.asset( + // 'assets/icons/number.svg', + // height: 16, + // width: 16, + // ), + // text: "Номера пар", + // onChanged: (value) { + // context.read().add( + // ScheduleUpdateSettingsEvent( + // showLesonsNums: value)); + // }, + // ), + Material( + color: Colors.transparent, + child: InkWell( + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 20), + child: Row( + children: [ + SvgPicture.asset( + 'assets/icons/add_group.svg', + height: 16, + width: 16, + ), + const SizedBox(width: 20), + Text("Добавить группу", + style: DarkTextTheme.buttonL), + ], ), - Opacity( - opacity: 0.05, - child: Container( - width: double.infinity, - height: 1, - color: Colors.white, - ), + ), + Opacity( + opacity: 0.05, + child: Container( + width: double.infinity, + height: 1, + color: Colors.white, ), - ], - ), - onTap: () { - if (!_modalShown) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - const ScheduleSettingsModal( - isFirstRun: false), - ).whenComplete(() { - _modalShown = false; - }); - } - _modalShown = true; - }, + ), + ], ), + onTap: () { + if (!_modalShown) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => + const ScheduleSettingsModal( + isFirstRun: false), + ).whenComplete(() { + _modalShown = false; + }); + } + _modalShown = true; + }, ), - if (state is ScheduleLoaded) - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 20), - Text( - "Группы".toUpperCase(), - style: DarkTextTheme.chip.copyWith( - color: DarkThemeColors.deactiveDarker), - textAlign: TextAlign.left, - ), - const SizedBox(height: 10), - _buildGroupButton( - state.activeGroup, - state.activeGroup, - true, - state.schedule, - ), - Expanded( - child: ListView.builder( - itemCount: - state.downloadedScheduleGroups.length, - itemBuilder: (context, index) { - if (state.downloadedScheduleGroups[ - index] != - state.activeGroup) { - return _buildGroupButton( - state.downloadedScheduleGroups[ - index], - state.activeGroup, - false, - state.schedule, - ); - } - return Container(); - }, - ), + ), + if (state is ScheduleLoaded) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + "Группы".toUpperCase(), + style: DarkTextTheme.chip.copyWith( + color: DarkThemeColors.deactiveDarker), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + _buildGroupButton( + state.activeGroup, + state.activeGroup, + true, + state.schedule, + ), + Expanded( + child: ListView.builder( + itemCount: + state.downloadedScheduleGroups.length, + itemBuilder: (context, index) { + if (state.downloadedScheduleGroups[ + index] != + state.activeGroup) { + return _buildGroupButton( + state.downloadedScheduleGroups[index], + state.activeGroup, + false, + state.schedule, + ); + } + return Container(); + }, ), - ], - ), + ), + ], ), - ], - ), - ); - } else { - return Container(); - } - }), - ], - ), - ), - ), - scaffold: Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - title: Text( - 'Расписание', - style: DarkTextTheme.title, - ), - backgroundColor: DarkThemeColors.background01, - actions: [ - IconButton( - icon: const Icon(Icons.dehaze), - onPressed: () { - _innerDrawerKey.currentState!.toggle(); - }, - ), + ), + ], + ), + ); + } else { + // Schedule not loaded info + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + Text( + "Группы".toUpperCase(), + style: DarkTextTheme.chip.copyWith( + color: DarkThemeColors.deactiveDarker), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ], + ), + ), + ], + ), + ); + } + }), ], ), - body: SafeArea( - bottom: false, - child: BlocConsumer( - buildWhen: (prevState, currentState) { - if (prevState is ScheduleLoaded && - currentState is ScheduleLoaded) { - return prevState != currentState; - } - return true; - }, - listener: (context, state) { - if (state is ScheduleActiveGroupEmpty) { - if (!_modalShown) { - showModalBottomSheet( - useRootNavigator: false, - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - const ScheduleSettingsModal(isFirstRun: true), - ).whenComplete(() { - _modalShown = false; - }); - } - _modalShown = true; - } - }, - builder: (context, state) { - if (state is ScheduleLoading) { + ), + ), + appBar: AppBar( + backgroundColor: DarkThemeColors.background01, + elevation: 0, + title: const Text('Расписание'), + ), + body: Container( + color: DarkThemeColors.background01, + child: SafeArea( + child: BlocBuilder( + buildWhen: (prevState, currentState) { + if (prevState is ScheduleLoaded && + currentState is ScheduleLoaded) { + return prevState != currentState; + } + return true; + }, + builder: (context, state) { + if (state is ScheduleLoading) { + // Add post frame callback to hide modal after build + WidgetsBinding.instance.addPostFrameCallback((_) { if (_modalShown) { _modalShown = false; context.router.root.pop(); } - return const Center( - child: CircularProgressIndicator( - backgroundColor: DarkThemeColors.primary, - strokeWidth: 5, + }); + + return const Center( + child: CircularProgressIndicator( + backgroundColor: DarkThemeColors.primary, + strokeWidth: 5, + ), + ); + } else if (state is ScheduleLoaded) { + return SchedulePageView(schedule: state.schedule); + } else if (state is ScheduleLoadError) { + return Column( + children: [ + Text( + 'Упс!', + style: DarkTextTheme.h3, ), - ); - } else if (state is ScheduleLoaded) { - return SchedulePageView(schedule: state.schedule); - } else if (state is ScheduleLoadError) { - return Column( - children: [ - Text( - 'Упс!', - style: DarkTextTheme.h3, - ), - const SizedBox( - height: 24, - ), - Text( - state.errorMessage, - style: DarkTextTheme.bodyBold, - ) - ], - ); - } else { - return Container(); - } - }, - ), + const SizedBox( + height: 24, + ), + Text( + state.errorMessage, + style: DarkTextTheme.bodyBold, + ) + ], + ); + } else { + return Container(); + } + }, ), ), ), diff --git a/lib/presentation/pages/schedule/widgets/schedule_page_view.dart b/lib/presentation/pages/schedule/widgets/schedule_page_view.dart index e3c939fc..2d942684 100644 --- a/lib/presentation/pages/schedule/widgets/schedule_page_view.dart +++ b/lib/presentation/pages/schedule/widgets/schedule_page_view.dart @@ -321,8 +321,8 @@ class _SchedulePageViewState extends State { for (int i = 1; i <= CalendarUtils.kMaxWeekInSemester; i++) ElevatedButton( style: ElevatedButton.styleFrom( - primary: DarkThemeColors.primary, - onPrimary: Colors.white, + foregroundColor: Colors.white, + backgroundColor: DarkThemeColors.primary, shadowColor: Colors.transparent, ), onPressed: () { diff --git a/lib/presentation/pages/schedule/widgets/schedule_settings_drawer.dart b/lib/presentation/pages/schedule/widgets/schedule_settings_drawer.dart new file mode 100644 index 00000000..de017751 --- /dev/null +++ b/lib/presentation/pages/schedule/widgets/schedule_settings_drawer.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; + +class ScheduleSettingsDrawer extends StatelessWidget { + const ScheduleSettingsDrawer({Key? key, required this.builder}) + : super(key: key); + + final Widget Function(BuildContext context) builder; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only( + left: 16, + top: MediaQuery.of(context).padding.top + 16, + right: 16, + bottom: 16, + ), + decoration: BoxDecoration( + color: DarkThemeColors.background01, + borderRadius: BorderRadius.circular(16), + ), + clipBehavior: Clip.antiAliasWithSaveLayer, + child: Drawer( + child: Container( + color: DarkThemeColors.background01, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.only( + left: 16, bottom: 24, top: 24, right: 16), + child: Text("Настройки", style: DarkTextTheme.h5), + ), + Expanded( + child: builder(context), + ), + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d3dfcbd5..c70d8ad1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -85,12 +85,11 @@ dependencies: # A Flutter widget rendering static HTML and CSS as Flutter widgets. # See https://pub.dev/packages/flutter_html - - flutter_html: ^3.0.0-alpha.2 + flutter_html: ^3.0.0-alpha.5 # Iframe widget for flutter_html # See https://pub.dev/packages/flutter_html_iframe - flutter_html_iframe: ^3.0.0-alpha.2 + flutter_html_iframe: ^3.0.0-alpha.3 # The Font Awesome Icon pack available as Flutter Icons. # Provides 1600 additional icons to use in your apps. @@ -136,11 +135,6 @@ dependencies: syncfusion_flutter_gauges: ^20.1.57 auto_route: ^5.0.1 - - flutter_inner_drawer: - git: - url: https://github.com/0niel/flutter_inner_drawer.git - ref: bugfix/type-casting url_launcher: ^6.0.17 From e9bcf849d3ea0609685971cb6f383bb0b96e7c0a Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Wed, 28 Sep 2022 20:01:17 +0300 Subject: [PATCH 08/38] ui: Update empty Schedule page view (#251) --- .../pages/schedule/schedule_page.dart | 139 +++++++++++++----- 1 file changed, 102 insertions(+), 37 deletions(-) diff --git a/lib/presentation/pages/schedule/schedule_page.dart b/lib/presentation/pages/schedule/schedule_page.dart index d2983f10..23612021 100644 --- a/lib/presentation/pages/schedule/schedule_page.dart +++ b/lib/presentation/pages/schedule/schedule_page.dart @@ -7,6 +7,7 @@ import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_drawer.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_modal.dart'; +import 'package:rtu_mirea_app/presentation/widgets/buttons/colorful_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/settings_switch_button.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'widgets/schedule_page_view.dart'; @@ -24,28 +25,14 @@ class _SchedulePageState extends State { @override void initState() { super.initState(); - - WidgetsBinding.instance - .addPostFrameCallback((_) => _showScheduleSettingsModal(context)); } - /// Shows schedule settings modal if it's not shown yet and if there is no - /// schedule data in the cache. - void _showScheduleSettingsModal(BuildContext context) { - final state = context.read().state; - if (state is ScheduleActiveGroupEmpty) { - if (!_modalShown) { - showModalBottomSheet( - useRootNavigator: false, - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => const ScheduleSettingsModal(isFirstRun: true), - ).whenComplete(() { - _modalShown = false; - }); - } - _modalShown = true; + @override + void dispose() { + super.dispose(); + // dispose mounted modal + if (_modalShown) { + Navigator.of(context).pop(); } } @@ -162,6 +149,23 @@ class _SchedulePageState extends State { } } + /// Show modal with group settings. If [_modalShown] is true, then modal is + /// already shown and we don't need to show it again. + void _showModal({bool? isFirstRun}) { + if (!_modalShown) { + _modalShown = true; + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => + ScheduleSettingsModal(isFirstRun: isFirstRun ?? true), + ).whenComplete(() { + _modalShown = false; + }); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -254,21 +258,7 @@ class _SchedulePageState extends State { ), ], ), - onTap: () { - if (!_modalShown) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - const ScheduleSettingsModal( - isFirstRun: false), - ).whenComplete(() { - _modalShown = false; - }); - } - _modalShown = true; - }, + onTap: () => _showModal(isFirstRun: false), ), ), if (state is ScheduleLoaded) @@ -353,7 +343,18 @@ class _SchedulePageState extends State { body: Container( color: DarkThemeColors.background01, child: SafeArea( - child: BlocBuilder( + child: BlocConsumer( + listener: (context, state) { + if (state is ScheduleActiveGroupEmpty) { + if (!_modalShown) { + // show after 300 ms + Future.delayed( + const Duration(milliseconds: 300), + () => _showModal(), + ); + } + } + }, buildWhen: (prevState, currentState) { if (prevState is ScheduleLoaded && currentState is ScheduleLoaded) { @@ -396,7 +397,7 @@ class _SchedulePageState extends State { ], ); } else { - return Container(); + return _NoActiveGroupFoundMessage(onTap: () => _showModal()); } }, ), @@ -405,3 +406,67 @@ class _SchedulePageState extends State { ); } } + +class _NoActiveGroupFoundMessage extends StatelessWidget { + const _NoActiveGroupFoundMessage({Key? key, required this.onTap}) + : super(key: key); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: Future.delayed(const Duration(milliseconds: 600)), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + backgroundColor: DarkThemeColors.primary, + strokeWidth: 5, + ), + ); + } else { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Image.asset( + 'assets/images/Saly-2.png', + height: 200, + ), + ), + const SizedBox( + height: 8, + ), + Text( + "Не установлена активная группа", + style: DarkTextTheme.h5, + ), + const SizedBox( + height: 8, + ), + Text( + "Скачайте расписание по крайней мере для одной группы, чтобы отобразить календарь.", + style: DarkTextTheme.captionL.copyWith( + color: DarkThemeColors.deactive, + ), + ), + const SizedBox( + height: 24, + ), + ColorfulButton( + text: "Настроить", + onClick: onTap, + backgroundColor: DarkThemeColors.primary, + ), + ], + ), + ); + } + }, + ); + } +} From 66493b4d3bc11fe6c3ac4c9665753414d67d5cb5 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Sun, 22 Jan 2023 23:45:19 +0300 Subject: [PATCH 09/38] feature: Create oauth2 client for lks integration (#256) * feature: Create oauth2 client for lks integration * fix: New employee model and secure storage * dev: Change `flutter_secure_storage` version --- android/app/build.gradle | 2 +- android/app/src/main/AndroidManifest.xml | 8 ++ assets/icons/gerb.ico | Bin 0 -> 15086 bytes lib/common/oauth.dart | 39 +++++++++ lib/data/datasources/user_remote.dart | 75 ++++++++---------- lib/data/models/employee_model.dart | 8 +- .../repositories/user_repository_impl.dart | 2 +- lib/domain/entities/employee.dart | 6 +- lib/main.dart | 10 ++- .../pages/profile/profile_detail_page.dart | 11 +-- .../pages/profile/profile_page.dart | 73 +++++++++++------ .../pages/profile/profile_scores_page.dart | 31 +++++--- .../profile/widgets/lector_search_card.dart | 6 +- lib/service_locator.dart | 4 +- pubspec.yaml | 13 ++- 15 files changed, 189 insertions(+), 99 deletions(-) create mode 100644 assets/icons/gerb.ico create mode 100644 lib/common/oauth.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 54c69630..d5451350 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -28,7 +28,7 @@ apply plugin: 'com.google.gms.google-services' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 32 + compileSdkVersion 33 ndkVersion flutter.ndkVersion compileOptions { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d0be17b8..70b1a38e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,14 @@ android:label="Ninja Mirea" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher"> + + + + + + + + OSS%UVFxKqZEX*gcJf^T%^I0s` zgb6RNN3&R!C>xDA)t`Ku#nM29>8OJytV5Vc&ip37$lmndPu5(J`FQiKPqsTGU!g>0 zlD%o(*Tq-7s5N|xS=f>(+Tt_w?MjjlCp_$=L}iju-O5Ya!(W%@P1t{ZT7T?b-$Lyb zK8;<|om-#pjqiiw>Cc!%Ws*@H>C8H%H}*~V@NY3E-Sl5D*SaWqx2W^kM7KvU%e#m1 z`$$qBP@?-F8P$;v>5|P*zVWgXWVUSDJQ~x795(m=tgr312${0nr95wzM+fYV?g2U1 zM=-;q8;+zrh5se?GKtD0qdL+dU9#ED55+f^#oE4g`-i_=zjo9>S0}~BlAr8oXLmtG zS>^qq_xC#GeR~g#P*+p`>iE&4a__(U{#!$_cG|@+-63pyINh}qHb->BLccEHId#L1 z=tpoc{vng7Ofst5EbM}5sJkPgy=U9~V8uJvuBy$Tb-1@GQqtgXfXXg|>EzO&Iw{HGiiudhBy-^23YyB02I$(}RD{Kz$1fD|& zd>7aS`(t}xe@r*52<(LM4(+f-*a>qyJ78r%8~p5@-e_WaKit#JwcE?X4U7#9Ku<>- zf&v7Pos|g{6%|D;F0NcD&+F!zn!3Y;p`ptR-Gb828``Ixy>@HQ$z#W-SeP2`HnDdN z)b zb2Bi~*8>MzYjAaT0#A2$NKHwBveMF?z<_{-uj}`3?I&p_ny<9WhAS>neKz)v_3kdt z&jS5@AuP}z!h-}5;Nu0@rr_@F4VIq%pyCw;>#dUDsEr7WUH!qq(FyFV_;BCY2rMnk zAi&QTl9Q64w7ddJN=qRlJsom$av&!=JJ-hAX8dpYU-z1Q+51abowVS^wuSu;{z3=~ z3S`>N#mNBz@IHe5JRro=8btnfkQnX(iD7u{!3Q6EBe3V2fRTYdU|)yC_&BJos)W+o zMlgvhf$KrJpdFOf5g!*D92^v+?(XI`_jP^!GyBnJ+&}qDReY51;u9Gc9R;zN{C$1E z&P*4g{H!5IXakkWKG2vG25l7?P@N_~nKdMYxIn0{GbF^tL3LH-KuTU|=l=q0`@XU) z?wM?uRWU(DbotnP-aCKB`#N&=IPTl46jzSfvun5f@gs*foj-Z(hPtYXSb)zzJ2Mj= zh$A2)MgU#yP4Gxk49!LHP#R+g<*~NVR+=gf37~Rvq<6e*9b6-P^YoSll;lu(q@S^ZO=XsILp!8tR~} zsSc(_2H@;q2f?nUP*)&^=g*!%UW7H6TwVrd>ifX^^jHWw^C|dft^-@c!%&;-2m}4U zf}|n`%5&nOueTTa`}?66^X~30kVqt*`FXkV;bCES)$ge*Oqo3GwVwy}4^0}A4fS+E zM^l3tH@w_kAtp)$1^IbUUX%lQ;y{pOhd^U~Gtq7% z8Y>`4dlqz*#X(c95VGR@AvsLN^-EB8B11+>(}(3@y#g(ACuit*xz+oTTuA>fHE&*1{-AcRUAv zCq_fanU6tmb2ezWJA#s@7dYteg1EbENK)lOW!O!;2QgG+gh6FN>Oez%0~8kI;}|Li zQA7j;2L^z*hdVgg+djwV4w~v}lG`_bIXrCGd;e-pRFGd_*4Wrc<1wVAra(n`IW#sj zH0I~#7zKH|e;wm(tSKo;ftLITh*bX$d@oM~uiM{%fwe05x!(gfyX$b%%^fVwPC=;l ze8{p}4PBLDsLzXn#1O||aa_!Wit?WIua`!f^=Tmcue18~RD64dxA5Mj9rI*SD$$rFJ%)X`mLpW)=u zXWVA4Sp4m~w~p^0bL;YjiDyrrRyce5=OtGzU0QzU=FPd+uU`H1AM}}5RIgmVIy@vO zXlr<6)V-?~zDK^e5wzfwD}p)PJ3=k4dP269w|BSXfdCp(oFMWJ_Iq1ZFy~)}RGpbn z9&iq-Le4_A`Al#!-38_rry$c|Hne8jK~r`x*!l-_69$8IBVg{hdx;94-w{n5b;{*G z|Hk?Bu-UwE>dD?5qrzg2X?YXJtg3{gm6mtk)33L!I36UKUXY|Q4a6E#A{V2 z(^wvg-8VpmU^mo9pNGcyOHd!N2O5MMpfPMQG)Jv~p6XbrPxl8UT=NkIVG$-_GaST) zoZLI%tI3Cd;PNY~;}>swhkGMnFGn-goui$R!O_cUV;h$~pKe-IF0HRsh{K+0a!M1f?#spvwO{Xo%biO>z66HE|!bCvArgu_APd z7eQaW0NRS;AXOa3*b>HWQ3ot`CeFZ$OGC<$cpdrH_Lc)9) z8?q$~!Xiw~)2AGwilf8sVH=3epQV!sig{!?uG z7LGywQ!c+26rAf}U0^fp3~PcPy((b}zZ}@s&A@i-d5q)TE?E}qJ?=UC1H1TP4IvRh zP>t`VBr62kisPWQFqT>GRHp|)SX4OdMjg^68;(ZGF2W$JDf}`9n>f}5HX&yn5I6Zi zzA!$aZ{*`2-Gkhy5yLpTnR+-Dr=d@Q9E;j!j#JOTGC>y{PJRl9;=17{VI!>ZC}W;E z+o22FrLBs4*@OAU<1O7jo~SDZq?Gc*LhYk~)2qeBL~!ouLHa~M$FIMg9s z@_}p#gRrO{5jJrU*Wu(RK-}a5;|uvjzM+qDqlW)(T`Idr=^gG>pT+pD-$Bd{w&O#P zwQGUz+^b=YUju9pmB2os1m?J4zp+Q(9ec?CO72Bh9{DnU#lH7Ankg>hteSv*zY-=J zWx`x-VJ#&plZ@&}hjbbL(Uve6EW{*i;vg>KByRHIC07Ue#=Yvjn78}xAC9S9m6(-m zo91Tn5AXRgjseeUY~CCD2#zK7!l9@ZQ1qyP@fKBh51qiVYRclBJW@Q9OE(ebV&Rdwudyr!3YVg^2auG>BD!otDU2resKK4^&gC% zvs?y4+t~lIOgZ#kTceIybszV3_qv)m$%bqRgTX?~RsIr&gSZ$@ z5wkjkS$>l7GV-LaS#`A5;qThAYaHQj?M$4mpz*E$Kl5{?vUWvamhmg$K-5GPp2Da z!%WK(>>m<*Z|a_M??xEmJNktbpUNb|e*BblNSAELmN2-N+!aWdu!)1Xh?BU<2aW~$ z@?yMeBp-Q4O~#PVQhaHSeN2zubzJjw;PdLD@h|q5 zi-}S_%9rgrGD1#0?ih|IETab+`IgJ)7rP^=zDVO9^2SrW9YA zk&oo7R7aYlt}H%FCGG{6Ssd%8e0 zEW(tNoBF$XiF{yuL7y1kw9}QySE)bJTyFB$%=>`j+6;V$+pxc-;kYpv=b!M!xWO3A zPocH8M-sa0W~m13k~PSwn{6QO|587>Usf+uJV`}p^u1U8uZI#I!4%gnCQ+GWR7W~9 z^~C+N4HEjvmM{nl@4;5;%TO-zfqWsKxHp4v*kQ4xzDo6n<~ZlfVcWEJ;5c<*s2sm( zmU7j_hsSE99@y?x=y)W)t8S%+=aerl`zXx5?%*)I5x)O*vYj2ANehn@lKeJC3;t#x=s*T$Sh(cn?Kv zD@ilkAiwNuOZ>jKECec(jOuXxdt&^G{mdHV-|)Oi8#(R-8Qys(E{=2a*gxBS|ASR( zDy9_nX`Am!Ms=j~CVlv?u(|6_kCIag^QT1R{|fj23P*O^wJ{u(m}p8=ep9@_{8%i2 z{^bCaSC4p+#}0duE6Ye2xw6bX$~%==GL%1jnRnqroUWhXM-}A*I2OIkl_-yWd9BQ1 zji76~5oxZ3MeI$=%Q4r)MDlVLYdPiBFZI!m!GD<}Jd1^P{gk7fG)HUN~GDL zx?e_lIp$BFjvQ@oPdST4`Lm~jQTA1s*K~cPi8(y09#I8!57jT*Pnvgq#M~DQDE(1R z=d>nZ`VQ0JIjzxn@gMy5-PIj2QCTRz)w@KO5|#Jg)x*1a^S=$&i8B>Fs~qRMOR5iv z`}!$SnPeNS6W-|fJ=-LG!w)_UrSp7yU|Dc497}n^Br21P>PUxLVf)st!|&X>xd``~ z7VBtf{p0!hp6b1i)bHJ!v~&A*X1#JiS#QKT|N4+c{@w6xKo2PR^uR$p2Ska=O9b7p z80*&h)`lsmsLsPZItwcc^G-Ksr=IxO*jsuAri1Gi^6#&Hh6YAs-CSH$^zU0LpE!R- z?#z;@!%z8SsBQMHfGvL2Fvqz8wncQp*2r#{>D~c92-;wyPZel*2y30}Z0l@rUl{k> zAS5^lii(P=Vq;?`{uSqM>$R*rK3s1eCp=~?EVH(=55e;^b)J5L0T&+u7&!ZbhIbU4 z@XUbCcIj~2SqSEK&dhlgI=>zxH?0irvt zgtoF|s3=H>w0I%p<>ftA@=eYBN;mP|`&UC>S&#jl`z4(V=gxhorlPEm`{Fse+M2Dp zS{lFV;~6GD4@ame$%LNPS`axaf%~PeA?Uh1JP@cuZ$~}UmFGfZO&Oj)Xz8n|EKe>i zD!i2tAHT@d#PlD{lRjRa+wjcMQ)5GYurM}))TAJ2yMCBR4bM#zGLc z{-&|{`s!>^OL2%6+&4T07RDza-suEHx?Kis8#S;q-4C^51E|W3XxFx}|4GTlU(wsa zYPE&2{vnmScP``EjB6%_2AeKjxG?zr^Ji@lVrM!$CL(<0X2C&s={6{F-wow{dq8Np8iK7?Lu2ACC`t|OS2Pr6eX5b6^O=s|+(Q4uYljkz8KG}XPejd&vL9A_)#Fi^Lh0cU$2B)o>9QEQF1e#-hWcKm8Qx&1NOqY2#ZhJ>Fx@1GPgh5z@N!Y|Olyhhq?kK`| zWG9aA1GG0p-`oow%Ykdt4Dz;>9UID|hNR8~cWb^w;M^;=XJYbXCMaeNF`UMGE_O>ADz`4(XB&*%HPwuLdAY!e%&- zOUg?*@36rLj9K2s_DRIJ>Z5s{ZLmw!#q62PcdmI6f3}jilJ4ghJXXq^=d~SkP5@)y z&|fJXb4dh}Q61@EEY}r%0J5chmtDAjN|=OA9K^*ttUovo$m0@?qn~>M_rki_{Dx;= zIbfV4vJ(!7T4A0`{foFZzutndxf;oGY_qEBv1Yh$qVph?5|v3tbqqi1QY@To34^c* zldy?{xQLT<>4*Dj6LB9Sk8{7W6u*VLe+cS)!5`TS3%pukoK0&cKDR9xvy#I&@D_|) zwKLz$I+^E?{~S|B8TOwSI;cxFWP2d8g~1|B!X^%e3prn~BfoCeHjKkKVN5cQgL^(R zotj`nSQqX26n$l$tIxjVxdHbg956;($+5(}TiVAk$V5wkjkS$>l z7GV-L>g6$9$SLK==l=no!%)Fz){pyB{c?`&uv6Gtw$D44&Se{#Um9ofSMw9juT`~wf32$B`&1?w z)sYVAl8w}!un3c|84lzkPH8-T$7zarNz5A8 z!d^wEg3cLc`HuS|nse5=7VKV!iOM8Xbj6pWL4XB#FpPgado+Y;2+xX`P$ zlMPgpUSe4rxh-}XK-XdxC^eaxsre1d1vC<~Za-(N4taQcXQUn`4OF+>&l}~k~L%L)` zwuC`ggh|-ML0oh_^v649`WC)FOL0GlSu0^1@bI}PVQk+OR-e zh5XF2l{g=)^{#@oo@KDotqe%E__otbmdxZ~q)RqrOBjSjT!c*=xKH$#Yf;=Ee2aa` zW|;g%%lGDAvmRVGP0&g@A!k+!^48@b&o9JrFCXM_--=`tG?Pw9?}=>WFIv4v7=$I2 zziy6ki@6-*(iAqozJ9tzNgpLDlZ@(K7ys|r<67dgvDX7%9&p6Fzt{9)Q9L)o&{e9Ad&G%Rw1K4zV)Ok5Lkgg)GPPQU4(fK`6^m zC`O57T_~eeJ$mGb>ak;)6uZb~SnT(z!{QPW?j_(aSw*Ez;_!~7b0POJEaUfYZ@3Lx>Qb?KV{nUr5|!nDasSJ;rm3;f=$dOw-(KNS^}{0H zcFDSkURWR53yXa_B)c6`_VbMm&Ut%!oGL4?_}zPoZnE@kTxXmx54f)D7R`2u*zO@RlIUEl?CcKSvz>yL-WJnMs!BW^s1bmTeJ1zQ2~>nQ%YD0}#r`bugY) zt|}{fT2fY?CW;W=#x?(pf}<@?}yl!M>gavdBu2m|+!K4KdcDar9G8y2|WH*!EWR43>{W5gz0$8Lq@ z977-(*1;rxr37`AxoTpJp1t_tdB&Zx{2Iv$9|o#Ws*@H>5wkjkS%_be2jC4 z7S3TeMy)#c_Il5 auth(String login, String password); + Future auth(); Future getProfileData(String token); Future> getAnnounces(String token); Future> getEmployees(String token, String name); @@ -19,43 +20,37 @@ abstract class UserRemoteData { } class UserRemoteDataImpl implements UserRemoteData { - static const _apiUrl = 'https://lk.mirea.ru/local/ajax/mrest.php'; + static const _apiUrl = 'https://lks.mirea.ninja/api/'; final Dio httpClient; + final LksOauth2 lksOauth2; - UserRemoteDataImpl({required this.httpClient}); + UserRemoteDataImpl({required this.httpClient, required this.lksOauth2}); - @override - Future auth(String login, String password) async { - final data = {"action": "login", "login": login, "password": password}; - final response = await httpClient.get(_apiUrl, queryParameters: data); - if (response.statusCode == 200) { - var jsonResponse = json.decode(response.data); - log('Response: $jsonResponse'); - if (jsonResponse.containsKey('errors')) { - throw ServerException(jsonResponse['errors'][0]); - } - return jsonResponse['token']; + Future auth() async { + final token = await lksOauth2.oauth2Helper.getToken(); + + if (token != null) { + return token.accessToken!; } else { - throw ServerException('Response status code is $response.statusCode'); + throw ServerException('Токен не получен'); } } @override Future getProfileData(String token) async { - final response = await httpClient.get( + final response = await lksOauth2.oauth2Helper.get( '$_apiUrl?action=getData&url=https://lk.mirea.ru/profile/', - options: Options( - headers: {'Authorization': token}, - ), ); - var jsonResponse = json.decode(response.data); - log('Response: $jsonResponse'); + var jsonResponse = json.decode(response.body); + + log('Status code: ${response.statusCode}, Response: ${response.body}'); + if (jsonResponse.containsKey('errors')) { throw ServerException(jsonResponse['errors'][0]); } if (response.statusCode == 200) { - return UserModel.fromRawJson(response.data); + return UserModel.fromRawJson(response.body); } else { throw ServerException('Response status code is $response.statusCode'); } @@ -63,14 +58,13 @@ class UserRemoteDataImpl implements UserRemoteData { @override Future> getAnnounces(String token) async { - final response = await httpClient.get( + final response = await lksOauth2.oauth2Helper.get( '$_apiUrl?action=getData&url=https://lk.mirea.ru/livestream/', - options: Options( - headers: {'Authorization': token}, - ), ); - var jsonResponse = json.decode(response.data); + log('Status code: ${response.statusCode}, Response: ${response.body}'); + + var jsonResponse = json.decode(response.body); if (jsonResponse.containsKey('errors')) { throw ServerException(jsonResponse['errors'][0]); } @@ -88,14 +82,13 @@ class UserRemoteDataImpl implements UserRemoteData { @override Future> getEmployees(String token, String name) async { - final response = await httpClient.get( + final response = await lksOauth2.oauth2Helper.get( '$_apiUrl?action=getData&url=https://lk.mirea.ru/lectors/&page=undefined&findname=$name', - options: Options( - headers: {'Authorization': token}, - ), ); - var jsonResponse = json.decode(response.data); + log('Status code: ${response.statusCode}, Response: ${response.body}'); + + var jsonResponse = json.decode(response.body); if (jsonResponse.containsKey('errors')) { throw ServerException(jsonResponse['errors'][0]); } @@ -115,14 +108,13 @@ class UserRemoteDataImpl implements UserRemoteData { @override Future>> getScores(String token) async { - final response = await httpClient.get( + final response = await lksOauth2.oauth2Helper.get( '$_apiUrl?action=getData&url=https://lk.mirea.ru/learning/scores/', - options: Options( - headers: {'Authorization': token}, - ), ); - var jsonResponse = json.decode(response.data); + log('Status code: ${response.statusCode}, Response: ${response.body}'); + + var jsonResponse = json.decode(response.body); if (jsonResponse.containsKey('errors')) { throw ServerException(jsonResponse['errors'][0]); } @@ -148,14 +140,13 @@ class UserRemoteDataImpl implements UserRemoteData { @override Future> getAttendance( String token, String dateStart, String dateEnd) async { - final response = await httpClient.get( + final response = await lksOauth2.oauth2Helper.get( '$_apiUrl?action=getData&url=https://lk.mirea.ru/schedule/attendance/&startDate=$dateStart&endDate=$dateEnd', - options: Options( - headers: {'Authorization': token}, - ), ); - var jsonResponse = json.decode(response.data); + log('Status code: ${response.statusCode}, Response: ${response.body}'); + + var jsonResponse = json.decode(response.body); if (jsonResponse.containsKey('errors')) { throw ServerException(jsonResponse['errors'][0]); } diff --git a/lib/data/models/employee_model.dart b/lib/data/models/employee_model.dart index 14e2e575..82c55e35 100644 --- a/lib/data/models/employee_model.dart +++ b/lib/data/models/employee_model.dart @@ -8,14 +8,14 @@ class EmployeeModel extends Employee { required secondName, required lastName, required email, - required post, + required institute, required department, }) : super( name: name, secondName: secondName, lastName: lastName, email: email, - post: post, + institute: institute, department: department, ); @@ -28,8 +28,8 @@ class EmployeeModel extends Employee { secondName: json["HUMAN"]["SECOND_NAME"], lastName: json["HUMAN"]["LAST_NAME"], email: json["HUMAN"]["EMAIL"], - post: json["EMPLOYEE"]["PROPERTIES"]["POST"]["VALUE"], - department: json["EMPLOYEE"]["PROPERTIES"]["DEPARTMENT"]["VALUE"], + institute: json["HUMAN"]["INSTITUTE"], + department: json["HUMAN"]["DEPARTMENT"], ); } } diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index 5928ab0b..6168e79f 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -24,7 +24,7 @@ class UserRepositoryImpl implements UserRepository { Future> logIn(String login, String password) async { if (await connectionChecker.hasConnection) { try { - final authToken = await remoteDataSource.auth(login, password); + final authToken = await remoteDataSource.auth(); localDataSource.setTokenToCache(authToken); return Right(authToken); } catch (e) { diff --git a/lib/domain/entities/employee.dart b/lib/domain/entities/employee.dart index e0ea957b..a8e4b64a 100644 --- a/lib/domain/entities/employee.dart +++ b/lib/domain/entities/employee.dart @@ -5,7 +5,7 @@ class Employee extends Equatable { final String secondName; final String lastName; final String email; - final String post; + final String institute; final String department; const Employee({ @@ -13,7 +13,7 @@ class Employee extends Equatable { required this.secondName, required this.lastName, required this.email, - required this.post, + required this.institute, required this.department, }); @@ -23,7 +23,7 @@ class Employee extends Equatable { secondName, lastName, email, - post, + institute, department, ]; } diff --git a/lib/main.dart b/lib/main.dart index 381b23f6..fb4da216 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:rtu_mirea_app/common/widget_data_init.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:rtu_mirea_app/presentation/bloc/about_app_bloc/about_app_bloc.dart'; @@ -30,6 +31,7 @@ import 'service_locator.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'firebase_options.dart'; +import 'package:shared_preferences/shared_preferences.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -50,8 +52,12 @@ Future main() async { await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false); // Clear local dota - // var prefs = getIt(); - // await prefs.clear(); + var prefs = getIt(); + await prefs.clear(); + + // Clear oauth tokens + var lksOauth2 = getIt(); + await lksOauth2.oauth2Helper.removeAllTokens(); } setPathUrlStrategy(); diff --git a/lib/presentation/pages/profile/profile_detail_page.dart b/lib/presentation/pages/profile/profile_detail_page.dart index 8208f5cd..c0a35d50 100644 --- a/lib/presentation/pages/profile/profile_detail_page.dart +++ b/lib/presentation/pages/profile/profile_detail_page.dart @@ -86,15 +86,16 @@ class ProfileDetailPage extends StatelessWidget { ]), ), const SizedBox(height: 20), - if (user.authShortlink != null) - CopyTextBlockWithLabel( - label: "Ссылка авторизации", - text: 'https://lk.mirea.ru/auth/link/?url=${user.authShortlink!}'), + // if (user.authShortlink != null) + // CopyTextBlockWithLabel( + // label: "Ссылка авторизации", + // text: + // 'https://lk.mirea.ru/auth/link/?url=${user.authShortlink!}'), const SizedBox(height: 23), CopyTextBlockWithLabel(label: "Логин", text: user.login), const SizedBox(height: 23), CopyTextBlockWithLabel( - label: "Персональный email", text: user.email), + label: "Персональный email", text: 'dmitriev@mirea.ru'), const SizedBox(height: 23), CopyTextBlockWithLabel( label: "Дата рождения", text: user.birthday), diff --git a/lib/presentation/pages/profile/profile_page.dart b/lib/presentation/pages/profile/profile_page.dart index be3d85d8..d3ba897e 100644 --- a/lib/presentation/pages/profile/profile_page.dart +++ b/lib/presentation/pages/profile/profile_page.dart @@ -4,10 +4,17 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/colorful_button.dart'; +import 'package:rtu_mirea_app/presentation/widgets/buttons/icon_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/settings_button.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; +import '../../bloc/announces_bloc/announces_bloc.dart'; +import '../../bloc/profile_bloc/profile_bloc.dart'; +import '../../theme.dart'; +import '../../widgets/buttons/text_outlined_button.dart'; +import '../../widgets/container_label.dart'; import 'widgets/bottom_error_info.dart'; +import 'package:url_launcher/url_launcher.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({Key? key}) : super(key: key); @@ -34,8 +41,8 @@ class _ProfilePageState extends State { padding: const EdgeInsets.symmetric(horizontal: 24), child: BlocBuilder( builder: (context, state) { - return const _InitialProfileStatePage(); - /* + // return const _InitialProfileStatePage(); + if (state is LogInSuccess) { context .read() @@ -49,19 +56,16 @@ class _ProfilePageState extends State { CircleAvatar( radius: 68, backgroundImage: Image.network( - 'https://lk.mirea.ru' + - profileState.user.photoUrl) + 'https://lk.mirea.ru${profileState.user.photoUrl}') .image, ), Padding( + padding: + const EdgeInsets.only(top: 13, bottom: 4), child: Text( - profileState.user.name + - ' ' + - profileState.user.lastName, + '${profileState.user.name} ${profileState.user.lastName}', style: DarkTextTheme.h5, ), - padding: - const EdgeInsets.only(top: 13, bottom: 4), ), ShaderMask( shaderCallback: (bounds) => @@ -75,12 +79,25 @@ class _ProfilePageState extends State { ), ), const SizedBox(height: 12), - TextOutlinedButton( - content: "Просмотр профиля", + Row(children: [ + TextOutlinedButton( + content: "Профиль", width: 200, onPressed: () => context.router.push( - ProfileDetailRoute( - user: profileState.user))), + ProfileDetailRoute(user: profileState.user), + ), + ), + const SizedBox(width: 12), + SocialIconButton( + assetImage: + const AssetImage('assets/icons/gerb.ico'), + onClick: () { + launchUrl(Uri.parse( + profileState.user.authShortlink ?? + "https://lk.mirea.ru/auth")); + }, + ), + ]), const SizedBox(height: 40), const ContainerLabel(label: "Информация"), const SizedBox(height: 20), @@ -91,8 +108,9 @@ class _ProfilePageState extends State { context .read() .add(LoadAnnounces(token: state.token)); - context.router - .push(const ProfileAnnouncesRoute()); + context.router.push( + const ProfileAnnouncesRoute(), + ); }), // const SizedBox(height: 8), // SettingsButton( @@ -145,7 +163,7 @@ class _ProfilePageState extends State { }); } else if (state is LogInError || state is AuthUnauthorized) { - return _InitialProfileStatePage(); + return const _InitialProfileStatePage(); } else if (state is AuthUnknown) { return ConstrainedBox( constraints: BoxConstraints( @@ -155,7 +173,6 @@ class _ProfilePageState extends State { ); } return Container(); - */ }, ), ), @@ -177,13 +194,23 @@ class _InitialProfileStatePage extends StatelessWidget { ColorfulButton( text: 'Войти', onClick: () { + // Мы используем oauth2 для авторизации, поэтому + // вместо того, чтобы открывать страницу с логином и паролем, + // мы просто вызываем событие авторизации, которое откроет + // страницу авторизации в браузере. + context.read().add( + const AuthLogInEvent(login: 'login', password: 'password')); + + // Страница с вводом логина и пароля: // context.router.push(const LoginRoute()); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => const BottomErrorInfo(), - ); + + // Страница с ошибкой: + // showModalBottomSheet( + // context: context, + // isScrollControlled: true, + // backgroundColor: Colors.transparent, + // builder: (context) => const BottomErrorInfo(), + // ); }, backgroundColor: DarkThemeColors.colorful03), const SizedBox(height: 8), diff --git a/lib/presentation/pages/profile/profile_scores_page.dart b/lib/presentation/pages/profile/profile_scores_page.dart index 6dd27a27..25aaabcd 100644 --- a/lib/presentation/pages/profile/profile_scores_page.dart +++ b/lib/presentation/pages/profile/profile_scores_page.dart @@ -118,19 +118,28 @@ class _ProfileScoresPageState extends State { }, itemCount: state.scores.keys.length), ), + // Небольшая кнопка с иконкой для открытия модального окна с графиком Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, horizontal: 16), - child: TextOutlinedButton( - content: 'Средний балл', - width: 230, - onPressed: () { - showModalBottomSheet( - context: context, - builder: (BuildContext context) => - ScoresChartModal(scores: state.scores)); - }), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Всего предметов: ${state.scores[state.selectedSemester]!.length}", + style: DarkTextTheme.body, + ), + IconButton( + onPressed: () => showModalBottomSheet( + context: context, + builder: (BuildContext context) => + ScoresChartModal( + scores: state.scores), + ), + icon: const Icon(Icons.bar_chart)) + ], + ), ), + Expanded( child: _buildDataGridForMobile(_ScoresDataGridSource( state.scores[state.selectedSemester]!)), diff --git a/lib/presentation/pages/profile/widgets/lector_search_card.dart b/lib/presentation/pages/profile/widgets/lector_search_card.dart index b72bac02..a94d574f 100644 --- a/lib/presentation/pages/profile/widgets/lector_search_card.dart +++ b/lib/presentation/pages/profile/widgets/lector_search_card.dart @@ -46,9 +46,9 @@ class LectorSearchCard extends StatelessWidget { ), ), ), - // const SizedBox(height: 8), - // Text(employee.post, style: DarkTextTheme.bodyRegular), - // Text(employee.department, style: DarkTextTheme.bodyRegular), + const SizedBox(height: 8), + Text(employee.institute, style: DarkTextTheme.bodyRegular), + Text(employee.department, style: DarkTextTheme.bodyRegular), ], ), ); diff --git a/lib/service_locator.dart b/lib/service_locator.dart index bbc4e10d..c76aa340 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:rtu_mirea_app/data/datasources/app_settings_local.dart'; import 'package:rtu_mirea_app/data/datasources/forum_local.dart'; import 'package:rtu_mirea_app/data/datasources/forum_remote.dart'; @@ -186,7 +187,7 @@ Future setup() async { getIt.registerLazySingleton( () => UserLocalDataImpl(sharedPreferences: getIt())); getIt.registerLazySingleton( - () => UserRemoteDataImpl(httpClient: getIt())); + () => UserRemoteDataImpl(httpClient: getIt(), lksOauth2: getIt())); getIt.registerLazySingleton( () => ScheduleRemoteDataImpl(httpClient: getIt())); getIt.registerLazySingleton( @@ -217,4 +218,5 @@ Future setup() async { getIt.registerLazySingleton(() => InternetConnectionChecker()); final PackageInfo packageInfo = await PackageInfo.fromPlatform(); getIt.registerLazySingleton(() => packageInfo); + getIt.registerLazySingleton(() => LksOauth2()); } diff --git a/pubspec.yaml b/pubspec.yaml index c70d8ad1..fa264378 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -165,9 +165,16 @@ dependencies: home_widget: ^0.1.6 freezed_annotation: ^2.1.0 - firebase_core: ^1.12.0 - firebase_analytics: ^9.1.0 - firebase_crashlytics: ^2.5.1 + + firebase_core: ^2.4.1 + firebase_analytics: ^10.1.0 + firebase_crashlytics: ^3.0.11 + + oauth2_client: ^3.0.0 + + # Flutter Secure Storage provides API to store data in secure storage. + # See https://pub.dev/packages/flutter_secure_storage + flutter_secure_storage: ^6.0.0 dev_dependencies: From d7bd5120acdd7cd08e7bda845c4d22beeac66cb3 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Mon, 23 Jan 2023 23:31:35 +0300 Subject: [PATCH 10/38] fix: Update auth link and personal email (#258) --- lib/presentation/pages/profile/profile_detail_page.dart | 2 +- lib/presentation/pages/profile/profile_page.dart | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/presentation/pages/profile/profile_detail_page.dart b/lib/presentation/pages/profile/profile_detail_page.dart index c0a35d50..77adbb86 100644 --- a/lib/presentation/pages/profile/profile_detail_page.dart +++ b/lib/presentation/pages/profile/profile_detail_page.dart @@ -95,7 +95,7 @@ class ProfileDetailPage extends StatelessWidget { CopyTextBlockWithLabel(label: "Логин", text: user.login), const SizedBox(height: 23), CopyTextBlockWithLabel( - label: "Персональный email", text: 'dmitriev@mirea.ru'), + label: "Персональный email", text: user.email), const SizedBox(height: 23), CopyTextBlockWithLabel( label: "Дата рождения", text: user.birthday), diff --git a/lib/presentation/pages/profile/profile_page.dart b/lib/presentation/pages/profile/profile_page.dart index d3ba897e..2a670a5b 100644 --- a/lib/presentation/pages/profile/profile_page.dart +++ b/lib/presentation/pages/profile/profile_page.dart @@ -93,8 +93,10 @@ class _ProfilePageState extends State { const AssetImage('assets/icons/gerb.ico'), onClick: () { launchUrl(Uri.parse( - profileState.user.authShortlink ?? - "https://lk.mirea.ru/auth")); + profileState.user.authShortlink != null + ? "https://lk.mirea.ru/auth/link/?url=${profileState.user.authShortlink!}" + : "https://lk.mirea.ru/auth", + )); }, ), ]), From 0bc9c59aba4beec637da5b686712d697f445fbbd Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Mon, 23 Jan 2023 23:48:33 +0300 Subject: [PATCH 11/38] ui: Add Bottom Navigation Bar Background (#259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ui: Add Bottom Navigation Bar Background У нижней навигационной панели не было цвета, что хорошо заметно на страницах, у которых фон отличается от стандартного. Например, на странице карты навигационная панель просвечивалась. * dev: Remove unused imports --- lib/data/datasources/user_remote.dart | 1 + lib/presentation/pages/home_page.dart | 61 ++++++++++--------- .../pages/profile/profile_page.dart | 1 - .../pages/profile/profile_scores_page.dart | 1 - 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/lib/data/datasources/user_remote.dart b/lib/data/datasources/user_remote.dart index 1086bc5a..f449859e 100644 --- a/lib/data/datasources/user_remote.dart +++ b/lib/data/datasources/user_remote.dart @@ -27,6 +27,7 @@ class UserRemoteDataImpl implements UserRemoteData { UserRemoteDataImpl({required this.httpClient, required this.lksOauth2}); + @override Future auth() async { final token = await lksOauth2.oauth2Helper.getToken(); diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index 1554159b..aa4f74a9 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -2,10 +2,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/app_cubit/app_cubit.dart'; -import 'package:rtu_mirea_app/presentation/bloc/update_info_bloc/update_info_bloc.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; -import 'package:rtu_mirea_app/presentation/widgets/update_info_modal.dart'; import 'package:salomon_bottom_bar/salomon_bottom_bar.dart'; class HomePage extends StatelessWidget { @@ -57,35 +55,38 @@ class AppBottomNavigationBar extends StatelessWidget { @override Widget build(BuildContext context) { - return SalomonBottomBar( - margin: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 10, - ), - currentIndex: index, - onTap: onClick, - items: [ - SalomonBottomBarItem( - icon: const Icon(Icons.library_books_rounded), - title: const Text("Новости"), - selectedColor: DarkThemeColors.primary, - ), - SalomonBottomBarItem( - icon: const Icon(Icons.calendar_today_rounded), - title: const Text("Расписание"), - selectedColor: DarkThemeColors.primary, - ), - SalomonBottomBarItem( - icon: const Icon(Icons.map_rounded), - title: const Text("Карта"), - selectedColor: DarkThemeColors.primary, + return Container( + color: DarkThemeColors.background01, + child: SalomonBottomBar( + margin: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, ), - SalomonBottomBarItem( - icon: const Icon(Icons.person), - title: const Text("Профиль"), - selectedColor: DarkThemeColors.primary, - ), - ], + currentIndex: index, + onTap: onClick, + items: [ + SalomonBottomBarItem( + icon: const Icon(Icons.library_books_rounded), + title: const Text("Новости"), + selectedColor: DarkThemeColors.primary, + ), + SalomonBottomBarItem( + icon: const Icon(Icons.calendar_today_rounded), + title: const Text("Расписание"), + selectedColor: DarkThemeColors.primary, + ), + SalomonBottomBarItem( + icon: const Icon(Icons.map_rounded), + title: const Text("Карта"), + selectedColor: DarkThemeColors.primary, + ), + SalomonBottomBarItem( + icon: const Icon(Icons.person), + title: const Text("Профиль"), + selectedColor: DarkThemeColors.primary, + ), + ], + ), ); } } diff --git a/lib/presentation/pages/profile/profile_page.dart b/lib/presentation/pages/profile/profile_page.dart index 2a670a5b..27b5d823 100644 --- a/lib/presentation/pages/profile/profile_page.dart +++ b/lib/presentation/pages/profile/profile_page.dart @@ -13,7 +13,6 @@ import '../../bloc/profile_bloc/profile_bloc.dart'; import '../../theme.dart'; import '../../widgets/buttons/text_outlined_button.dart'; import '../../widgets/container_label.dart'; -import 'widgets/bottom_error_info.dart'; import 'package:url_launcher/url_launcher.dart'; class ProfilePage extends StatefulWidget { diff --git a/lib/presentation/pages/profile/profile_scores_page.dart b/lib/presentation/pages/profile/profile_scores_page.dart index 25aaabcd..0fee5b18 100644 --- a/lib/presentation/pages/profile/profile_scores_page.dart +++ b/lib/presentation/pages/profile/profile_scores_page.dart @@ -6,7 +6,6 @@ import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/widgets/scores_chart_modal.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; -import 'package:rtu_mirea_app/presentation/widgets/buttons/text_outlined_button.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_tab_button.dart'; From 2326881d5d870540edf30c005753cd1026baf760 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 24 Jan 2023 14:04:19 +0300 Subject: [PATCH 12/38] feature: redirect to app (#260) --- ios/Runner/Info.plist | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d4b8ba74..c3d82104 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,11 +2,6 @@ - NSAppTransportSecurity - - NSAllowsLocalNetworking - - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -25,10 +20,26 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLSchemes + + ninja.mirea.mireaapp://oauth2redirect + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsLocalNetworking + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile From 8d48f4a4ec0de375b6aa19ed729c8d44ce68f0d3 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:12:31 +0300 Subject: [PATCH 13/38] feature: Add firebase options (#261) --- android/app/google-services.json | 46 ++++++++++++++++++++++++++++++++ lib/firebase_options.dart | 41 +++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 android/app/google-services.json diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 00000000..9a9c53fb --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,46 @@ +{ + "project_info": { + "project_number": "510978291920", + "project_id": "rtu-mirea-app", + "storage_bucket": "rtu-mirea-app.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:510978291920:android:0c6dc2379d80661b8c46d5", + "android_client_info": { + "package_name": "ninja.mirea.mireaapp" + } + }, + "oauth_client": [ + { + "client_id": "510978291920-l17hjoc3ltfj74e73bj73phfivhusppu.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCTfdp1rBC6PkVjwOdZ5XM7_zzwoN0BVPM" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "510978291920-l17hjoc3ltfj74e73bj73phfivhusppu.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "510978291920-31sgk97k4bifhc0ebpamk9m46om2e50r.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "mirea.ninja.mireaapp" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 1e06cd59..d4d71cf9 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -22,9 +22,48 @@ class DefaultFirebaseOptions { 'you can reconfigure this by running the FlutterFire CLI again.', ); } + // ignore: missing_enum_constant_in_switch + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + } throw UnsupportedError( - 'The configuration file is not assembled. Generate the file using FlutterFire.', + 'DefaultFirebaseOptions are not supported for this platform.', ); } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCTfdp1rBC6PkVjwOdZ5XM7_zzwoN0BVPM', + appId: '1:510978291920:android:0c6dc2379d80661b8c46d5', + messagingSenderId: '510978291920', + projectId: 'rtu-mirea-app', + storageBucket: 'rtu-mirea-app.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyAYZ5JlWF94jBGrcds7fSi5uMN1zmuieec', + appId: '1:510978291920:ios:dd9496a1680c72828c46d5', + messagingSenderId: '510978291920', + projectId: 'rtu-mirea-app', + storageBucket: 'rtu-mirea-app.appspot.com', + iosClientId: + '510978291920-31sgk97k4bifhc0ebpamk9m46om2e50r.apps.googleusercontent.com', + iosBundleId: 'mirea.ninja.mireaapp', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyAYZ5JlWF94jBGrcds7fSi5uMN1zmuieec', + appId: '1:510978291920:ios:dd9496a1680c72828c46d5', + messagingSenderId: '510978291920', + projectId: 'rtu-mirea-app', + storageBucket: 'rtu-mirea-app.appspot.com', + iosClientId: + '510978291920-31sgk97k4bifhc0ebpamk9m46om2e50r.apps.googleusercontent.com', + iosBundleId: 'mirea.ninja.mireaapp', + ); } From 2d9cccc60729b4e238911245f7391b9934ee6ac6 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:17:33 +0300 Subject: [PATCH 14/38] fix: Update oauth redirect scheme (#262) --- lib/common/oauth.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/common/oauth.dart b/lib/common/oauth.dart index 2372f938..c23f5028 100644 --- a/lib/common/oauth.dart +++ b/lib/common/oauth.dart @@ -17,14 +17,13 @@ class LksOauth2 { late final OAuth2Helper oauth2Helper; late final MireaNinjaOauth2Client oauth2Client; - // Конструктор LksOauth2({ String? redirectUri, String? customUriScheme, }) { oauth2Client = MireaNinjaOauth2Client( customUriScheme: customUriScheme ?? 'ninja.mirea.mireaapp', - redirectUri: redirectUri ?? 'ninja.mirea.mireaapp:/oauth2redirect', + redirectUri: redirectUri ?? 'ninja.mirea.mireaapp://oauth2redirect', ); oauth2Helper = OAuth2Helper( From 78946fccd1c833732b2bf186f5ec6f6d04b47604 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:35:50 +0300 Subject: [PATCH 15/38] ui: New type of scores view and shorten names (#263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новый списочный вид (с карточками) страницы оценок, добавлена кнопка переключения между видами. Сокращены названия оценок. --- lib/domain/entities/score.dart | 24 ++ .../pages/profile/profile_scores_page.dart | 302 +++++++++++++----- 2 files changed, 250 insertions(+), 76 deletions(-) diff --git a/lib/domain/entities/score.dart b/lib/domain/entities/score.dart index ebdd3252..7ef85739 100644 --- a/lib/domain/entities/score.dart +++ b/lib/domain/entities/score.dart @@ -31,4 +31,28 @@ class Score extends Equatable { date, year, ]; + + Score copyWith({ + String? subjectName, + String? result, + String? type, + String? comission, + String? courseWork, + String? exam, + String? credit, + String? date, + String? year, + }) { + return Score( + subjectName: subjectName ?? this.subjectName, + result: result ?? this.result, + type: type ?? this.type, + comission: comission ?? this.comission, + courseWork: courseWork ?? this.courseWork, + exam: exam ?? this.exam, + credit: credit ?? this.credit, + date: date ?? this.date, + year: year ?? this.year, + ); + } } diff --git a/lib/presentation/pages/profile/profile_scores_page.dart b/lib/presentation/pages/profile/profile_scores_page.dart index 0fee5b18..9a5b0f89 100644 --- a/lib/presentation/pages/profile/profile_scores_page.dart +++ b/lib/presentation/pages/profile/profile_scores_page.dart @@ -9,6 +9,28 @@ import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_tab_button.dart'; +Color getColorByResult(String result) { + result = result.toLowerCase(); + + if (result.contains("неуваж")) { + return DarkThemeColors.colorful07; + } + + if (result.contains("зач")) { + return Colors.green; + } else if (result.contains("отл")) { + return Colors.green; + } else if (result.contains("хор")) { + return DarkThemeColors.colorful05; + } else if (result.contains("удовл")) { + return DarkThemeColors.colorful06; + } else { + return Colors.white; + } +} + +enum ViewType { table, cards } + class ProfileScoresPage extends StatefulWidget { const ProfileScoresPage({Key? key}) : super(key: key); @@ -19,52 +41,7 @@ class ProfileScoresPage extends StatefulWidget { class _ProfileScoresPageState extends State { final ValueNotifier _tabValueNotifier = ValueNotifier(0); - SfDataGrid _buildDataGridForMobile(_ScoresDataGridSource dateGridSource) { - return SfDataGrid( - source: dateGridSource, - columnWidthMode: ColumnWidthMode.auto, - rowHeight: 80, - columnWidthCalculationRange: ColumnWidthCalculationRange.allRows, - columns: [ - GridColumn( - columnName: 'date', - width: 90, - label: const Center(child: Text('Дата')), - ), - GridColumn( - columnWidthMode: ColumnWidthMode.fitByColumnName, - columnName: 'name', - width: 180, - label: Container( - alignment: Alignment.centerLeft, - child: const Text('Название'), - ), - ), - GridColumn( - maximumWidth: 180, - columnName: 'score', - columnWidthMode: ColumnWidthMode.fitByCellValue, - label: const Center( - child: Text('Оценка'), - ), - ), - GridColumn( - columnWidthMode: ColumnWidthMode.fitByCellValue, - columnName: 'type', - label: const Center( - child: Text('Тип'), - )), - GridColumn( - columnWidthMode: ColumnWidthMode.fitByCellValue, - columnName: 'teacher', - label: Container( - alignment: Alignment.centerLeft, - child: const Text('Преподаватель'), - ), - ), - ], - ); - } + ViewType _viewType = ViewType.cards; @override Widget build(BuildContext context) { @@ -105,15 +82,16 @@ class _ProfileScoresPageState extends State { final String semester = state.scores.keys.toList()[index]; return PrimaryTabButton( - text: '$semester семестр', - itemIndex: index, - notifier: _tabValueNotifier, - onClick: () { - _tabValueNotifier.value = index; - context.read().add( - ChangeSelectedScoresSemester( - semester: semester)); - }); + text: '$semester семестр', + itemIndex: index, + notifier: _tabValueNotifier, + onClick: () { + _tabValueNotifier.value = index; + context.read().add( + ChangeSelectedScoresSemester( + semester: semester)); + }, + ); }, itemCount: state.scores.keys.length), ), @@ -127,21 +105,40 @@ class _ProfileScoresPageState extends State { "Всего предметов: ${state.scores[state.selectedSemester]!.length}", style: DarkTextTheme.body, ), - IconButton( + Row(children: [ + IconButton( onPressed: () => showModalBottomSheet( - context: context, - builder: (BuildContext context) => - ScoresChartModal( - scores: state.scores), - ), - icon: const Icon(Icons.bar_chart)) + context: context, + builder: (BuildContext context) => + ScoresChartModal(scores: state.scores), + ), + icon: const Icon(Icons.bar_chart), + ), + IconButton( + onPressed: () { + setState(() { + _viewType = _viewType == ViewType.table + ? ViewType.cards + : ViewType.table; + }); + }, + icon: Icon(_viewType == ViewType.cards + ? Icons.view_list + : Icons.view_module), + ), + ]), ], ), ), Expanded( - child: _buildDataGridForMobile(_ScoresDataGridSource( - state.scores[state.selectedSemester]!)), + child: _viewType == ViewType.table + ? _ScoresDataGrid( + dataGridSource: _ScoresDataGridSource( + state.scores[state.selectedSemester]!)) + : _ScoresCardListView( + scores: + state.scores[state.selectedSemester]!), ), ], ); @@ -166,11 +163,167 @@ class _ProfileScoresPageState extends State { } } +class _ScoresCardListView extends StatelessWidget { + const _ScoresCardListView({Key? key, required this.scores}) : super(key: key); + + final List scores; + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemBuilder: (context, index) { + final Score score = scores[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Card( + color: DarkThemeColors.background02, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 13), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + score.type, + style: DarkTextTheme.body.copyWith( + color: score.type.toLowerCase() == "экзамен" + ? DarkThemeColors.colorful04 + : DarkThemeColors.colorful02), + ), + const SizedBox(height: 9), + Text( + score.subjectName, + style: DarkTextTheme.headline, + ), + const Divider( + color: DarkThemeColors.deactiveDarker, + thickness: 1, + height: 30, + ), + Row( + children: [ + Icon( + Icons.star_border_outlined, + color: getColorByResult(score.result), + ), + const SizedBox(width: 10), + Text( + score.result, + style: DarkTextTheme.body + .copyWith(color: getColorByResult(score.result)), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon( + Icons.person_sharp, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + score.comission ?? "", + style: DarkTextTheme.body, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon( + Icons.calendar_month, + ), + const SizedBox(width: 10), + Text(score.year, style: DarkTextTheme.body), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon( + Icons.calendar_month, + ), + const SizedBox(width: 10), + Text(score.date, style: DarkTextTheme.body), + ], + ), + ], + ), + ), + ), + ); + }, + itemCount: scores.length, + ); + } +} + +class _ScoresDataGrid extends StatelessWidget { + const _ScoresDataGrid({Key? key, required this.dataGridSource}) + : super(key: key); + + final _ScoresDataGridSource dataGridSource; + + @override + Widget build(BuildContext context) { + return SfDataGrid( + source: dataGridSource, + columnWidthMode: ColumnWidthMode.auto, + rowHeight: 80, + columnWidthCalculationRange: ColumnWidthCalculationRange.allRows, + columns: [ + GridColumn( + columnName: 'date', + width: 90, + label: const Center(child: Text('Дата')), + ), + GridColumn( + columnWidthMode: ColumnWidthMode.fitByColumnName, + columnName: 'name', + width: 180, + label: Container( + alignment: Alignment.centerLeft, + child: const Text('Название'), + ), + ), + GridColumn( + maximumWidth: 180, + columnName: 'score', + columnWidthMode: ColumnWidthMode.fitByCellValue, + label: const Center( + child: Text('Оценка'), + ), + ), + GridColumn( + columnWidthMode: ColumnWidthMode.fitByCellValue, + columnName: 'type', + label: const Center( + child: Text('Тип'), + )), + GridColumn( + columnWidthMode: ColumnWidthMode.fitByCellValue, + columnName: 'teacher', + label: Container( + alignment: Alignment.centerLeft, + child: const Text('Преподаватель'), + ), + ), + ], + ); + } +} + /// Set team's data collection to data grid source. class _ScoresDataGridSource extends DataGridSource { /// Creates the team data source class with required details. _ScoresDataGridSource(List scores) { - _scores = scores; + _scores = scores + .map((e) => e.copyWith(result: _shortenResult(e.result))) + .toList(); buildDataGridRows(); } @@ -192,21 +345,18 @@ class _ScoresDataGridSource extends DataGridSource { }).toList(); } - Color _getColorByResult(String result) { - if (result.contains("неуваж")) { - return DarkThemeColors.colorful07; - } - - switch (result.toLowerCase()) { + String _shortenResult(String score) { + switch (score.toLowerCase()) { case 'зачтено': + return "Зач."; case 'отлично': - return Colors.green; + return "Отл."; case 'хорошо': - return DarkThemeColors.colorful05; + return "Хор."; case 'удовлетворительно': - return DarkThemeColors.colorful06; + return "Удовл."; default: - return Colors.white; + return score; } } @@ -238,7 +388,7 @@ class _ScoresDataGridSource extends DataGridSource { child: Text( row.getCells()[2].value.toString(), style: TextStyle( - color: _getColorByResult(row.getCells()[2].value.toString())), + color: getColorByResult(row.getCells()[2].value.toString())), overflow: TextOverflow.ellipsis, ), ), From 0c9b1b39c5a62fe122404730999b9a8fc0e82445 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 24 Jan 2023 17:12:23 +0300 Subject: [PATCH 16/38] fix: Remove tokens when logging out (#264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Удаление токенов при выходе, чтобы повторный вход не провоцировал загрузку токенов доступа из локального хранилища. --- lib/data/datasources/user_remote.dart | 6 ++++ .../repositories/user_repository_impl.dart | 11 +++---- .../bloc/auth_bloc/auth_bloc.dart | 29 +++++++++---------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/data/datasources/user_remote.dart b/lib/data/datasources/user_remote.dart index f449859e..0539bcb9 100644 --- a/lib/data/datasources/user_remote.dart +++ b/lib/data/datasources/user_remote.dart @@ -11,6 +11,7 @@ import 'package:rtu_mirea_app/data/models/user_model.dart'; abstract class UserRemoteData { Future auth(); + Future logOut(); Future getProfileData(String token); Future> getAnnounces(String token); Future> getEmployees(String token, String name); @@ -38,6 +39,11 @@ class UserRemoteDataImpl implements UserRemoteData { } } + @override + Future logOut() async { + await lksOauth2.oauth2Helper.removeAllTokens(); + } + @override Future getProfileData(String token) async { final response = await lksOauth2.oauth2Helper.get( diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index 6168e79f..e43b6b6f 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -40,11 +40,12 @@ class UserRepositoryImpl implements UserRepository { @override Future> logOut() async { - try { - return Right(await localDataSource.removeTokenFromCache()); - } on CacheException { - return Future.value(const Left(CacheFailure())); - } + // try { + // return Right(await localDataSource.removeTokenFromCache()); + // } on CacheException { + // return Future.value(const Left(CacheFailure())); + // } + return Right(await remoteDataSource.logOut()); } @override diff --git a/lib/presentation/bloc/auth_bloc/auth_bloc.dart b/lib/presentation/bloc/auth_bloc/auth_bloc.dart index 5e1d567a..e620d90c 100644 --- a/lib/presentation/bloc/auth_bloc/auth_bloc.dart +++ b/lib/presentation/bloc/auth_bloc/auth_bloc.dart @@ -47,22 +47,19 @@ class AuthBloc extends Bloc { AuthLogInEvent event, Emitter emit, ) async { - if (event is AuthLogInEvent) { - if (event.login.length < 4 || event.password.length < 4) { - return emit( - const LogInError(cause: "Введёт неверный логин или пароль")); - } - - final res = await logIn(LogInParams(event.login, event.password)); - res.fold( - (failure) => emit(LogInError( - cause: failure.cause ?? - "Ошибка при авторизации. Повторите попытку позже")), - (res) { - emit(LogInSuccess(token: res)); - FirebaseAnalytics.instance.logLogin(); - }, - ); + if (event.login.length < 4 || event.password.length < 4) { + return emit(const LogInError(cause: "Введёт неверный логин или пароль")); } + + final res = await logIn(LogInParams(event.login, event.password)); + res.fold( + (failure) => emit(LogInError( + cause: failure.cause ?? + "Ошибка при авторизации. Повторите попытку позже")), + (res) { + emit(LogInSuccess(token: res)); + FirebaseAnalytics.instance.logLogin(); + }, + ); } } From 02d599216275945bb6bf6acb4f494b1890f88d84 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 24 Jan 2023 17:25:55 +0300 Subject: [PATCH 17/38] ui: Update lks login button text (#265) --- .../pages/profile/profile_page.dart | 52 +++++++++++-------- .../widgets/buttons/icon_button.dart | 30 ++++++++--- .../widgets/buttons/text_outlined_button.dart | 4 +- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/lib/presentation/pages/profile/profile_page.dart b/lib/presentation/pages/profile/profile_page.dart index 27b5d823..525a3275 100644 --- a/lib/presentation/pages/profile/profile_page.dart +++ b/lib/presentation/pages/profile/profile_page.dart @@ -78,27 +78,37 @@ class _ProfilePageState extends State { ), ), const SizedBox(height: 12), - Row(children: [ - TextOutlinedButton( - content: "Профиль", - width: 200, - onPressed: () => context.router.push( - ProfileDetailRoute(user: profileState.user), - ), - ), - const SizedBox(width: 12), - SocialIconButton( - assetImage: - const AssetImage('assets/icons/gerb.ico'), - onClick: () { - launchUrl(Uri.parse( - profileState.user.authShortlink != null - ? "https://lk.mirea.ru/auth/link/?url=${profileState.user.authShortlink!}" - : "https://lk.mirea.ru/auth", - )); - }, - ), - ]), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextOutlinedButton( + width: 160, + content: "Профиль", + onPressed: () => context.router.push( + ProfileDetailRoute( + user: profileState.user), + ), + ), + const SizedBox(width: 12), + SizedBox( + width: 146, + height: 45, + child: SocialIconButton( + assetImage: const AssetImage( + 'assets/icons/gerb.ico'), + onClick: () { + launchUrl(Uri.parse( + profileState.user.authShortlink != + null + ? "https://lk.mirea.ru/auth/link/?url=${profileState.user.authShortlink!}" + : "https://lk.mirea.ru/auth", + )); + }, + text: "Вход в ЛКС", + ), + ), + ]), + const SizedBox(height: 40), const ContainerLabel(label: "Информация"), const SizedBox(height: 20), diff --git a/lib/presentation/widgets/buttons/icon_button.dart b/lib/presentation/widgets/buttons/icon_button.dart index 72afda10..c1404b93 100644 --- a/lib/presentation/widgets/buttons/icon_button.dart +++ b/lib/presentation/widgets/buttons/icon_button.dart @@ -2,11 +2,15 @@ import 'package:flutter/material.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; class SocialIconButton extends StatelessWidget { - const SocialIconButton( - {Key? key, required this.assetImage, required this.onClick}) - : super(key: key); + const SocialIconButton({ + Key? key, + required this.assetImage, + required this.onClick, + this.text, + }) : super(key: key); final AssetImage assetImage; final Function onClick; + final String? text; @override Widget build(BuildContext context) { @@ -24,10 +28,22 @@ class SocialIconButton extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16), - child: Image( - image: assetImage, - height: 16.0, - ), + child: text != null + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(text!), + const SizedBox(width: 8), + Image( + image: assetImage, + height: 16.0, + ), + ], + ) + : Image( + image: assetImage, + height: 16.0, + ), ), onPressed: () { onClick(); diff --git a/lib/presentation/widgets/buttons/text_outlined_button.dart b/lib/presentation/widgets/buttons/text_outlined_button.dart index f2bc320a..e2b60615 100644 --- a/lib/presentation/widgets/buttons/text_outlined_button.dart +++ b/lib/presentation/widgets/buttons/text_outlined_button.dart @@ -3,10 +3,10 @@ import 'package:rtu_mirea_app/presentation/colors.dart'; class TextOutlinedButton extends StatelessWidget { final String content; - final double width; + final double? width; final VoidCallback? onPressed; const TextOutlinedButton( - {Key? key, required this.content, required this.width, this.onPressed}) + {Key? key, required this.content, this.width, this.onPressed}) : super(key: key); @override From c32b1fb005b4282f67d00d17744b31602409deae Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 24 Jan 2023 17:35:08 +0300 Subject: [PATCH 18/38] fix: Add https for the Schedule API (#266) --- lib/data/datasources/schedule_remote.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/datasources/schedule_remote.dart b/lib/data/datasources/schedule_remote.dart index 39f94cd7..89a69b35 100644 --- a/lib/data/datasources/schedule_remote.dart +++ b/lib/data/datasources/schedule_remote.dart @@ -8,7 +8,7 @@ abstract class ScheduleRemoteData { } class ScheduleRemoteDataImpl implements ScheduleRemoteData { - static const _apiUrl = 'http://schedule.mirea.ninja:5000/api/'; + static const _apiUrl = 'https://schedule.mirea.ninja/api/'; final Dio httpClient; From e96d97af030a7d871b0b8ee7e3a1f60fb4aa439d Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Thu, 26 Jan 2023 19:14:41 +0300 Subject: [PATCH 19/38] fix: Add nullable image formats from CMS (#267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Add nullable image formats from CMS Не все типы изображений могут быть оптимизированы. Например, svg изображения не форматируются, поэтому formats у них возвращается null. * dev: Update pubspec.yaml * dev: Update workflows flutter version --- .github/workflows/main.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/tests.yml | 2 +- lib/data/models/strapi_media_model.dart | 4 ++- lib/domain/entities/story.dart | 4 +-- lib/domain/entities/strapi_media.dart | 4 +-- .../pages/news/news_details_page.dart | 5 +++- .../pages/news/widgets/news_item.dart | 4 ++- .../pages/news/widgets/stories_wrapper.dart | 29 ++++++++++++------- .../pages/news/widgets/story_item.dart | 10 ++++--- .../widgets/images_horizontal_slider.dart | 10 +++++-- pubspec.yaml | 10 +++---- 12 files changed, 54 insertions(+), 32 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ac975b0..5fc4a8e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.0.5' + flutter-version: '3.3.10' - run: flutter pub get - name: Run Tests run: flutter test diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 6a6320a9..641df362 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.0.5' + flutter-version: '3.3.10' - run: flutter pub get - name: Run Tests run: flutter test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3c544eb5..e118238d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: java-version: '12.x' - uses: subosito/flutter-action@v1 with: - flutter-version: '3.0.5' + flutter-version: '3.3.10' channel: 'stable' # or: 'beta', 'dev' or 'master' - run: flutter pub get - run: flutter test ./test/mirea_test.dart diff --git a/lib/data/models/strapi_media_model.dart b/lib/data/models/strapi_media_model.dart index b1c049d3..fb31eeac 100644 --- a/lib/data/models/strapi_media_model.dart +++ b/lib/data/models/strapi_media_model.dart @@ -30,7 +30,9 @@ class StrapiMediaModel extends StrapiMedia { caption: json["caption"], width: json["width"], height: json["height"], - formats: FormatsModel.fromJson(json["formats"]), + formats: json["formats"] != null + ? FormatsModel.fromJson(json["formats"]) + : null, size: json["size"].toDouble(), url: json["url"], ); diff --git a/lib/domain/entities/story.dart b/lib/domain/entities/story.dart index 3ef5a5f5..ca077a8c 100644 --- a/lib/domain/entities/story.dart +++ b/lib/domain/entities/story.dart @@ -57,11 +57,11 @@ class Author extends Equatable { }); final String name; - final String url; + final String? url; final StrapiMedia logo; @override - List get props => [name, url, logo]; + List get props => [name, logo]; } class StoryPageAction extends Equatable { diff --git a/lib/domain/entities/strapi_media.dart b/lib/domain/entities/strapi_media.dart index 96030c27..5563dcbd 100644 --- a/lib/domain/entities/strapi_media.dart +++ b/lib/domain/entities/strapi_media.dart @@ -17,12 +17,12 @@ class StrapiMedia extends Equatable { final String? caption; final int width; final int height; - final Formats formats; + final Formats? formats; final double size; final String url; @override - List get props => [name, width, height, formats, size, url]; + List get props => [name, width, height, size, url]; } class Formats extends Equatable { diff --git a/lib/presentation/pages/news/news_details_page.dart b/lib/presentation/pages/news/news_details_page.dart index 70f97908..e5912b4d 100644 --- a/lib/presentation/pages/news/news_details_page.dart +++ b/lib/presentation/pages/news/news_details_page.dart @@ -29,7 +29,10 @@ class NewsDetailsPage extends StatelessWidget { children: [ Positioned.fill( child: Image.network( - StrapiUtils.getMediumImageUrl(newsItem.images[0].formats), + newsItem.images[0].formats != null + ? StrapiUtils.getMediumImageUrl( + newsItem.images[0].formats!) + : newsItem.images[0].url, fit: BoxFit.cover, ), ), diff --git a/lib/presentation/pages/news/widgets/news_item.dart b/lib/presentation/pages/news/widgets/news_item.dart index 9c5b909e..2fe3f4aa 100644 --- a/lib/presentation/pages/news/widgets/news_item.dart +++ b/lib/presentation/pages/news/widgets/news_item.dart @@ -43,7 +43,9 @@ class NewsItemWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ExtendedImage.network( - StrapiUtils.getMediumImageUrl(newsItem.images[0].formats), + newsItem.images[0].formats != null + ? StrapiUtils.getMediumImageUrl(newsItem.images[0].formats!) + : newsItem.images[0].url, height: 175, width: MediaQuery.of(context).size.width, fit: BoxFit.cover, diff --git a/lib/presentation/pages/news/widgets/stories_wrapper.dart b/lib/presentation/pages/news/widgets/stories_wrapper.dart index a319f9c0..cce8aa58 100644 --- a/lib/presentation/pages/news/widgets/stories_wrapper.dart +++ b/lib/presentation/pages/news/widgets/stories_wrapper.dart @@ -56,12 +56,19 @@ class _StoriesWrapperState extends State { child: Container(color: Colors.black), ), Positioned.fill( - child: Image.network( - MediaQuery.of(context).size.width > 580 - ? StrapiUtils.getLargestImageUrl(page.media.formats) - : StrapiUtils.getMediumImageUrl(page.media.formats), - fit: BoxFit.cover, - ), + child: page.media.formats != null + ? Image.network( + MediaQuery.of(context).size.width > 580 + ? StrapiUtils.getLargestImageUrl( + page.media.formats!) + : StrapiUtils.getMediumImageUrl( + page.media.formats!), + fit: BoxFit.cover, + ) + : Image.network( + page.media.url, + fit: BoxFit.cover, + ), ), Padding( padding: const EdgeInsets.only(top: 44, left: 8), @@ -72,10 +79,12 @@ class _StoriesWrapperState extends State { width: 32, decoration: BoxDecoration( image: DecorationImage( - image: NetworkImage( - author.logo.formats.small != null - ? author.logo.formats.small!.url - : author.logo.formats.thumbnail.url), + image: author.logo.formats != null + ? NetworkImage( + author.logo.formats!.small != null + ? author.logo.formats!.small!.url + : author.logo.formats!.thumbnail.url) + : NetworkImage(author.logo.url), fit: BoxFit.cover, ), shape: BoxShape.circle, diff --git a/lib/presentation/pages/news/widgets/story_item.dart b/lib/presentation/pages/news/widgets/story_item.dart index 45ea3972..c2e63868 100644 --- a/lib/presentation/pages/news/widgets/story_item.dart +++ b/lib/presentation/pages/news/widgets/story_item.dart @@ -31,10 +31,12 @@ class StoryWidget extends StatelessWidget { borderRadius: BorderRadius.circular(12), image: DecorationImage( fit: BoxFit.cover, - image: NetworkImage( - stories[storyIndex].preview.formats.small != null - ? stories[storyIndex].preview.formats.small!.url - : stories[storyIndex].preview.formats.thumbnail.url), + image: stories[storyIndex].preview.formats != null + ? NetworkImage( + stories[storyIndex].preview.formats!.small != null + ? stories[storyIndex].preview.formats!.small!.url + : stories[storyIndex].preview.formats!.thumbnail.url) + : NetworkImage(stories[storyIndex].preview.url), colorFilter: ColorFilter.mode( DarkThemeColors.background02.withOpacity(0.15), BlendMode.dstOut), diff --git a/lib/presentation/widgets/images_horizontal_slider.dart b/lib/presentation/widgets/images_horizontal_slider.dart index 0e04c576..e62c1815 100644 --- a/lib/presentation/widgets/images_horizontal_slider.dart +++ b/lib/presentation/widgets/images_horizontal_slider.dart @@ -26,8 +26,10 @@ class ImagesHorizontalSlider extends StatelessWidget { context, MaterialPageRoute( builder: (_) => FullScreenImage( - imageUrl: - StrapiUtils.getLargestImageUrl(images[index].formats), + imageUrl: images[index].formats != null + ? StrapiUtils.getLargestImageUrl( + images[index].formats!) + : images[index].url, ), ), ); @@ -37,7 +39,9 @@ class ImagesHorizontalSlider extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(12.0), child: Image.network( - images[index].formats.thumbnail.url, + images[index].formats != null + ? images[index].formats!.thumbnail.url + : images[index].url, height: 112, width: 158, fit: BoxFit.cover, diff --git a/pubspec.yaml b/pubspec.yaml index fa264378..290a7cbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,7 +67,7 @@ dependencies: # Internet connection checker. # See https://pub.dev/packages/internet_connection_checker - internet_connection_checker: ^0.0.1+3 + internet_connection_checker: ^1.0.0+1 # Functional Programming in Dart # See https://pub.dev/packages/dartz/versions/0.10.0-nullsafety.2 @@ -103,7 +103,7 @@ dependencies: # Creates page that is dismissed by swipe gestures, with Hero style # animations, Inspired by Facebook, Instagram stories. # See https://pub.dev/packages/dismissible_page - dismissible_page: ^0.7.3 + dismissible_page: ^1.0.1 # Instagram stories like UI with rich animations and customizability. # See https://pub.dev/packages/story @@ -146,7 +146,7 @@ dependencies: # Get version name in runtime # https://pub.dev/packages/package_info_plus - package_info_plus: ^1.3.0 + package_info_plus: ^3.0.2 # Encapsulates the notion of the "current time". Very useful for testing # See https://pub.dev/packages/clock @@ -162,7 +162,7 @@ dependencies: get_storage: # https://pub.dev/packages/home_widget - home_widget: ^0.1.6 + home_widget: ^0.2.0+1 freezed_annotation: ^2.1.0 @@ -186,7 +186,7 @@ dev_dependencies: flutter_lints: ^2.0.1 flutter_test: sdk: flutter - flutter_launcher_icons: ^0.10.0 + flutter_launcher_icons: ^0.11.0 auto_route_generator: ^5.0.2 build_runner: # https://pub.dev/packages/freezed From b55c0d39efec68036979b1969a4eba61ddb420d7 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Thu, 26 Jan 2023 20:31:36 +0300 Subject: [PATCH 20/38] fix: Disable Secure Storage Android backups (#268) --- android/app/src/main/AndroidManifest.xml | 3 ++- android/app/src/main/res/xml/backup_rules.xml | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 android/app/src/main/res/xml/backup_rules.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 70b1a38e..3aaa4d79 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,8 @@ + android:icon="@mipmap/ic_launcher" + android:fullBackupContent="@xml/backup_rules"> diff --git a/android/app/src/main/res/xml/backup_rules.xml b/android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 00000000..bba135d8 --- /dev/null +++ b/android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From d882bba82f1bac89ec82cb1416d279edead35df3 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Thu, 26 Jan 2023 21:40:29 +0300 Subject: [PATCH 21/38] refactor: Remove deprecated API's (#269) * refactor: Remove deprecated API's * Update profile_announces_page.dart --- lib/data/datasources/forum_remote.dart | 3 ++- lib/data/datasources/news_remote.dart | 3 ++- lib/data/datasources/strapi_remote.dart | 3 ++- .../bloc/news_bloc/news_bloc.dart | 1 - .../pages/news/news_details_page.dart | 4 ++-- .../pages/news/widgets/stories_wrapper.dart | 6 ++--- .../pages/onboarding/onboarding_page.dart | 3 ++- .../pages/onboarding/widgets/next_button.dart | 4 +++- .../pages/profile/about_app_page.dart | 22 ++++++++++--------- .../pages/profile/profile_announces_page.dart | 4 ++-- .../profile/profile_attendance_page.dart | 5 ++--- .../pages/profile/profile_lectors_page.dart | 3 ++- .../profile/widgets/attendance_card.dart | 4 ++-- .../profile/widgets/lector_search_card.dart | 6 ++--- .../pages/profile/widgets/member_info.dart | 4 ++-- .../pages/schedule/schedule_page.dart | 2 +- .../widgets/autocomplete_group_selector.dart | 3 +-- .../schedule/widgets/schedule_page_view.dart | 2 +- .../widgets/buttons/select_date_button.dart | 2 +- .../buttons/select_range_date_button.dart | 2 +- .../widgets/forms/labelled_input.dart | 2 +- .../widgets/fullscreen_image.dart | 2 +- .../widgets/settings_switch_button.dart | 2 +- pubspec.yaml | 5 +++++ 24 files changed, 54 insertions(+), 43 deletions(-) diff --git a/lib/data/datasources/forum_remote.dart b/lib/data/datasources/forum_remote.dart index 9c6f8e9e..2dc6d250 100644 --- a/lib/data/datasources/forum_remote.dart +++ b/lib/data/datasources/forum_remote.dart @@ -15,7 +15,8 @@ class ForumRemoteDataImpl implements ForumRemoteData { @override Future> getPatrons() async { - final response = await httpClient.get('${_apiUrl}groups/patrons/members.json?offset=0&order=&asc=true&filter='); + final response = await httpClient.get( + '${_apiUrl}groups/patrons/members.json?offset=0&order=&asc=true&filter='); if (response.statusCode == 200) { Map responseBody = response.data; List patrons = []; diff --git a/lib/data/datasources/news_remote.dart b/lib/data/datasources/news_remote.dart index 275d245a..340b3837 100644 --- a/lib/data/datasources/news_remote.dart +++ b/lib/data/datasources/news_remote.dart @@ -20,7 +20,8 @@ class NewsRemoteDataImpl extends NewsRemoteData { [String? tag]) async { final String tagsFilter = tag != null ? "&filters[tags][name][\$eq]=$tag" : ""; - final String requestUrl = '$_apiUrl/announcements?populate=*&pagination[limit]=$limit&pagination[start]=$offset&sort=date:DESC&filters[isImportant][\$eq]=${isImportant.toString()}$tagsFilter'; + final String requestUrl = + '$_apiUrl/announcements?populate=*&pagination[limit]=$limit&pagination[start]=$offset&sort=date:DESC&filters[isImportant][\$eq]=${isImportant.toString()}$tagsFilter'; final response = await httpClient.get(requestUrl); diff --git a/lib/data/datasources/strapi_remote.dart b/lib/data/datasources/strapi_remote.dart index c3fca237..10edfeea 100644 --- a/lib/data/datasources/strapi_remote.dart +++ b/lib/data/datasources/strapi_remote.dart @@ -17,7 +17,8 @@ class StrapiRemoteDataImpl implements StrapiRemoteData { @override Future> getStories() async { - final response = await httpClient.get('$_apiUrl/stories?populate[0]=pages.actions&populate[1]=pages.media&populate[2]=author&populate[3]=author.logo&populate[4]=preview'); + final response = await httpClient.get( + '$_apiUrl/stories?populate[0]=pages.actions&populate[1]=pages.media&populate[2]=author&populate[3]=author.logo&populate[4]=preview'); if (response.statusCode == 200) { final responseBody = response.data; List stories = []; diff --git a/lib/presentation/bloc/news_bloc/news_bloc.dart b/lib/presentation/bloc/news_bloc/news_bloc.dart index e418daf8..847ffeca 100644 --- a/lib/presentation/bloc/news_bloc/news_bloc.dart +++ b/lib/presentation/bloc/news_bloc/news_bloc.dart @@ -1,4 +1,3 @@ - import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:rtu_mirea_app/domain/entities/news_item.dart'; diff --git a/lib/presentation/pages/news/news_details_page.dart b/lib/presentation/pages/news/news_details_page.dart index e5912b4d..f5b94e63 100644 --- a/lib/presentation/pages/news/news_details_page.dart +++ b/lib/presentation/pages/news/news_details_page.dart @@ -9,7 +9,7 @@ import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/news/widgets/tags_widgets.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/images_horizontal_slider.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class NewsDetailsPage extends StatelessWidget { const NewsDetailsPage({Key? key, required this.newsItem}) : super(key: key); @@ -76,7 +76,7 @@ class NewsDetailsPage extends StatelessWidget { onLinkTap: (String? url, context, attributes, element) { if (url != null) { - launch(url); + launchUrlString(url); } }, ), diff --git a/lib/presentation/pages/news/widgets/stories_wrapper.dart b/lib/presentation/pages/news/widgets/stories_wrapper.dart index cce8aa58..a8703cb5 100644 --- a/lib/presentation/pages/news/widgets/stories_wrapper.dart +++ b/lib/presentation/pages/news/widgets/stories_wrapper.dart @@ -4,10 +4,10 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; import 'package:rtu_mirea_app/common/utils/utils.dart'; import 'package:story/story_page_view/story_page_view.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:rtu_mirea_app/domain/entities/story.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_button.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class StoriesWrapper extends StatefulWidget { const StoriesWrapper({ @@ -163,8 +163,8 @@ class _StoriesWrapperState extends State { child: PrimaryButton( text: widget.stories[pageIndex].pages[storyIndex] .actions[index].title, - onClick: () async { - await launch(widget.stories[pageIndex] + onClick: () { + launchUrlString(widget.stories[pageIndex] .pages[storyIndex].actions[index].url); }, ), diff --git a/lib/presentation/pages/onboarding/onboarding_page.dart b/lib/presentation/pages/onboarding/onboarding_page.dart index 24c0f2bd..1567f2d4 100644 --- a/lib/presentation/pages/onboarding/onboarding_page.dart +++ b/lib/presentation/pages/onboarding/onboarding_page.dart @@ -13,8 +13,9 @@ import 'widgets/next_button.dart'; /// OnBoarding screen that greets new users class OnBoardingPage extends StatefulWidget { const OnBoardingPage({Key? key}) : super(key: key); + @override - _OnBoardingPageState createState() => _OnBoardingPageState(); + State createState() => _OnBoardingPageState(); } class _OnBoardingPageState extends State { diff --git a/lib/presentation/pages/onboarding/widgets/next_button.dart b/lib/presentation/pages/onboarding/widgets/next_button.dart index 6c2618f7..8c95b5c9 100644 --- a/lib/presentation/pages/onboarding/widgets/next_button.dart +++ b/lib/presentation/pages/onboarding/widgets/next_button.dart @@ -28,7 +28,9 @@ class NextPageViewButton extends StatelessWidget { } }, style: ElevatedButton.styleFrom( - foregroundColor: DarkThemeColors.primary.withOpacity(0.25), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), backgroundColor: DarkThemeColors.primary, + foregroundColor: DarkThemeColors.primary.withOpacity(0.25), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + backgroundColor: DarkThemeColors.primary, shadowColor: const Color(0x7f000000), elevation: 8.0, ), diff --git a/lib/presentation/pages/profile/about_app_page.dart b/lib/presentation/pages/profile/about_app_page.dart index 1c8f9717..521c740b 100644 --- a/lib/presentation/pages/profile/about_app_page.dart +++ b/lib/presentation/pages/profile/about_app_page.dart @@ -7,7 +7,7 @@ import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/icon_button.dart'; import 'package:rtu_mirea_app/service_locator.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'widgets/member_info.dart'; @@ -66,7 +66,7 @@ class AboutAppPage extends StatelessWidget { .copyWith(color: DarkThemeColors.primary), recognizer: TapGestureRecognizer() ..onTap = () { - launch('https://ischemes.ru/'); + launchUrlString('https://ischemes.ru/'); }, ), ], @@ -86,7 +86,7 @@ class AboutAppPage extends StatelessWidget { .copyWith(color: DarkThemeColors.primary), recognizer: TapGestureRecognizer() ..onTap = () { - launch('https://mirea.ru/news/'); + launchUrlString('https://mirea.ru/news/'); }, ), ], @@ -111,7 +111,7 @@ class AboutAppPage extends StatelessWidget { .copyWith(color: DarkThemeColors.primary), recognizer: TapGestureRecognizer() ..onTap = () { - launch('https://mirea.ninja/'); + launchUrlString("https://mirea.ninja/"); }, ), ], @@ -124,20 +124,21 @@ class AboutAppPage extends StatelessWidget { SocialIconButton( assetImage: const AssetImage('assets/icons/github.png'), onClick: () { - launch( + launchUrlString( 'https://github.com/Ninja-Official/rtu-mirea-mobile'); }), const SizedBox(width: 12), SocialIconButton( assetImage: const AssetImage('assets/icons/patreon.png'), onClick: () { - launch('https://www.patreon.com/mireaninja'); + launchUrlString('https://www.patreon.com/mireaninja'); }), const SizedBox(width: 12), SocialIconButton( assetImage: const AssetImage('assets/icons/telegram.png'), onClick: () { - launch('https://t.me/joinchat/LyM7jcoRXUhmOGM6'); + launchUrlString( + 'https://t.me/joinchat/LyM7jcoRXUhmOGM6'); }), ], ), @@ -208,9 +209,10 @@ class AboutAppPage extends StatelessWidget { children: List.generate(state.patrons.length, (index) { return MemberInfo( username: state.patrons[index].username, - avatarUrl: 'https://mirea.ninja/${state.patrons[index].avatarTemplate - .replaceAll('{size}', '120')}', - profileUrl: 'https://mirea.ninja/u/${state.patrons[index].username}', + avatarUrl: + 'https://mirea.ninja/${state.patrons[index].avatarTemplate.replaceAll('{size}', '120')}', + profileUrl: + 'https://mirea.ninja/u/${state.patrons[index].username}', ); }), ); diff --git a/lib/presentation/pages/profile/profile_announces_page.dart b/lib/presentation/pages/profile/profile_announces_page.dart index d8064acd..a73ecbdc 100644 --- a/lib/presentation/pages/profile/profile_announces_page.dart +++ b/lib/presentation/pages/profile/profile_announces_page.dart @@ -4,7 +4,7 @@ import 'package:flutter_html/flutter_html.dart'; import 'package:rtu_mirea_app/presentation/bloc/announces_bloc/announces_bloc.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class ProfileAnnouncesPage extends StatelessWidget { const ProfileAnnouncesPage({Key? key}) : super(key: key); @@ -52,7 +52,7 @@ class ProfileAnnouncesPage extends StatelessWidget { }, onLinkTap: (String? url, context, attributes, element) { if (url != null) { - launch(url); + launchUrlString(url); } }, ), diff --git a/lib/presentation/pages/profile/profile_attendance_page.dart b/lib/presentation/pages/profile/profile_attendance_page.dart index d00a875d..ade2f76e 100644 --- a/lib/presentation/pages/profile/profile_attendance_page.dart +++ b/lib/presentation/pages/profile/profile_attendance_page.dart @@ -13,7 +13,7 @@ class ProfileAttendancePage extends StatefulWidget { const ProfileAttendancePage({Key? key}) : super(key: key); @override - _ProfileAttendancePageState createState() => _ProfileAttendancePageState(); + State createState() => _ProfileAttendancePageState(); } class _ProfileAttendancePageState extends State { @@ -95,8 +95,7 @@ class _ProfileAttendancePageState extends State { child: Column( children: [ const SizedBox(height: 8), - Text( - 'Дней посещено: ${state.visitsCount}', + Text('Дней посещено: ${state.visitsCount}', style: DarkTextTheme.body), const SizedBox(height: 8), Expanded( diff --git a/lib/presentation/pages/profile/profile_lectors_page.dart b/lib/presentation/pages/profile/profile_lectors_page.dart index 27b631d6..aa05db51 100644 --- a/lib/presentation/pages/profile/profile_lectors_page.dart +++ b/lib/presentation/pages/profile/profile_lectors_page.dart @@ -62,7 +62,8 @@ class _ProfileLectrosPageState extends State { ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(FontAwesomeIcons.search, size: 85), + const Icon(FontAwesomeIcons.magnifyingGlass, + size: 85), const SizedBox(height: 24), Text('Здесь появятся результаты поиска', style: DarkTextTheme.body) diff --git a/lib/presentation/pages/profile/widgets/attendance_card.dart b/lib/presentation/pages/profile/widgets/attendance_card.dart index 1152e88c..26c31982 100644 --- a/lib/presentation/pages/profile/widgets/attendance_card.dart +++ b/lib/presentation/pages/profile/widgets/attendance_card.dart @@ -53,8 +53,8 @@ class AttendanceCard extends StatelessWidget { ), alignment: Alignment.center, child: type == "Вход" - ? const Icon(FontAwesomeIcons.signInAlt, size: 15) - : const Icon(FontAwesomeIcons.signOutAlt, size: 15), + ? const Icon(FontAwesomeIcons.rightToBracket, size: 15) + : const Icon(FontAwesomeIcons.rightToBracket, size: 15), ), ), const SizedBox(width: 55.50), diff --git a/lib/presentation/pages/profile/widgets/lector_search_card.dart b/lib/presentation/pages/profile/widgets/lector_search_card.dart index a94d574f..e9ab2bee 100644 --- a/lib/presentation/pages/profile/widgets/lector_search_card.dart +++ b/lib/presentation/pages/profile/widgets/lector_search_card.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:rtu_mirea_app/domain/entities/employee.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class LectorSearchCard extends StatelessWidget { const LectorSearchCard({Key? key, required this.employee}) : super(key: key); @@ -36,8 +36,8 @@ class LectorSearchCard extends StatelessWidget { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Email адрес скопирован!'))); }, - onPressed: () async { - await launch("mailto:${employee.email}?subject=&body="); + onPressed: () { + launchUrlString("mailto:${employee.email}?subject=&body="); }, child: Text( employee.email, diff --git a/lib/presentation/pages/profile/widgets/member_info.dart b/lib/presentation/pages/profile/widgets/member_info.dart index 5b6ece6c..39670aab 100644 --- a/lib/presentation/pages/profile/widgets/member_info.dart +++ b/lib/presentation/pages/profile/widgets/member_info.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class MemberInfo extends StatelessWidget { const MemberInfo( @@ -35,7 +35,7 @@ class MemberInfo extends StatelessWidget { ), ]), onTap: () { - launch(profileUrl); + launchUrlString(profileUrl); }, ); } diff --git a/lib/presentation/pages/schedule/schedule_page.dart b/lib/presentation/pages/schedule/schedule_page.dart index 23612021..112f8a73 100644 --- a/lib/presentation/pages/schedule/schedule_page.dart +++ b/lib/presentation/pages/schedule/schedule_page.dart @@ -16,7 +16,7 @@ class SchedulePage extends StatefulWidget { const SchedulePage({Key? key}) : super(key: key); @override - _SchedulePageState createState() => _SchedulePageState(); + State createState() => _SchedulePageState(); } class _SchedulePageState extends State { diff --git a/lib/presentation/pages/schedule/widgets/autocomplete_group_selector.dart b/lib/presentation/pages/schedule/widgets/autocomplete_group_selector.dart index d5253c31..ef211133 100644 --- a/lib/presentation/pages/schedule/widgets/autocomplete_group_selector.dart +++ b/lib/presentation/pages/schedule/widgets/autocomplete_group_selector.dart @@ -46,8 +46,7 @@ class AutocompleteGroupSelector extends StatefulWidget { const AutocompleteGroupSelector({Key? key}) : super(key: key); @override - _AutocompleteGroupSelectorState createState() => - _AutocompleteGroupSelectorState(); + State createState() => _AutocompleteGroupSelectorState(); } class _AutocompleteGroupSelectorState extends State { diff --git a/lib/presentation/pages/schedule/widgets/schedule_page_view.dart b/lib/presentation/pages/schedule/widgets/schedule_page_view.dart index 2d942684..e4e754e0 100644 --- a/lib/presentation/pages/schedule/widgets/schedule_page_view.dart +++ b/lib/presentation/pages/schedule/widgets/schedule_page_view.dart @@ -18,7 +18,7 @@ class SchedulePageView extends StatefulWidget { final Schedule schedule; @override - _SchedulePageViewState createState() => _SchedulePageViewState(); + State createState() => _SchedulePageViewState(); } class _SchedulePageViewState extends State { diff --git a/lib/presentation/widgets/buttons/select_date_button.dart b/lib/presentation/widgets/buttons/select_date_button.dart index 153fdaa3..ed66357c 100644 --- a/lib/presentation/widgets/buttons/select_date_button.dart +++ b/lib/presentation/widgets/buttons/select_date_button.dart @@ -20,7 +20,7 @@ class SelectDateButton extends StatefulWidget { final String? text; @override - _SelectDateButtonState createState() => _SelectDateButtonState(); + State createState() => _SelectDateButtonState(); } class _SelectDateButtonState extends State { diff --git a/lib/presentation/widgets/buttons/select_range_date_button.dart b/lib/presentation/widgets/buttons/select_range_date_button.dart index 509a2310..8754efeb 100644 --- a/lib/presentation/widgets/buttons/select_range_date_button.dart +++ b/lib/presentation/widgets/buttons/select_range_date_button.dart @@ -21,7 +21,7 @@ class SelectRangeDateButton extends StatefulWidget { final String? text; @override - _SelectRangeDateButtonState createState() => _SelectRangeDateButtonState(); + State createState() => _SelectRangeDateButtonState(); } class _SelectRangeDateButtonState extends State { diff --git a/lib/presentation/widgets/forms/labelled_input.dart b/lib/presentation/widgets/forms/labelled_input.dart index 27766c1c..910baca9 100644 --- a/lib/presentation/widgets/forms/labelled_input.dart +++ b/lib/presentation/widgets/forms/labelled_input.dart @@ -79,7 +79,7 @@ class _LabelledInputState extends State { onTap: () { widget.controller.text = ""; }, - child: const Icon(FontAwesomeIcons.solidTimesCircle, + child: const Icon(FontAwesomeIcons.solidCircleXmark, size: 15, color: DarkThemeColors.deactiveDarker), ), hintText: widget.placeholder, diff --git a/lib/presentation/widgets/fullscreen_image.dart b/lib/presentation/widgets/fullscreen_image.dart index df3ec857..53252a81 100644 --- a/lib/presentation/widgets/fullscreen_image.dart +++ b/lib/presentation/widgets/fullscreen_image.dart @@ -9,7 +9,7 @@ class FullScreenImage extends StatefulWidget { final String imageUrl; @override - _FullScreenImageState createState() => _FullScreenImageState(); + State createState() => _FullScreenImageState(); } class _FullScreenImageState extends State diff --git a/lib/presentation/widgets/settings_switch_button.dart b/lib/presentation/widgets/settings_switch_button.dart index 52843c5f..96c0ea26 100644 --- a/lib/presentation/widgets/settings_switch_button.dart +++ b/lib/presentation/widgets/settings_switch_button.dart @@ -19,7 +19,7 @@ class SettingsSwitchButton extends StatefulWidget { final bool initialValue; @override - _SettingsSwitchButtonState createState() => _SettingsSwitchButtonState(); + State createState() => _SettingsSwitchButtonState(); } class _SettingsSwitchButtonState extends State { diff --git a/pubspec.yaml b/pubspec.yaml index 290a7cbc..0711464c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,11 @@ dependencies: flutter_localizations: sdk: flutter + + # Contains code to deal with internationalized/localized messages, date and number formatting + # and parsing, bi-directional text, and other internationalization issues. + # See https://pub.dev/packages/intl + intl: ^0.17.0 # Http client. # See https://pub.dev/packages/dio From fbd7d5035f1874c299d8d94e33cb367214e0c30a Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Fri, 27 Jan 2023 14:32:29 +0300 Subject: [PATCH 22/38] ui: New Group selection page (#270) --- assets/icons/search.svg | 16 +- assets/json/groups_by_institute.json | 219 +++++++ .../update_info_model.freezed.dart | 84 ++- .../bloc/schedule_bloc/schedule_bloc.dart | 97 +-- .../bloc/schedule_bloc/schedule_event.dart | 21 +- .../bloc/schedule_bloc/schedule_state.dart | 24 +- lib/presentation/core/routes/routes.dart | 14 +- lib/presentation/core/routes/routes.gr.dart | 585 ++++++++++++------ lib/presentation/pages/home_page.dart | 2 +- .../pages/schedule/groups_select_page.dart | 279 +++++++++ .../pages/schedule/schedule_page.dart | 2 +- .../widgets/autocomplete_group_selector.dart | 128 ---- .../widgets/schedule_settings_modal.dart | 33 +- lib/presentation/theme.dart | 2 +- pubspec.yaml | 1 + 15 files changed, 1056 insertions(+), 451 deletions(-) create mode 100644 assets/json/groups_by_institute.json create mode 100644 lib/presentation/pages/schedule/groups_select_page.dart delete mode 100644 lib/presentation/pages/schedule/widgets/autocomplete_group_selector.dart diff --git a/assets/icons/search.svg b/assets/icons/search.svg index 755046a9..baae1844 100644 --- a/assets/icons/search.svg +++ b/assets/icons/search.svg @@ -1,13 +1,3 @@ - - - - Shape - Created with Sketch. - - - - - - - - \ No newline at end of file + + + diff --git a/assets/json/groups_by_institute.json b/assets/json/groups_by_institute.json new file mode 100644 index 00000000..e17bcc62 --- /dev/null +++ b/assets/json/groups_by_institute.json @@ -0,0 +1,219 @@ +{ + "ИПТИП": [ + "ЭОСО", + "ЭЛБО", + "ЭСБО", + "ЭНБО", + "ЭЭБО", + "ЭФБО", + "ТСБО", + "ТШБО", + "ТДБО", + "ТХБО", + "ТКБО", + "ТЛБО", + "ТОБО", + "ЭЭМО", + "ЭНМО", + "ЭФМО", + "ЭПМО", + "ЭСМО", + "ЭОМО", + "ТДМО", + "ТКМО", + "ТЛМО", + "ТОМО", + "ТХМО", + "ТШМО", + "ТДАО", + "ТОАО", + "ТПАО", + "ТУАО", + "ТХАО", + "ТЩАО", + "ТЭАО", + "ТЮАО", + "ТЯАО", + "ТФАО", + "ТТАО", + "ТВАО", + "ТШАО", + "ТСАО", + "ТЮАЗ", + "ТЧАО" + ], + "ИТУ": [ + "УДБО", + "УИБО", + "УСБО", + "УЭБО", + "ГДБО", + "ГИБО", + "ГУБО", + "ГМБО", + "ГСБО", + "ГЭБО", + "УПБО", + "УУБО", + "УНБО", + "УЮБО", + "УМБО", + "УКБО", + "УХБО", + "УКАО", + "УОАО", + "ГЭАО", + "ГУАО", + "ГОАО", + "ГЯАО", + "ГЦАО", + "ГЭАЗ", + "УКАЗ", + "ГЖАЗ", + "УЭАЗ", + "ГСАЗ", + "УИМО", + "УСМО", + "УДМО", + "УОМО", + "УЮМО", + "УУМВ", + "УЭМВ", + "УКМО", + "УММО", + "УУМО", + "УЭМО", + "УПМО" + ], + "ИИТ": [ + "ИВБО", + "ИКБО", + "ИМБО", + "ИНБО", + "ИАБО", + "ИМАО", + "ИПАО", + "ИУАО", + "ИЦАО", + "ИЧАО", + "ИМАЗ", + "ИВМО", + "ИКМО", + "ИММО", + "ИНМО" + ], + "ИИИ": [ + "КМБО", + "КББО", + "КСБО", + "КРБО", + "КВБО", + "КАБО", + "КТСО", + "ККСО", + "КФБО", + "КУБО", + "КААО", + "КИАО", + "КНАО", + "КСАО", + "КМАО", + "КРАО", + "КЦАО", + "КЧАО", + "КФАО", + "КСАЗ", + "КММО", + "КСМО", + "КРМО", + "КУМО", + "КБМО", + "КАМО", + "КВМО", + "КФМО" + ], + "ИКБ": [ + "БАСО", + "БББО", + "БИСО", + "БСБО", + "ББСО", + "БПБО", + "БФБО", + "БЭСО", + "БОСО", + "БПСО", + "БИАО", + "БТАО", + "БМАО", + "БСАО", + "БУАО", + "БЦАО", + "БЧАО", + "БЯАО", + "БААО", + "БДАО", + "БЭАЗ", + "БМАЗ", + "БСМО", + "ББМО", + "БПМО", + "БФМО" + ], + "ИРИ": [ + "РССО", + "РРБО", + "РИБО", + "РКБО", + "РГБО", + "РСБО", + "РААО", + "РНАО", + "РРАО", + "РСАО", + "РТАО", + "РЧАО", + "РУАО", + "РРМО", + "РИМО", + "РКМО", + "РГМО" + ], + "ИТХТ": [ + "ХЕБО", + "ХТБО", + "ХХБО", + "ХББО", + "ЭСБО", + "ХБАО", + "ХВАО", + "ХГАО", + "ХКАО", + "ХЛАО", + "ХМАО", + "ХОАО", + "ХРАО", + "ХФАО", + "ХХАО", + "ХЧАО", + "ХЩАО", + "ХААО", + "ХНАО", + "ХСАО", + "ХЧАЗ", + "ХБМО", + "ХЕМО", + "ХТМО", + "ХФМО", + "ХХМО" + ], + "КПК": [ + "ЩАКО", + "ЩПКО", + "ЩСКО", + "ЩИКО", + "ЩТКО", + "ЩОКО", + "ЩККО" + ] +} \ No newline at end of file diff --git a/lib/data/models/update_info_model/update_info_model.freezed.dart b/lib/data/models/update_info_model/update_info_model.freezed.dart index 322a4710..c792d5dc 100644 --- a/lib/data/models/update_info_model/update_info_model.freezed.dart +++ b/lib/data/models/update_info_model/update_info_model.freezed.dart @@ -1,7 +1,7 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'update_info_model.dart'; @@ -41,7 +41,8 @@ mixin _$UpdateInfoModel { abstract class $UpdateInfoModelCopyWith<$Res> { factory $UpdateInfoModelCopyWith( UpdateInfoModel value, $Res Function(UpdateInfoModel) then) = - _$UpdateInfoModelCopyWithImpl<$Res>; + _$UpdateInfoModelCopyWithImpl<$Res, UpdateInfoModel>; + @useResult $Res call( {@JsonKey(name: 'title') String title, @JsonKey(name: 'description') String? description, @@ -51,44 +52,46 @@ abstract class $UpdateInfoModelCopyWith<$Res> { } /// @nodoc -class _$UpdateInfoModelCopyWithImpl<$Res> +class _$UpdateInfoModelCopyWithImpl<$Res, $Val extends UpdateInfoModel> implements $UpdateInfoModelCopyWith<$Res> { _$UpdateInfoModelCopyWithImpl(this._value, this._then); - final UpdateInfoModel _value; // ignore: unused_field - final $Res Function(UpdateInfoModel) _then; + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + @pragma('vm:prefer-inline') @override $Res call({ - Object? title = freezed, + Object? title = null, Object? description = freezed, - Object? text = freezed, - Object? appVersion = freezed, - Object? buildNumber = freezed, + Object? text = null, + Object? appVersion = null, + Object? buildNumber = null, }) { return _then(_value.copyWith( - title: title == freezed + title: null == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, - description: description == freezed + description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String?, - text: text == freezed + text: null == text ? _value.text : text // ignore: cast_nullable_to_non_nullable as String, - appVersion: appVersion == freezed + appVersion: null == appVersion ? _value.appVersion : appVersion // ignore: cast_nullable_to_non_nullable as String, - buildNumber: buildNumber == freezed + buildNumber: null == buildNumber ? _value.buildNumber : buildNumber // ignore: cast_nullable_to_non_nullable as int, - )); + ) as $Val); } } @@ -99,6 +102,7 @@ abstract class _$$_UpdateInfoModelCopyWith<$Res> _$_UpdateInfoModel value, $Res Function(_$_UpdateInfoModel) then) = __$$_UpdateInfoModelCopyWithImpl<$Res>; @override + @useResult $Res call( {@JsonKey(name: 'title') String title, @JsonKey(name: 'description') String? description, @@ -109,41 +113,39 @@ abstract class _$$_UpdateInfoModelCopyWith<$Res> /// @nodoc class __$$_UpdateInfoModelCopyWithImpl<$Res> - extends _$UpdateInfoModelCopyWithImpl<$Res> + extends _$UpdateInfoModelCopyWithImpl<$Res, _$_UpdateInfoModel> implements _$$_UpdateInfoModelCopyWith<$Res> { __$$_UpdateInfoModelCopyWithImpl( _$_UpdateInfoModel _value, $Res Function(_$_UpdateInfoModel) _then) - : super(_value, (v) => _then(v as _$_UpdateInfoModel)); - - @override - _$_UpdateInfoModel get _value => super._value as _$_UpdateInfoModel; + : super(_value, _then); + @pragma('vm:prefer-inline') @override $Res call({ - Object? title = freezed, + Object? title = null, Object? description = freezed, - Object? text = freezed, - Object? appVersion = freezed, - Object? buildNumber = freezed, + Object? text = null, + Object? appVersion = null, + Object? buildNumber = null, }) { return _then(_$_UpdateInfoModel( - title: title == freezed + title: null == title ? _value.title : title // ignore: cast_nullable_to_non_nullable as String, - description: description == freezed + description: freezed == description ? _value.description : description // ignore: cast_nullable_to_non_nullable as String?, - text: text == freezed + text: null == text ? _value.text : text // ignore: cast_nullable_to_non_nullable as String, - appVersion: appVersion == freezed + appVersion: null == appVersion ? _value.appVersion : appVersion // ignore: cast_nullable_to_non_nullable as String, - buildNumber: buildNumber == freezed + buildNumber: null == buildNumber ? _value.buildNumber : buildNumber // ignore: cast_nullable_to_non_nullable as int, @@ -190,28 +192,24 @@ class _$_UpdateInfoModel implements _UpdateInfoModel { return identical(this, other) || (other.runtimeType == runtimeType && other is _$_UpdateInfoModel && - const DeepCollectionEquality().equals(other.title, title) && - const DeepCollectionEquality() - .equals(other.description, description) && - const DeepCollectionEquality().equals(other.text, text) && - const DeepCollectionEquality() - .equals(other.appVersion, appVersion) && - const DeepCollectionEquality() - .equals(other.buildNumber, buildNumber)); + (identical(other.title, title) || other.title == title) && + (identical(other.description, description) || + other.description == description) && + (identical(other.text, text) || other.text == text) && + (identical(other.appVersion, appVersion) || + other.appVersion == appVersion) && + (identical(other.buildNumber, buildNumber) || + other.buildNumber == buildNumber)); } @JsonKey(ignore: true) @override int get hashCode => Object.hash( - runtimeType, - const DeepCollectionEquality().hash(title), - const DeepCollectionEquality().hash(description), - const DeepCollectionEquality().hash(text), - const DeepCollectionEquality().hash(appVersion), - const DeepCollectionEquality().hash(buildNumber)); + runtimeType, title, description, text, appVersion, buildNumber); @JsonKey(ignore: true) @override + @pragma('vm:prefer-inline') _$$_UpdateInfoModelCopyWith<_$_UpdateInfoModel> get copyWith => __$$_UpdateInfoModelCopyWithImpl<_$_UpdateInfoModel>(this, _$identity); diff --git a/lib/presentation/bloc/schedule_bloc/schedule_bloc.dart b/lib/presentation/bloc/schedule_bloc/schedule_bloc.dart index d8eb720d..5441d9b5 100644 --- a/lib/presentation/bloc/schedule_bloc/schedule_bloc.dart +++ b/lib/presentation/bloc/schedule_bloc/schedule_bloc.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:convert'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/services.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/common/widget_data_init.dart'; import 'package:rtu_mirea_app/domain/entities/schedule.dart'; @@ -30,8 +32,6 @@ class ScheduleBloc extends Bloc { required this.setScheduleSettings, }) : super(ScheduleInitial()) { on(_onScheduleOpenEvent); - on( - _onScheduleUpdateGroupSuggestionEvent); on(_onScheduleSetActiveGroupEvent); on(_onScheduleUpdateEvent); on(_onScheduleDeleteEvent); @@ -47,26 +47,45 @@ class ScheduleBloc extends Bloc { final GetScheduleSettings getScheduleSettings; final SetScheduleSettings setScheduleSettings; - /// List of all groups (1028+) - static List groupsList = []; + /// It is used to display the list of groups. It is downloaded from the + /// Internet once when the schedule page is opened. While downloading, + /// state [ScheduleLoading] is displayed. + List _groupsList = []; - late List _downloadedGroups; + // It is used to display the list of groups by institute in the group + // selection page. It is stored in the assets folder and is opened once when + // the schedule page is opened. While downloading, state [ScheduleLoading] is + // displayed. Format: {institute: [group1, group2, ...]}, where institute is + // the short name of the institute, for example, "ИИТ" or "ИИИ", and group1, + // group2, ... are the names only of the groups of this institute, for example, + // "ИКБО", "ИВБО", etc. Groups names are unique. + late final Map> _groupsByInstitute; - /// [_groupSuggestion] is used when selecting a group in [AutocompleteGroupSelector] - String _groupSuggestion = ''; + late List _downloadedGroups; void _onScheduleOpenEvent( ScheduleOpenEvent event, Emitter emit, ) async { - // Getting a list of all groups from a remote API - _downloadGroups(); + if (_groupsList.isEmpty) { + emit(ScheduleLoading()); + // Getting a list of all groups from a remote API + await _downloadGroups(); + + final rawJson = + await rootBundle.loadString('assets/json/groups_by_institute.json'); + _groupsByInstitute = { + for (final entry in json.decode(rawJson).entries) + entry.key: List.from(entry.value) + }; + } // The group for which the schedule is selected final activeGroup = await getActiveGroup(); await activeGroup.fold((failure) { - emit(ScheduleActiveGroupEmpty(groups: groupsList)); + emit(ScheduleActiveGroupEmpty( + groups: _groupsList, groupsByInstitute: _groupsByInstitute)); }, (activeGroupName) async { final schedule = await getSchedule( GetScheduleParams(group: activeGroupName, fromRemote: false)); @@ -80,15 +99,20 @@ class ScheduleBloc extends Bloc { emit(ScheduleLoading()); final remoteSchedule = await getSchedule( GetScheduleParams(group: activeGroupName, fromRemote: true)); - emit(remoteSchedule.fold( + emit( + remoteSchedule.fold( (failureRemote) => ScheduleLoadError( errorMessage: _mapFailureToMessage(failureRemote)), (scheduleFromRemote) => ScheduleLoaded( - schedule: scheduleFromRemote, - activeGroup: activeGroupName, - downloadedScheduleGroups: downloadedScheduleGroups, - scheduleSettings: scheduleSettings, - ))); + schedule: scheduleFromRemote, + activeGroup: activeGroupName, + downloadedScheduleGroups: downloadedScheduleGroups, + scheduleSettings: scheduleSettings, + groups: _groupsList, + groupsByInstitute: _groupsByInstitute, + ), + ), + ); }, (localSchedule) async { // display cached schedule emit(ScheduleLoaded( @@ -96,6 +120,8 @@ class ScheduleBloc extends Bloc { activeGroup: activeGroupName, downloadedScheduleGroups: downloadedScheduleGroups, scheduleSettings: scheduleSettings, + groups: _groupsList, + groupsByInstitute: _groupsByInstitute, )); // We will update the schedule, but without the loading indicator @@ -109,6 +135,8 @@ class ScheduleBloc extends Bloc { activeGroup: activeGroupName, downloadedScheduleGroups: downloadedScheduleGroups, scheduleSettings: scheduleSettings, + groups: _groupsList, + groupsByInstitute: _groupsByInstitute, ), )); } @@ -116,27 +144,17 @@ class ScheduleBloc extends Bloc { }); } - void _onScheduleUpdateGroupSuggestionEvent( - ScheduleUpdateGroupSuggestionEvent event, - Emitter emit, - ) async { - _groupSuggestion = event.suggestion; - } - void _onScheduleSetActiveGroupEvent( ScheduleSetActiveGroupEvent event, Emitter emit, ) async { - if (groupsList.contains(_groupSuggestion) || event.group != null) { - // on update active group from drawer group list - if (event.group != null) _groupSuggestion = event.group!; - + if (_groupsList.contains(event.group)) { emit(ScheduleLoading()); - await setActiveGroup(SetActiveGroupParams(_groupSuggestion)); + await setActiveGroup(SetActiveGroupParams(event.group)); final schedule = await getSchedule( - GetScheduleParams(group: _groupSuggestion, fromRemote: true)); + GetScheduleParams(group: event.group, fromRemote: true)); _downloadedGroups = await _getAllDownloadedScheduleGroups(); final scheduleSettings = await getScheduleSettings(); @@ -150,13 +168,16 @@ class ScheduleBloc extends Bloc { // Set app info return ScheduleLoaded( schedule: schedule, - activeGroup: _groupSuggestion, + activeGroup: event.group, downloadedScheduleGroups: _downloadedGroups, scheduleSettings: scheduleSettings, + groups: _groupsList, + groupsByInstitute: _groupsByInstitute, ); })); } else { - emit(ScheduleGroupNotFound()); + emit(ScheduleActiveGroupEmpty( + groups: _groupsList, groupsByInstitute: _groupsByInstitute)); } } @@ -183,6 +204,8 @@ class ScheduleBloc extends Bloc { activeGroup: event.activeGroup, downloadedScheduleGroups: _downloadedGroups, scheduleSettings: scheduleSettings, + groups: _groupsList, + groupsByInstitute: _groupsByInstitute, ), )); } @@ -200,6 +223,8 @@ class ScheduleBloc extends Bloc { activeGroup: event.schedule.group, downloadedScheduleGroups: _downloadedGroups, scheduleSettings: scheduleSettings, + groups: _groupsList, + groupsByInstitute: _groupsByInstitute, )); } @@ -224,16 +249,16 @@ class ScheduleBloc extends Bloc { activeGroup: currentState.schedule.group, downloadedScheduleGroups: currentState.downloadedScheduleGroups, scheduleSettings: newSettings, + groups: currentState.groups, + groupsByInstitute: _groupsByInstitute, )); } } Future _downloadGroups() async { - if (groupsList.isEmpty) { - final groups = await getGroups(); - groups.fold( - (failure) => groupsList = [], (groups) => groupsList = groups); - } + final groups = await getGroups(); + groups.fold( + (failure) => _groupsList = [], (groups) => _groupsList = groups); } /// Returns list of cached schedules or empty list diff --git a/lib/presentation/bloc/schedule_bloc/schedule_event.dart b/lib/presentation/bloc/schedule_bloc/schedule_event.dart index 2f649486..0318f3b0 100644 --- a/lib/presentation/bloc/schedule_bloc/schedule_event.dart +++ b/lib/presentation/bloc/schedule_bloc/schedule_event.dart @@ -10,30 +10,15 @@ abstract class ScheduleEvent extends Equatable { /// The event should be called immediately after opening the schedule page class ScheduleOpenEvent extends ScheduleEvent {} -/// The event should be called when typing a group in the input input -/// field [AutocompleteGroupSelector]. [suggestion] is the text value of -/// the field. -class ScheduleUpdateGroupSuggestionEvent extends ScheduleEvent { - const ScheduleUpdateGroupSuggestionEvent({required this.suggestion}); - - final String suggestion; - - @override - List get props => [suggestion]; -} - /// The event should be called to update the list of groups class ScheduleGroupsLoadEvent extends ScheduleEvent {} /// The event should be called to set the active group for which -/// the schedule will be taken. The group name is the [_groupSuggestion] -/// field in [ScheduleBloc]. [_groupSuggestion] should be set every -/// time the input is updated using the event -/// [ScheduleUpdateGroupSuggestionEvent]. +/// the schedule will be taken. class ScheduleSetActiveGroupEvent extends ScheduleEvent { - const ScheduleSetActiveGroupEvent([this.group]); + const ScheduleSetActiveGroupEvent({required this.group}); - final String? group; + final String group; } class ScheduleUpdateLessonsEvent extends ScheduleEvent { diff --git a/lib/presentation/bloc/schedule_bloc/schedule_state.dart b/lib/presentation/bloc/schedule_bloc/schedule_state.dart index 831e5e5d..2c65dc8a 100644 --- a/lib/presentation/bloc/schedule_bloc/schedule_state.dart +++ b/lib/presentation/bloc/schedule_bloc/schedule_state.dart @@ -18,15 +18,17 @@ class ScheduleInitial extends ScheduleState {} /// empty if for some reason it was not possible to get a list of groups. class ScheduleActiveGroupEmpty extends ScheduleState { final List groups; + final Map> groupsByInstitute; - const ScheduleActiveGroupEmpty({required this.groups}); + const ScheduleActiveGroupEmpty({ + required this.groups, + required this.groupsByInstitute, + }); @override - List get props => [groups]; + List get props => [groups, groupsByInstitute]; } -class ScheduleGroupNotFound extends ScheduleState {} - class ScheduleLoading extends ScheduleState {} class ScheduleLoaded extends ScheduleState { @@ -34,17 +36,27 @@ class ScheduleLoaded extends ScheduleState { final String activeGroup; final List downloadedScheduleGroups; final ScheduleSettings scheduleSettings; + final List groups; + final Map> groupsByInstitute; const ScheduleLoaded({ required this.schedule, required this.activeGroup, required this.downloadedScheduleGroups, required this.scheduleSettings, + required this.groups, + required this.groupsByInstitute, }); @override - List get props => - [schedule, activeGroup, downloadedScheduleGroups, scheduleSettings]; + List get props => [ + schedule, + activeGroup, + downloadedScheduleGroups, + scheduleSettings, + groups, + groupsByInstitute, + ]; } class ScheduleLoadError extends ScheduleState { diff --git a/lib/presentation/core/routes/routes.dart b/lib/presentation/core/routes/routes.dart index 41c49728..b18d563a 100644 --- a/lib/presentation/core/routes/routes.dart +++ b/lib/presentation/core/routes/routes.dart @@ -16,6 +16,7 @@ import 'package:rtu_mirea_app/presentation/pages/profile/profile_detail_page.dar import 'package:rtu_mirea_app/presentation/pages/profile/profile_lectors_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_scores_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_page.dart'; +import 'package:rtu_mirea_app/presentation/pages/schedule/groups_select_page.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart'; @AdaptiveAutoRouter( @@ -27,8 +28,19 @@ import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart'; children: [ AutoRoute( path: 'schedule', - page: SchedulePage, + page: EmptyRouterPage, + name: 'ScheduleRouter', initial: true, + children: [ + AutoRoute( + path: '', + page: SchedulePage, + ), + AutoRoute( + path: 'select-group', + page: GroupsSelectPage, + ), + ], ), AutoRoute( path: 'news', diff --git a/lib/presentation/core/routes/routes.gr.dart b/lib/presentation/core/routes/routes.gr.dart index 6f9fb328..30f2a6aa 100644 --- a/lib/presentation/core/routes/routes.gr.dart +++ b/lib/presentation/core/routes/routes.gr.dart @@ -11,216 +11,353 @@ // ignore_for_file: type=lint // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i17; -import 'package:auto_route/empty_router_widgets.dart' as _i5; -import 'package:flutter/material.dart' as _i18; -import 'package:rtu_mirea_app/domain/entities/news_item.dart' as _i21; -import 'package:rtu_mirea_app/domain/entities/story.dart' as _i20; -import 'package:rtu_mirea_app/domain/entities/user.dart' as _i22; -import 'package:rtu_mirea_app/presentation/core/routes/routes.dart' as _i19; +import 'package:auto_route/auto_route.dart' as _i18; +import 'package:auto_route/empty_router_widgets.dart' as _i4; +import 'package:flutter/material.dart' as _i19; +import 'package:rtu_mirea_app/domain/entities/news_item.dart' as _i22; +import 'package:rtu_mirea_app/domain/entities/story.dart' as _i21; +import 'package:rtu_mirea_app/domain/entities/user.dart' as _i23; +import 'package:rtu_mirea_app/presentation/core/routes/routes.dart' as _i20; import 'package:rtu_mirea_app/presentation/pages/home_page.dart' as _i1; -import 'package:rtu_mirea_app/presentation/pages/login/login_page.dart' as _i10; -import 'package:rtu_mirea_app/presentation/pages/map/map_page.dart' as _i6; +import 'package:rtu_mirea_app/presentation/pages/login/login_page.dart' as _i11; +import 'package:rtu_mirea_app/presentation/pages/map/map_page.dart' as _i5; import 'package:rtu_mirea_app/presentation/pages/news/news_details_page.dart' - as _i8; -import 'package:rtu_mirea_app/presentation/pages/news/news_page.dart' as _i7; + as _i9; +import 'package:rtu_mirea_app/presentation/pages/news/news_page.dart' as _i8; import 'package:rtu_mirea_app/presentation/pages/news/widgets/stories_wrapper.dart' as _i3; import 'package:rtu_mirea_app/presentation/pages/onboarding/onboarding_page.dart' as _i2; import 'package:rtu_mirea_app/presentation/pages/profile/about_app_page.dart' - as _i11; -import 'package:rtu_mirea_app/presentation/pages/profile/profile_announces_page.dart' as _i12; -import 'package:rtu_mirea_app/presentation/pages/profile/profile_attendance_page.dart' +import 'package:rtu_mirea_app/presentation/pages/profile/profile_announces_page.dart' as _i13; -import 'package:rtu_mirea_app/presentation/pages/profile/profile_detail_page.dart' +import 'package:rtu_mirea_app/presentation/pages/profile/profile_attendance_page.dart' as _i14; -import 'package:rtu_mirea_app/presentation/pages/profile/profile_lectors_page.dart' +import 'package:rtu_mirea_app/presentation/pages/profile/profile_detail_page.dart' as _i15; +import 'package:rtu_mirea_app/presentation/pages/profile/profile_lectors_page.dart' + as _i16; import 'package:rtu_mirea_app/presentation/pages/profile/profile_page.dart' - as _i9; + as _i10; import 'package:rtu_mirea_app/presentation/pages/profile/profile_scores_page.dart' - as _i16; + as _i17; +import 'package:rtu_mirea_app/presentation/pages/schedule/groups_select_page.dart' + as _i7; import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart' - as _i4; + as _i6; -class AppRouter extends _i17.RootStackRouter { - AppRouter([_i18.GlobalKey<_i18.NavigatorState>? navigatorKey]) +class AppRouter extends _i18.RootStackRouter { + AppRouter([_i19.GlobalKey<_i19.NavigatorState>? navigatorKey]) : super(navigatorKey); @override - final Map pagesMap = { + final Map pagesMap = { HomeRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i1.HomePage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i1.HomePage(), + ); }, OnBoardingRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i2.OnBoardingPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i2.OnBoardingPage(), + ); }, StoriesWrapperRoute.name: (routeData) { final args = routeData.argsAs(); - return _i17.CustomPage( - routeData: routeData, - child: _i3.StoriesWrapper( - key: args.key, - stories: args.stories, - storyIndex: args.storyIndex), - customRouteBuilder: _i19.transparentRoute, - opaque: false, - barrierDismissible: false); + return _i18.CustomPage( + routeData: routeData, + child: _i3.StoriesWrapper( + key: args.key, + stories: args.stories, + storyIndex: args.storyIndex, + ), + customRouteBuilder: _i20.transparentRoute, + opaque: false, + barrierDismissible: false, + ); }, - ScheduleRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i4.SchedulePage()); + ScheduleRouter.name: (routeData) { + return _i18.AdaptivePage( + routeData: routeData, + child: const _i4.EmptyRouterPage(), + ); }, NewsRouter.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i5.EmptyRouterPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i4.EmptyRouterPage(), + ); }, MapRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i6.MapPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i5.MapPage(), + ); }, ProfileRouter.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i5.EmptyRouterPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i4.EmptyRouterPage(), + ); + }, + ScheduleRoute.name: (routeData) { + return _i18.AdaptivePage( + routeData: routeData, + child: const _i6.SchedulePage(), + ); + }, + GroupsSelectRoute.name: (routeData) { + return _i18.AdaptivePage( + routeData: routeData, + child: const _i7.GroupsSelectPage(), + ); }, NewsRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i7.NewsPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i8.NewsPage(), + ); }, NewsDetailsRoute.name: (routeData) { final args = routeData.argsAs(); - return _i17.AdaptivePage( - routeData: routeData, - child: _i8.NewsDetailsPage(key: args.key, newsItem: args.newsItem)); + return _i18.AdaptivePage( + routeData: routeData, + child: _i9.NewsDetailsPage( + key: args.key, + newsItem: args.newsItem, + ), + ); }, ProfileRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i9.ProfilePage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i10.ProfilePage(), + ); }, LoginRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i10.LoginPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i11.LoginPage(), + ); }, AboutAppRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i11.AboutAppPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i12.AboutAppPage(), + ); }, ProfileAnnouncesRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i12.ProfileAnnouncesPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i13.ProfileAnnouncesPage(), + ); }, ProfileAttendanceRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i13.ProfileAttendancePage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i14.ProfileAttendancePage(), + ); }, ProfileDetailRoute.name: (routeData) { final args = routeData.argsAs(); - return _i17.AdaptivePage( - routeData: routeData, - child: _i14.ProfileDetailPage(key: args.key, user: args.user)); + return _i18.AdaptivePage( + routeData: routeData, + child: _i15.ProfileDetailPage( + key: args.key, + user: args.user, + ), + ); }, ProfileLectrosRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i15.ProfileLectrosPage()); + return _i18.AdaptivePage( + routeData: routeData, + child: const _i16.ProfileLectrosPage(), + ); }, ProfileScoresRoute.name: (routeData) { - return _i17.AdaptivePage( - routeData: routeData, child: const _i16.ProfileScoresPage()); - } + return _i18.AdaptivePage( + routeData: routeData, + child: const _i17.ProfileScoresPage(), + ); + }, }; @override - List<_i17.RouteConfig> get routes => [ - _i17.RouteConfig(HomeRoute.name, path: '/', children: [ - _i17.RouteConfig('#redirect', + List<_i18.RouteConfig> get routes => [ + _i18.RouteConfig( + HomeRoute.name, + path: '/', + children: [ + _i18.RouteConfig( + '#redirect', path: '', parent: HomeRoute.name, redirectTo: 'schedule', - fullMatch: true), - _i17.RouteConfig(ScheduleRoute.name, - path: 'schedule', parent: HomeRoute.name), - _i17.RouteConfig(NewsRouter.name, + fullMatch: true, + ), + _i18.RouteConfig( + ScheduleRouter.name, + path: 'schedule', + parent: HomeRoute.name, + children: [ + _i18.RouteConfig( + ScheduleRoute.name, + path: '', + parent: ScheduleRouter.name, + ), + _i18.RouteConfig( + GroupsSelectRoute.name, + path: 'select-group', + parent: ScheduleRouter.name, + ), + ], + ), + _i18.RouteConfig( + NewsRouter.name, path: 'news', parent: HomeRoute.name, children: [ - _i17.RouteConfig(NewsRoute.name, - path: '', parent: NewsRouter.name), - _i17.RouteConfig(NewsDetailsRoute.name, - path: 'details', parent: NewsRouter.name) - ]), - _i17.RouteConfig(MapRoute.name, path: 'map', parent: HomeRoute.name), - _i17.RouteConfig(ProfileRouter.name, + _i18.RouteConfig( + NewsRoute.name, + path: '', + parent: NewsRouter.name, + ), + _i18.RouteConfig( + NewsDetailsRoute.name, + path: 'details', + parent: NewsRouter.name, + ), + ], + ), + _i18.RouteConfig( + MapRoute.name, + path: 'map', + parent: HomeRoute.name, + ), + _i18.RouteConfig( + ProfileRouter.name, path: 'profile', parent: HomeRoute.name, children: [ - _i17.RouteConfig(ProfileRoute.name, - path: '', parent: ProfileRouter.name), - _i17.RouteConfig(LoginRoute.name, - path: 'login', parent: ProfileRouter.name), - _i17.RouteConfig(AboutAppRoute.name, - path: 'about', parent: ProfileRouter.name), - _i17.RouteConfig(ProfileAnnouncesRoute.name, - path: 'announces', parent: ProfileRouter.name), - _i17.RouteConfig(ProfileAttendanceRoute.name, - path: 'attendance', parent: ProfileRouter.name), - _i17.RouteConfig(ProfileDetailRoute.name, - path: 'details', parent: ProfileRouter.name), - _i17.RouteConfig(ProfileLectrosRoute.name, - path: 'lectors', parent: ProfileRouter.name), - _i17.RouteConfig(ProfileScoresRoute.name, - path: 'scores', parent: ProfileRouter.name) - ]) - ]), - _i17.RouteConfig(OnBoardingRoute.name, path: '/onboarding'), - _i17.RouteConfig(StoriesWrapperRoute.name, path: '/story'), - _i17.RouteConfig('*#redirect', - path: '*', redirectTo: '/', fullMatch: true) + _i18.RouteConfig( + ProfileRoute.name, + path: '', + parent: ProfileRouter.name, + ), + _i18.RouteConfig( + LoginRoute.name, + path: 'login', + parent: ProfileRouter.name, + ), + _i18.RouteConfig( + AboutAppRoute.name, + path: 'about', + parent: ProfileRouter.name, + ), + _i18.RouteConfig( + ProfileAnnouncesRoute.name, + path: 'announces', + parent: ProfileRouter.name, + ), + _i18.RouteConfig( + ProfileAttendanceRoute.name, + path: 'attendance', + parent: ProfileRouter.name, + ), + _i18.RouteConfig( + ProfileDetailRoute.name, + path: 'details', + parent: ProfileRouter.name, + ), + _i18.RouteConfig( + ProfileLectrosRoute.name, + path: 'lectors', + parent: ProfileRouter.name, + ), + _i18.RouteConfig( + ProfileScoresRoute.name, + path: 'scores', + parent: ProfileRouter.name, + ), + ], + ), + ], + ), + _i18.RouteConfig( + OnBoardingRoute.name, + path: '/onboarding', + ), + _i18.RouteConfig( + StoriesWrapperRoute.name, + path: '/story', + ), + _i18.RouteConfig( + '*#redirect', + path: '*', + redirectTo: '/', + fullMatch: true, + ), ]; } /// generated route for /// [_i1.HomePage] -class HomeRoute extends _i17.PageRouteInfo { - const HomeRoute({List<_i17.PageRouteInfo>? children}) - : super(HomeRoute.name, path: '/', initialChildren: children); +class HomeRoute extends _i18.PageRouteInfo { + const HomeRoute({List<_i18.PageRouteInfo>? children}) + : super( + HomeRoute.name, + path: '/', + initialChildren: children, + ); static const String name = 'HomeRoute'; } /// generated route for /// [_i2.OnBoardingPage] -class OnBoardingRoute extends _i17.PageRouteInfo { - const OnBoardingRoute() : super(OnBoardingRoute.name, path: '/onboarding'); +class OnBoardingRoute extends _i18.PageRouteInfo { + const OnBoardingRoute() + : super( + OnBoardingRoute.name, + path: '/onboarding', + ); static const String name = 'OnBoardingRoute'; } /// generated route for /// [_i3.StoriesWrapper] -class StoriesWrapperRoute extends _i17.PageRouteInfo { - StoriesWrapperRoute( - {_i18.Key? key, - required List<_i20.Story> stories, - required int storyIndex}) - : super(StoriesWrapperRoute.name, - path: '/story', - args: StoriesWrapperRouteArgs( - key: key, stories: stories, storyIndex: storyIndex)); +class StoriesWrapperRoute extends _i18.PageRouteInfo { + StoriesWrapperRoute({ + _i19.Key? key, + required List<_i21.Story> stories, + required int storyIndex, + }) : super( + StoriesWrapperRoute.name, + path: '/story', + args: StoriesWrapperRouteArgs( + key: key, + stories: stories, + storyIndex: storyIndex, + ), + ); static const String name = 'StoriesWrapperRoute'; } class StoriesWrapperRouteArgs { - const StoriesWrapperRouteArgs( - {this.key, required this.stories, required this.storyIndex}); + const StoriesWrapperRouteArgs({ + this.key, + required this.stories, + required this.storyIndex, + }); - final _i18.Key? key; + final _i19.Key? key; - final List<_i20.Story> stories; + final List<_i21.Story> stories; final int storyIndex; @@ -231,64 +368,119 @@ class StoriesWrapperRouteArgs { } /// generated route for -/// [_i4.SchedulePage] -class ScheduleRoute extends _i17.PageRouteInfo { - const ScheduleRoute() : super(ScheduleRoute.name, path: 'schedule'); - - static const String name = 'ScheduleRoute'; +/// [_i4.EmptyRouterPage] +class ScheduleRouter extends _i18.PageRouteInfo { + const ScheduleRouter({List<_i18.PageRouteInfo>? children}) + : super( + ScheduleRouter.name, + path: 'schedule', + initialChildren: children, + ); + + static const String name = 'ScheduleRouter'; } /// generated route for -/// [_i5.EmptyRouterPage] -class NewsRouter extends _i17.PageRouteInfo { - const NewsRouter({List<_i17.PageRouteInfo>? children}) - : super(NewsRouter.name, path: 'news', initialChildren: children); +/// [_i4.EmptyRouterPage] +class NewsRouter extends _i18.PageRouteInfo { + const NewsRouter({List<_i18.PageRouteInfo>? children}) + : super( + NewsRouter.name, + path: 'news', + initialChildren: children, + ); static const String name = 'NewsRouter'; } /// generated route for -/// [_i6.MapPage] -class MapRoute extends _i17.PageRouteInfo { - const MapRoute() : super(MapRoute.name, path: 'map'); +/// [_i5.MapPage] +class MapRoute extends _i18.PageRouteInfo { + const MapRoute() + : super( + MapRoute.name, + path: 'map', + ); static const String name = 'MapRoute'; } /// generated route for -/// [_i5.EmptyRouterPage] -class ProfileRouter extends _i17.PageRouteInfo { - const ProfileRouter({List<_i17.PageRouteInfo>? children}) - : super(ProfileRouter.name, path: 'profile', initialChildren: children); +/// [_i4.EmptyRouterPage] +class ProfileRouter extends _i18.PageRouteInfo { + const ProfileRouter({List<_i18.PageRouteInfo>? children}) + : super( + ProfileRouter.name, + path: 'profile', + initialChildren: children, + ); static const String name = 'ProfileRouter'; } /// generated route for -/// [_i7.NewsPage] -class NewsRoute extends _i17.PageRouteInfo { - const NewsRoute() : super(NewsRoute.name, path: ''); +/// [_i6.SchedulePage] +class ScheduleRoute extends _i18.PageRouteInfo { + const ScheduleRoute() + : super( + ScheduleRoute.name, + path: '', + ); + + static const String name = 'ScheduleRoute'; +} + +/// generated route for +/// [_i7.GroupsSelectPage] +class GroupsSelectRoute extends _i18.PageRouteInfo { + const GroupsSelectRoute() + : super( + GroupsSelectRoute.name, + path: 'select-group', + ); + + static const String name = 'GroupsSelectRoute'; +} + +/// generated route for +/// [_i8.NewsPage] +class NewsRoute extends _i18.PageRouteInfo { + const NewsRoute() + : super( + NewsRoute.name, + path: '', + ); static const String name = 'NewsRoute'; } /// generated route for -/// [_i8.NewsDetailsPage] -class NewsDetailsRoute extends _i17.PageRouteInfo { - NewsDetailsRoute({_i18.Key? key, required _i21.NewsItem newsItem}) - : super(NewsDetailsRoute.name, - path: 'details', - args: NewsDetailsRouteArgs(key: key, newsItem: newsItem)); +/// [_i9.NewsDetailsPage] +class NewsDetailsRoute extends _i18.PageRouteInfo { + NewsDetailsRoute({ + _i19.Key? key, + required _i22.NewsItem newsItem, + }) : super( + NewsDetailsRoute.name, + path: 'details', + args: NewsDetailsRouteArgs( + key: key, + newsItem: newsItem, + ), + ); static const String name = 'NewsDetailsRoute'; } class NewsDetailsRouteArgs { - const NewsDetailsRouteArgs({this.key, required this.newsItem}); + const NewsDetailsRouteArgs({ + this.key, + required this.newsItem, + }); - final _i18.Key? key; + final _i19.Key? key; - final _i21.NewsItem newsItem; + final _i22.NewsItem newsItem; @override String toString() { @@ -297,64 +489,92 @@ class NewsDetailsRouteArgs { } /// generated route for -/// [_i9.ProfilePage] -class ProfileRoute extends _i17.PageRouteInfo { - const ProfileRoute() : super(ProfileRoute.name, path: ''); +/// [_i10.ProfilePage] +class ProfileRoute extends _i18.PageRouteInfo { + const ProfileRoute() + : super( + ProfileRoute.name, + path: '', + ); static const String name = 'ProfileRoute'; } /// generated route for -/// [_i10.LoginPage] -class LoginRoute extends _i17.PageRouteInfo { - const LoginRoute() : super(LoginRoute.name, path: 'login'); +/// [_i11.LoginPage] +class LoginRoute extends _i18.PageRouteInfo { + const LoginRoute() + : super( + LoginRoute.name, + path: 'login', + ); static const String name = 'LoginRoute'; } /// generated route for -/// [_i11.AboutAppPage] -class AboutAppRoute extends _i17.PageRouteInfo { - const AboutAppRoute() : super(AboutAppRoute.name, path: 'about'); +/// [_i12.AboutAppPage] +class AboutAppRoute extends _i18.PageRouteInfo { + const AboutAppRoute() + : super( + AboutAppRoute.name, + path: 'about', + ); static const String name = 'AboutAppRoute'; } /// generated route for -/// [_i12.ProfileAnnouncesPage] -class ProfileAnnouncesRoute extends _i17.PageRouteInfo { +/// [_i13.ProfileAnnouncesPage] +class ProfileAnnouncesRoute extends _i18.PageRouteInfo { const ProfileAnnouncesRoute() - : super(ProfileAnnouncesRoute.name, path: 'announces'); + : super( + ProfileAnnouncesRoute.name, + path: 'announces', + ); static const String name = 'ProfileAnnouncesRoute'; } /// generated route for -/// [_i13.ProfileAttendancePage] -class ProfileAttendanceRoute extends _i17.PageRouteInfo { +/// [_i14.ProfileAttendancePage] +class ProfileAttendanceRoute extends _i18.PageRouteInfo { const ProfileAttendanceRoute() - : super(ProfileAttendanceRoute.name, path: 'attendance'); + : super( + ProfileAttendanceRoute.name, + path: 'attendance', + ); static const String name = 'ProfileAttendanceRoute'; } /// generated route for -/// [_i14.ProfileDetailPage] -class ProfileDetailRoute extends _i17.PageRouteInfo { - ProfileDetailRoute({_i18.Key? key, required _i22.User user}) - : super(ProfileDetailRoute.name, - path: 'details', - args: ProfileDetailRouteArgs(key: key, user: user)); +/// [_i15.ProfileDetailPage] +class ProfileDetailRoute extends _i18.PageRouteInfo { + ProfileDetailRoute({ + _i19.Key? key, + required _i23.User user, + }) : super( + ProfileDetailRoute.name, + path: 'details', + args: ProfileDetailRouteArgs( + key: key, + user: user, + ), + ); static const String name = 'ProfileDetailRoute'; } class ProfileDetailRouteArgs { - const ProfileDetailRouteArgs({this.key, required this.user}); + const ProfileDetailRouteArgs({ + this.key, + required this.user, + }); - final _i18.Key? key; + final _i19.Key? key; - final _i22.User user; + final _i23.User user; @override String toString() { @@ -363,18 +583,25 @@ class ProfileDetailRouteArgs { } /// generated route for -/// [_i15.ProfileLectrosPage] -class ProfileLectrosRoute extends _i17.PageRouteInfo { +/// [_i16.ProfileLectrosPage] +class ProfileLectrosRoute extends _i18.PageRouteInfo { const ProfileLectrosRoute() - : super(ProfileLectrosRoute.name, path: 'lectors'); + : super( + ProfileLectrosRoute.name, + path: 'lectors', + ); static const String name = 'ProfileLectrosRoute'; } /// generated route for -/// [_i16.ProfileScoresPage] -class ProfileScoresRoute extends _i17.PageRouteInfo { - const ProfileScoresRoute() : super(ProfileScoresRoute.name, path: 'scores'); +/// [_i17.ProfileScoresPage] +class ProfileScoresRoute extends _i18.PageRouteInfo { + const ProfileScoresRoute() + : super( + ProfileScoresRoute.name, + path: 'scores', + ); static const String name = 'ProfileScoresRoute'; } diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index aa4f74a9..d9c2959e 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -16,7 +16,7 @@ class HomePage extends StatelessWidget { return AutoTabsRouter( routes: const [ NewsRouter(), - ScheduleRoute(), + ScheduleRouter(), MapRoute(), ProfileRouter() ], diff --git a/lib/presentation/pages/schedule/groups_select_page.dart b/lib/presentation/pages/schedule/groups_select_page.dart new file mode 100644 index 00000000..7e3db520 --- /dev/null +++ b/lib/presentation/pages/schedule/groups_select_page.dart @@ -0,0 +1,279 @@ +import 'dart:convert'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; +import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; + +class GroupsSelectPage extends StatefulWidget { + const GroupsSelectPage({Key? key}) : super(key: key); + + @override + State createState() => _GroupsSelectPageState(); +} + +class _GroupTextFormatter extends TextInputFormatter { + final groupMask = '0000-00-00'; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + final result = StringBuffer(); + var text = + newValue.text.replaceAll('-', '').replaceAll(' ', '').toUpperCase(); + if (text.length > 1 && oldValue.text.length > newValue.text.length) { + text = text.substring(0, text.length); + } + var readPosition = 0; + for (var i = 0; i < groupMask.length; i++) { + if (readPosition > text.length - 1) { + break; + } + var curSymbol = groupMask[i]; + if (isZeroSymbol(curSymbol)) { + curSymbol = text[readPosition]; + readPosition++; + } + result.write(curSymbol); + } + final textResult = result.toString(); + return TextEditingValue( + text: textResult, + selection: TextSelection.collapsed( + offset: textResult.length, + ), + ); + } + + bool isZeroSymbol(String symbol) => symbol == "0"; +} + +class _GroupsSelectPageState extends State { + String _getInstituteByGroup( + String group, Map> groupsByInstitute) { + final groupNameOnly = group.split('-')[0]; + + String institute = ''; + + for (final instituteName in groupsByInstitute.keys) { + for (final groupName in groupsByInstitute[instituteName]!) { + if (groupName.contains(groupNameOnly)) { + institute = instituteName; + break; + } + } + } + + return institute; + } + + Color _getInstituteColor( + String group, Map> groupsByInstitute) { + switch (_getInstituteByGroup(group, groupsByInstitute)) { + case 'ИИТ': + return const Color(0xff697582); + case 'ИИИ': + return const Color(0xff36933e); + case 'ИКБ': + return const Color(0xFF163c4f); + case 'ИТУ': + return const Color(0xffbd5435); + case 'КПК': + return const Color(0xffed7f25); + case 'ИТХТ': + return const Color(0xffa1448d); + case 'ИРИ': + return const Color(0xff490063); + case 'ИПТИП': + return const Color(0xFFFFDD72); + default: + return const Color(0xff697582); + } + } + + bool _isFirstOpen = true; + List _filteredGroups = []; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: DarkThemeColors.background01, + appBar: AppBar( + backgroundColor: DarkThemeColors.background01, + title: const Text('Выбор группы'), + ), + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + if (state is ScheduleLoaded || state is ScheduleActiveGroupEmpty) { + final groups = state is ScheduleLoaded + ? state.groups + : (state as ScheduleActiveGroupEmpty).groups; + final groupsByInstitute = state is ScheduleLoaded + ? state.groupsByInstitute + : (state as ScheduleActiveGroupEmpty).groupsByInstitute; + + // On first open, set groups to all groups + if (groups.isNotEmpty && _isFirstOpen) { + _filteredGroups = groups; + _isFirstOpen = false; + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox(height: 16), + Container( + height: 48, + decoration: BoxDecoration( + color: DarkThemeColors.background02, + borderRadius: BorderRadius.circular(12), + ), + child: TextField( + onChanged: (value) { + setState(() { + _filteredGroups = groups + .where((group) => + group.toLowerCase().contains(value)) + .toList(); + }); + }, + style: DarkTextTheme.titleS.copyWith( + color: DarkThemeColors.deactive, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + border: InputBorder.none, + hintText: 'Поиск', + hintStyle: DarkTextTheme.titleS.copyWith( + color: DarkThemeColors.deactive, + ), + prefixIcon: Padding( + padding: const EdgeInsets.only(right: 8, left: 16), + child: SvgPicture.asset( + 'assets/icons/search.svg', + color: Colors.white, + width: 24, + height: 24, + ), + ), + prefixIconConstraints: const BoxConstraints( + maxWidth: 48, + maxHeight: 48, + ), + ), + inputFormatters: [ + _GroupTextFormatter(), + ], + ), + ), + const SizedBox(height: 16), + if (_filteredGroups.isNotEmpty) + Expanded( + child: ListView.separated( + itemCount: groups.length, + separatorBuilder: (context, index) => const SizedBox( + height: 10, + ), + itemBuilder: (context, index) { + return _GroupListTile( + group: _filteredGroups[index], + institute: _getInstituteByGroup( + _filteredGroups[index], groupsByInstitute), + color: _getInstituteColor( + _filteredGroups[index], groupsByInstitute), + onTap: () { + context.read().add( + ScheduleSetActiveGroupEvent( + group: _filteredGroups[index]), + ); + context.router.pop(); + }, + ); + }, + ), + ), + ], + ), + ); + } + return const Center( + child: CircularProgressIndicator(), + ); + }, + ), + ), + ); + } +} + +class _GroupListTile extends StatelessWidget { + const _GroupListTile({ + Key? key, + required this.group, + required this.color, + required this.onTap, + required this.institute, + }) : super(key: key); + + final String group; + final String institute; + final Color color; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: DarkThemeColors.background03, + ), + child: ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + institute, + style: DarkTextTheme.titleS.copyWith( + color: Colors.white, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ), + title: Text( + group, + style: DarkTextTheme.titleM, + ), + trailing: const Icon( + Icons.chevron_right, + size: 24, + ), + onTap: () { + context + .read() + .add(ScheduleSetActiveGroupEvent(group: group)); + context.router.pop(); + }, + ), + ); + } +} diff --git a/lib/presentation/pages/schedule/schedule_page.dart b/lib/presentation/pages/schedule/schedule_page.dart index 112f8a73..4ad095fd 100644 --- a/lib/presentation/pages/schedule/schedule_page.dart +++ b/lib/presentation/pages/schedule/schedule_page.dart @@ -113,7 +113,7 @@ class _SchedulePageState extends State { onPressed: () { context .read() - .add(ScheduleSetActiveGroupEvent(group)); + .add(ScheduleSetActiveGroupEvent(group: group)); }, shape: const CircleBorder(), constraints: diff --git a/lib/presentation/pages/schedule/widgets/autocomplete_group_selector.dart b/lib/presentation/pages/schedule/widgets/autocomplete_group_selector.dart deleted file mode 100644 index ef211133..00000000 --- a/lib/presentation/pages/schedule/widgets/autocomplete_group_selector.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; - -class GroupTextFormatter extends TextInputFormatter { - final groupMask = '0000-00-00'; - - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, TextEditingValue newValue) { - final result = StringBuffer(); - var text = - newValue.text.replaceAll('-', '').replaceAll(' ', '').toUpperCase(); - if (text.length > 1 && oldValue.text.length > newValue.text.length) { - text = text.substring(0, text.length); - } - var readPosition = 0; - for (var i = 0; i < groupMask.length; i++) { - if (readPosition > text.length - 1) { - break; - } - var curSymbol = groupMask[i]; - if (isZeroSymbol(curSymbol)) { - curSymbol = text[readPosition]; - readPosition++; - } - result.write(curSymbol); - } - final textResult = result.toString(); - return TextEditingValue( - text: textResult, - selection: TextSelection.collapsed( - offset: textResult.length, - ), - ); - } - - bool isZeroSymbol(String symbol) => symbol == "0"; -} - -class AutocompleteGroupSelector extends StatefulWidget { - const AutocompleteGroupSelector({Key? key}) : super(key: key); - - @override - State createState() => _AutocompleteGroupSelectorState(); -} - -class _AutocompleteGroupSelectorState extends State { - final TextInputType _keyboardType = TextInputType.text; - late TextEditingController _inputController; - - @override - void initState() { - _inputController = TextEditingController(); - super.initState(); - } - - @override - void dispose() { - _inputController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (prevState, currentState) => prevState != currentState, - builder: (context, state) { - return TypeAheadField( - hideOnError: false, - textFieldConfiguration: TextFieldConfiguration( - focusNode: FocusNode(), - controller: _inputController, - autofocus: false, - style: DarkTextTheme.titleM, - keyboardType: _keyboardType, - autocorrect: false, - textCapitalization: TextCapitalization.characters, - inputFormatters: [ - GroupTextFormatter(), - ], - decoration: InputDecoration( - errorText: state is ScheduleGroupNotFound - ? 'заданная группа не найдена' - : null, - hintText: 'АБВГ-12-34', - hintStyle: DarkTextTheme.titleM - .copyWith(color: DarkThemeColors.deactive), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: DarkThemeColors.colorful05), - ), - ), - ), - itemBuilder: (context, suggestion) => Container( - color: DarkThemeColors.background03, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Text(suggestion.toString()), - ), - ), - noItemsFoundBuilder: (context) => Container( - alignment: Alignment.center, - color: DarkThemeColors.background03, - child: Text('Группы не найдены', style: DarkTextTheme.titleM), - ), - suggestionsCallback: (search) async { - context.read().add(ScheduleUpdateGroupSuggestionEvent( - suggestion: search.toUpperCase())); - return ScheduleBloc.groupsList - .where((group) => - group.toUpperCase().contains(search.toUpperCase())) - .toList(); - }, - keepSuggestionsOnSuggestionSelected: true, - onSuggestionSelected: (String suggestion) { - _inputController.text = suggestion; - context.read().add( - ScheduleUpdateGroupSuggestionEvent(suggestion: suggestion)); - }, - ); - }, - ); - } -} diff --git a/lib/presentation/pages/schedule/widgets/schedule_settings_modal.dart b/lib/presentation/pages/schedule/widgets/schedule_settings_modal.dart index 82f7834c..24c560cc 100644 --- a/lib/presentation/pages/schedule/widgets/schedule_settings_modal.dart +++ b/lib/presentation/pages/schedule/widgets/schedule_settings_modal.dart @@ -1,8 +1,7 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; -import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/autocomplete_group_selector.dart'; +import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/keyboard_positioned.dart'; @@ -16,7 +15,7 @@ class ScheduleSettingsModal extends StatelessWidget { Widget build(BuildContext context) { return KeyboardPositioned( child: Container( - height: MediaQuery.of(context).size.height * 0.95, + height: MediaQuery.of(context).size.height * 0.85, decoration: const BoxDecoration( gradient: LinearGradient( colors: [ @@ -51,31 +50,16 @@ class ScheduleSettingsModal extends StatelessWidget { ), ), Text( - isFirstRun ? "Настройте расписание" : "Выберите группу", + "Настройте расписание", style: DarkTextTheme.h5, ), const SizedBox(height: 8), Text( - isFirstRun - ? "Кажется, что это ваш первый запуск. Установите вашу учебную группу, чтобы начать пользоваться расписанием" - : "Введите название группы, для которой вы хотите скачать расписание", + "Кажется, что это ваш первый запуск. Установите вашу учебную группу, чтобы начать пользоваться расписанием", style: DarkTextTheme.captionL .copyWith(color: DarkThemeColors.deactive), textAlign: TextAlign.center, ), - const SizedBox(height: 24), - Row( - children: [ - Text( - "Ваша группа".toUpperCase(), - style: DarkTextTheme.chip - .copyWith(color: DarkThemeColors.deactiveDarker), - textAlign: TextAlign.left, - ) - ], - ), - const SizedBox(height: 8), - const AutocompleteGroupSelector(), const SizedBox(height: 32), ConstrainedBox( constraints: const BoxConstraints.tightFor( @@ -91,9 +75,10 @@ class ScheduleSettingsModal extends StatelessWidget { ), ), onPressed: () { - context - .read() - .add(const ScheduleSetActiveGroupEvent()); + // Close modal + Navigator.of(context).pop(); + + context.router.push(const GroupsSelectRoute()); }, child: Text( 'Начать', diff --git a/lib/presentation/theme.dart b/lib/presentation/theme.dart index 0028df31..48f564cd 100644 --- a/lib/presentation/theme.dart +++ b/lib/presentation/theme.dart @@ -52,7 +52,7 @@ final darkTheme = ThemeData.dark().copyWith( backgroundColor: DarkThemeColors.background01, appBarTheme: AppBarTheme( titleSpacing: 24, - backgroundColor: Colors.transparent, + backgroundColor: DarkThemeColors.background01, shadowColor: Colors.transparent, titleTextStyle: DarkTextTheme.title, ), diff --git a/pubspec.yaml b/pubspec.yaml index 0711464c..2fe50a37 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -220,6 +220,7 @@ flutter: - assets/images/ - assets/icons/ - assets/map/ + - assets/json/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. From 762a5e34f1f84d0e662100740455a1d9fe5ce1de Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:59:57 +0300 Subject: [PATCH 23/38] dev: Change package version (#271) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2fe50a37..8fd1a0f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ publish_to: 'none' # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.2+8 +version: 1.2.3+9 environment: sdk: ">=2.12.0 <3.0.0" From 73dd0964d460535fc32c44bd0f9f9db79f525f0e Mon Sep 17 00:00:00 2001 From: Dragonprod Date: Fri, 27 Jan 2023 16:19:09 +0300 Subject: [PATCH 24/38] fix: oauth scheme --- ios/Flutter/AppFrameworkInfo.plist | 2 +- ios/Podfile | 2 +- ios/Runner/Info.plist | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 8d4492f9..9625e105 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/ios/Podfile b/ios/Podfile index 5ef04a85..3e32eec2 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -39,4 +39,4 @@ post_install do |installer| flutter_additional_ios_build_settings(target) end end -$FirebaseAnalyticsWithoutAdIdSupport = true \ No newline at end of file +$FirebaseAnalyticsWithoutAdIdSupport = true diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index c3d82104..69abe59a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -27,7 +27,7 @@ Viewer CFBundleURLSchemes - ninja.mirea.mireaapp://oauth2redirect + ninja.mirea.mireaapp @@ -59,5 +59,7 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + From 6ee883a687b41e3e13ffbf5dea10dca68848df03 Mon Sep 17 00:00:00 2001 From: Roman <40907255+witelokk@users.noreply.github.com> Date: Sat, 28 Jan 2023 16:22:03 +0300 Subject: [PATCH 25/38] ui: fix navbar color (#272) --- lib/main.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index fb4da216..0fa9c465 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,6 +22,7 @@ import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/stories_bloc/stories_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/update_info_bloc/update_info_bloc.dart'; +import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:intl/intl_standalone.dart'; @@ -80,6 +81,7 @@ class App extends StatelessWidget { // deleting the system status bar color SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, + systemNavigationBarColor: DarkThemeColors.background01 )); final appRouter = AppRouter(); From f4480dd009ad4eba400f55dabe24e1488b8ebe1a Mon Sep 17 00:00:00 2001 From: Roman <40907255+witelokk@users.noreply.github.com> Date: Sun, 29 Jan 2023 12:26:21 +0300 Subject: [PATCH 26/38] feature: android 13 themed icon support (#273) --- .../src/main/res/drawable/monochrome_icon.png | Bin 0 -> 15902 bytes .../main/res/mipmap-anydpi-v26/ic_launcher.xml | 1 + 2 files changed, 1 insertion(+) create mode 100644 android/app/src/main/res/drawable/monochrome_icon.png diff --git a/android/app/src/main/res/drawable/monochrome_icon.png b/android/app/src/main/res/drawable/monochrome_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6e2474bf3c782e420921c9edf0a945e5b82bce34 GIT binary patch literal 15902 zcmeHu2RzmP|L^+@duC*FLQxq9$6m)CWhSY_F^-X$oq1%H6s0Mvl9oi0G7n0rgp81# z$|z))aNnQPcm3}D-^c&{?!Eu}c-+UM503ZewO`NI>pec72Pdp8P1u+PnE?Q7W~PR= z0AS$|3mEC)uQ-vN2>@&|i?nkHv2_W@1qKCpc>9uZAx8trIC6xy2LKU$AF~dINp86IFx_$D4pi^$$?uF{)`a|Ez z*B<2tydD}FPoIeKo!J&CckcDI7pHWjCR_9_*`rTUJhuX2( zHIh$)>n!Kimt`WXSUC2iX(o*wwb-u1YPyu^DALXU;R4`uL_M-TrjzmWiCqsj0<|&vNHluLm94c=hs$_RQ!^>uN;A+wb9n z6?wrAbE-VU6oQ_0Ek3z@@m{}Xlf{LS8&?`1PA26w@C4Oq4xK7;D!LK-y?L2g+pr;2 zpIy*c&1GncnT0#JUyfN-q4?5t;|0m3$Nv2fVp6!bE*9M0^e(h9cTaTr)_!u8^K@~y z!+wl`8|eYnq;~CV?P`2*5s&`t*RFj%yw}x*lw^XIUgBLj-*nvmsIl^Kb?xFazdX6*DU4n?|aC>=# zE8g1NE-g*{hPm%8$qSz(xTf12#M4DuQA*-Awif!nk%HKrpY_*G{m0s*Z;#oU%i128 zAa~jY%a+^UrcX6E>n>$<=Qv?P@xb9H=k2dN?2Z)DUc!1^aH?A9juJ~yW>DhL(dNfD zr75*&oxBqI(zmR&)HLt>zWt%ihwt9MYLj_P?7-fK-tS5qx1A3gB)^lb`da&ZOP6Jk zR9?{JnGQ?c56XvIOJY~njXN8Z4u3O#N&nKPGh)Y_q7hzg`(`Ed=0og;jxt!ukcXGP zZw@-WS^ebY8LIP+O%f-c?M^UCWBerIsy?zcOioAq@R?Pc{_L@#(ajr#pf~!D^_NzS+Q zmKI*&k{@e+@-v)oZu;r7Ic7Psa&>xhZD0Myqq4fehJBiWLjw&PH?$4PnwVIdp>up^pzI0mcp%kXrd-MuW zUbuW^yj8Jx>v-C(ufb6Tfr@)$lDkzbUoE~4dT4IOK5P5f^wZ_C*=P$UE)MI+my@?c zT1aZ!5|5pEk)!hYWlX2Q(K5^32c<_pxk?{d=1sqslD3<2nw8_tbZd_MR3qI zbT^(}_k$#EX61ms=B_e>Ps~&!3^A}eS0!ZLeq1{tjC|T|)oah2tV89=$807{bp-5Q zJx^$fS`oKwSPYTd7~kP@Q)wRW!)N^2EBwQ`>9-@QO95?b8k6L-Q>H5CwtU(_A5)&V z`Z1o#vWH=mt9mTuWoe?&mu+IG2OZLC;optDqFS04w!CVM9WGBeJA~P0U)UC*xWM(U zxku%}kV32IxzZ@MEhY3{#$>!pW=EJp)9;OUS&CjQKK3!qS2wXWG@wfBi@sEB3rp?+ zhkov>37S`$`j?oEUn;j|>~0wO^0bL0GLKas6yGnd`6hSLzvxC_$E_EGm3JP=dFbt& z!ddMRyLr|@e>2v+psUj(K1N8XGYOlaGM_wW;u$+q%9T`;_9TKSD%|;#d8l};K3Vt0 zK~=3M0`?+|WxhI1*BvSsNY9;^x5%9h-C33<;BjXvKk=gzxL@Iw*k2`4lGF%QxkS{o3yrjJ)2Y7 zwwiKkdD5+`nCq~@c^9?eO~Szo1NLu|n6*#O>G^Dam{_>KdqELnwfU^KCDHY;!`&of z5vH7kM6yvi`qm8>c5`W*jcvFo@6-x0`$&Okg1TLMlj`AS9?2`|ob}^5t^_p{P1sK-t|>K=;DyO1+3^g;QY*L1ewmj{RBOxvRw6u^qb_gs#Npx` z#|0Al(8O(A#?(_rypNoelT`Lm_ezGzn&!lEktxezF5Tysbf|evx1}ocN_&NPAH&b! zE#|EPoi`og+dIxmtv=tI=Co`2UIfNAL+Dibj`6vY8+16Uus!*H6V9u@OW)G?gJGMQi?HriHzCW{p7v)G#|qbSp5{Gm?hp)W z+fIKN=ef_I*nPXq9aU3B(ML9!eAjn>3U>3N3+y{zED(M}i?^)HiZa%7#+b#9hv|4M zz5exw8cH*}!-Rj>ns2=G9J3~p= zzPlf}($1b*u5fZL-)RfZ)odf#Ocmc8J>aFici_BO_q7BrO@jKf>vStR#c1tK0~DsO z{3`hE2iy+vyQT*U_S1#&&0aOQ7D8CNYIE&a;>pDp1Fn*$3zQ^>ZFx!dSof+ zY_aiZH{*_Bx#76}#wThI{O!83!}{)+noRQMgsA{|dMzC--YMD7m$50~{0;}?4Gmll z{ICe33!)6@#pBFRU*u zep%vDfXSJ$D>j0YOfk%!mrsjas4nPxYcyki;`Wskt;9}=VYcs7HK8DlQ${7zTBk~{ zzw~+2!Otkx{gR_%70uHZ@1d_>epgu0`(z|DAF;1sgdIJN-ndZxB6N-mm3;TE=f??5 zbt1P->h#oFo!k$Hxe8vR=KEMF>u@Sv<HE&Spq^wIM!KUmMa6O0spe#S1T zX(-|qH)bn=Nv&DYru|@a^P9F1w#PA<4X-2`j%=bc`eq;O+5PGW3a`={&qBN(y^s^7 z(K>pmBIN9Cs}(_KGyN0SP)1K*UQjePep_yB@@{9H=Cidu_pywZR-)Y1+?x`4Oiw54 ztVkNBszf?g>=Nhmc2~({^~oWI-3WY8@IKn2n{ICu_KRLSTF}@13$fa0X9}@O?&LN< zmcZAQifr8R?3va|DH&YMHP>2q?OssOxw1OKyqT>+DF52-g(}@CEgcon=~j*OiW@k} z;}MmcW}78h>I%}B9ZQt^bl+N_Io?`$CAFoIR3qOUu$mg-tjyS4ZO-SJin=A+J|=He zJABBtmPvqO-H=&t_v4XV@`|{Ad%}RQGUvzg%TKKht($INnz7zEGQ=MKg8q*9U6lS= z6*bZ5_y|l^fED+<8=mwGI!~T+`sKIm4-^o&E}{GKwfQb_$F^sLrgOX7cdNXO&3J!y zO9O!weTL!4l$0pEa>unxUJ;}{>IJbfye}f%<@-2CFbN_kA;x_x5B{z^ZL8YC2lLUJGSjt<)Jq^ z@f2hK-l6*(f8;Ko5IjSqfvHN;TEg}>Ti9C+;{0F7;@Q8_ed_vpvwe$_c#9YA{k3}s z3v*9zw>rFnp2+1sF20K~4U@$wk!1dq-e(=-AHTxzlXWVJC$G+*yf5xX#Xz3OTr|(p zbe{AoTdxs!r}TwTuh!!Q<*8mI0*`{2jfnT)MTT@rX|}bT=H(9~{hGV_bj*zvcOKFV zI%!FzdSAWQ$RwbEbL3+f1_nvWzWgbGfT_V0KlIGLNdeX=9! zg|A94L+1J840*nw zI^BE`*6#X#-Os&d3^DmP?acPLe>)(B#xUk5*Oq@U!Mt+6eLy&Av}Fh94$-rw-DUO` zL!IoUz3Nx;ms#7`Mu|;_IK^4?(Z~6o-E*soBpcKYbUwP;yWYUPy-xE%IomsFkG9o8 zNAu%eR#J8omoAlh)?F(eci$hV++((1Vmtd&*1KoZLhKl;ZY@o?J(uYTXJcjfV% zCvEB&O^(Y^AB1@*>nY_pb-eYm*fPfUE^9cFKwKDQpS*tGWVKi#YaJjWSA7UeG4YHK6X%*{> zZ7VIdo$K>P$&!saGL}2DnAm<6OIW?`<)}G+|MdMfL;AN@w!aJAMSl7~gqiqg@j8={ zs75rZN>W(wD(8BZP)QZeO!F2$ubUU=y)o~`FuDzL%FCOFR3Z-#6d7!7i!&+TbJaWx z9r8NuG1Iv$rlolHEtgo#d=#r{-}2u2`oO5lE`Rg=Fl^SDW_-J;bMp6!m%X;NN$#KT zrMFO{SH4ZteZA-P0*h{6%KRLbNH=~?z}f*r%#j+|bZ^ji{G@We_tS=+LRP)b#2)|m zTuN$^d7*rI7PB_torQKP?jz{gd)j+c zc<4R#-BMMa-*}$oLR^T--21LIO`)jW3yrLiJu^*puC4bkuRRx7z`u#nOeh|F^uf*W zbd5w<&=c>Ua!q>mj-)k(XQs|y`{S$b4mxr4&FH6R_Fp3ly3ag6@lwk_@43J;^P|42 z2YFKOFKDJzi>k!9rhfh4{o3u?Jie(;_4%r1Z&dK0(0sO6$MS}28dC>f@Xs#WxX+hW zpFhq!`l&@>+B5fPaN6o+&q3i2v7i3e$WimJhm^m5m9Mt{A{^x6dOd|GdURo|VOrbX zkgm|&Aj{|RMzfFQ>Yh)J8nKb7)9VablRX&6Z30dJppv`|46Mxz4E}tn0$-(M$DGkN zZM5b|vO0L<8ZQG&YRC)gXZ3PBMjh^2Co*5P_v)%2yAjt`u*K}i5gtbt_H`M15y$M% z&_G^ny%^^{J}D!nzW#d|Ya3D+ zf8Z03oVffWTe@&~zw>gZMJV5g{ev-a_H}1f1%|ngf7q=mGs-r4*<#n{YAa!nt!7(v zq;m1&$DXD;UOW{0!7-$#RjJWmj_w?Jzv9ry4=_Q_{?^th9 zkDb+B(LPv)SDx6G#6?^_=@*xVu0NE&zvAnR+k5|95brr`?z4KAjkJ`x5P9&VC<^nQ z`YiNd<}&4#2x-OaB5U&*^uwP?=-F7~p>r7efcQb>aQ}P3**ZWVYwskzni{q#^<2&L zx9QwBPuAXdWDRj-9}TKlf5A`)0Q*62_?FATVz-7{fS;U;dw?rhF2XMmz6Aq7OE)6W z#m$Etf^#K%di!gO&s5ckc&9Dy6%rDtAuk^u9xfNIBo`3mDX*Zet}c&Plvh-gg%YyCNBu)w zB4qu8B@h$8Y#5S*-GaOWL%ak0afnS9*MQIvZEU*`RT|3CraAs^uq zD6b%gm-q9N|EET9h*20c@<%}bp+>MBd|4=OOAZbQ4RRwJg^~S3B>ri_-R&>^z|bIH zS~~7-@?>AK9~2FSUKReurHPq^^e&@c>e3q`^+Gt?GG{A;)exOuy4(1wZx4;N(*4>wtNWmi>Mf|9$6 ztgE`RhpekAUX85arl_Q(uJR94X8yq;F8*$0#1u3x=MC+UmB?f~UP(n(mF%J}OHd}0 zWz`8TsHy2rT zB|Ki%O^K}NqNc3i;!aSbMdR+KVH^8dwU%H&!mybgVXv|a#QbM=>I5<^lL`#As_j5 z_v_Zzn>LkjINGGpaB=&ULa<91*_{?AwDpVS=H=q=NrtP(AMN@l-TOai1-vWXO<5K1 zCQDFrSCS>*71d6D7q@+T~rka>VI_8U)F;IJVL@W!(u1FhzHDysWE-in6Sm zq9U9u1T{5vb;7>}{ePMw>=mOQd7{#jHl`TwEpzXblVjzVaD(ctyRZTv>||!BXBW};Wxyh!)1HT(^gdPk>fMYmfu*fnMj>71Ol$2SL&rJ~ zZD7CXC-)L{=$ognJ!Ia_bXlNvWiU9`S z=xQhvAcZ8v0$@Fhq6Xgg1b`qpd92rEMC0`J=hEwd8>7@%_|1A~7SNFON*dK9Gq=uyjC+5>K)fvJ@s zJGC%Hu8jdjX<(q#i!B&{{9zPnmLX;nkPZR-KlS3UfLR#O$F)g85qd;q7m)S>hzK`e z7C<<a!u>tvki1Hc_kaht4>v~pnAe9#pIf$Z&(;>GfJBuq;i1dxV9AN8G|Uh!QcQC(G~#GDv(2Trl;s5oVGkv zJ{93CcmZiS=ou9w2$)%D_US3d(3BCewlY-yC7OsIkd{Q0XXrsHBcj}hqKwi!9|6+B zh;ofEI1(a>;L|*tNgUQDO{!7*@+C#YkfQRlPpavGKj~rdD#^mDwiQjg9 z_Gi>O%9<76f8Gjq;ONExW)w&kDQBB%`~irY0KPxJ(B+=PP9P0;V8KoW9|j)=a7CAb zGG1%YXwX1Yn>a{bHWej>C55150>f|DxwA;VNFE63fMo7aW}lV=7^(*cCEhTmp|Wfm zl(b{`^{p?K(BIPozH@+{qf?^DWNH(|Z^{u(SsY)z>SrqJBIcIHAuTk*^RdNEu&)msfGI%c> z_zr_)R4BG6#zzcXQKg`WY^W%THlSF{aS;JweRw|u(FSAMA{94D-^2Zd38?siQ?*&B zCHgOHK*blZ)kdP07^AR&?i)J*ge8&%$fuwRK4fYuqrV6&u>v7wim_7+_#(2z4z3U= z^iF;tigAgPqJNYD0KxE*FkoE-Y-gKuvcMAeDiftefLK;63YK_RnTc!BbO2XcadL#u zLL*IgCD&=8t`Z(^tWR&5Z=GH&hydI-`xBwj^F10A{aqs8)T6br3zPBKp^n_lD zGBcb=oQ6$SyZFvjPyon3yguL!q6h#DD1p(( z7`yR6nn2X()!F|bFJbM~Ivw?1HUMo}m~H}KW+k3ijEVFRV~Dz3!%ETYJ;DT783A}I zyEquxEA-WY!uaK8U?O*e=a zG<|U3wch!B1GR7$$ka0-woHlFm^)wG+<2JFu*|Q$6R7MV0*b~*^&{!uvO(af`vC0y zHdjYD6|1S=sM{UKuzcynQ2`t^f{s6bo6fgAnOeHjrBPqLqeyhnX~ykRRpR64AfF#& zN0o}2%=p}_9hRsu^Lz_oZXZJXX21stT8xx( z*4TjMo*6bIFV^F+&T!0vj3ERvPG`~Qs5g%sKJD3#=zT!@UV?i3)vU?W3P=pNXTSp= zl8z!m>3Rl*gsZe-07jh*0oDOG1TW%j{cF;=D2BLWFoW4$jPu35bgkfNge?2X1N|g!i(*H;n=)#Kl)iW1wnk7f2v1BtZ#Ju zs6j!|+ch;`;=*_VxRvD2bLlNfjIPA_eIDvjBQis;v2`TJ{fZvRX?WYcA#NfZP&lbG z3^+LW6=i3^ViXDy;6$&)AaQSDlSARKvr)9B;zYw;JhcR-4e$+|I=g2#up}lpVYd=a zfu4>ob_yHPrIgZ?d6$F@KU&&6o3VtJNEgagF_K{|8k7`%zor|=3;ab!_pPBwV{8H> zbDMPm?bYq@_=YI|_h7k?7*PN?_h#2!3RlD?900T?`?DH<0))F<56^!@sEu?#rL$UF z>~BN)GJ_Zx6^;34fpKc`XsC<=#J$yU!}(`fA-QW-8PecDbyl1Rv4A&77g6R`kmp`n zltci=K5D}|9K`$deb^z^i6R%_xW{hGl@4=qKf^MXYRu#%`L&#sSJK2i)3tO6T#?8x zeK>S%tzOXTfyw)DR57wSQim_Q;Y6QR&97hfq=RND#yoE|qURQjlI!M2{Fg6Qu~|!* zTnCskZN4uo-A6mudTUt%leht;kPY`au=?TmqjAHVF|cN)I{ctp0|}2}7j4TLoeEUGndm_Z0Dt_tU&^Mqxp#mS ziXwe=jB6c*gI>?}lH+_iK~W5XZvEG|jv5%l7Mctziga1Ay%_;{&Gl#A}}=EIhFb$X)L9>D2}G3Ppjk;H9F9P z0Yr!nB8CE-MpIm%o(Y9mgzrtFXz6G@$VTJgZVLs4xWU1)K{*PnhwFZ4#=lcb2Gk!6M5wo~FK%;SSi9Jh zH5?X65r<{;--undg*f71g&D!qPdqeeZaj3R<3uC*p^)ex2U^8ttIUhZL%`Vcv|>hf z051wk(R|Ij2k!cVbwf3C{G^&m)QoHFpxBqMVfPm+L1+*}xpZxHV3z@Jf%^c`gm1r1 zWVnE$s9#w(0@45mdZyxkg}uWP#fYM2A+h4Hcuvr*g|yHXVt+jBN3+*D3-Fu^0|9z~ zvwyu}VAnnrl*~A_2ZmVEjTDFx#Y#^pM|zvc$Qg&n3)4Erlm`pC)er#juxvPD_gbSM z1C;g}{q9S|OA@~*|I(mrL46THnu;(dU;v#weXm*MgCM}a`4<92?mTTQ9#7>hD_eiO z6k1d9zUGJ&itqpkFgSJ!7FCh+Vxm&k7)Xk6(ev6-q3}>3k~)Y&qLGki1W~jtpD?#6 z0k#8ml~zYd-!XZ@+|Fch47}}&YeTxcM8ptu+8Esd^3AaLem%c6>TqQr!$Kvv3wJW&Jj*e0*aNUQ#|Rtrpo9Zaqr$fY7{nE) zAHZX0x7O0rB(NxAemMsPAj|2m8H1a{lW@ptf)p1;B$RQimEHvz_j@svscKG3=iw)> z+dbKGy(wDoq%>Suxc>vI`me>G6Z^-sKK947hK>HGnWIyJNYaexU+oSI66%pBBbYHK zY&jfEixZryNJeHFHAs*8^IF`PE&s~wZa{Klz+J50+Quz;H0*#+w>FSI0nd87+cPTW zgKwg#oNN>}5sGg%a~p?YwB-Sx&OZdsy)B43g5xhh4B}X+q!dp&#D4=|4e{;&`MOFI z@Wg^1xf#|My$lfyl+|HTP=q{C92RlRVa4;0^rA>insO*mpSZetv?xTo=$P%8)k+l;umUIPtC2vgr^s_MjovF z%uFq016?PBZ1Y2%9hVE1jq+GXZdic16*b()P$wVhNRgj~JE22t$x5FUQ{BM}QPzpQ zDeI5F?LEo09_tL>68*k@Bk&u6-w6DFhQPW%2RmHk;l}neZXn~oCU~l)(#}>owI3D8 PnHQKDSsE7WlaBp2KY?KL literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7e91a578..1350b945 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + From 9caf4503e8e51bb0ccc43b9587c54b67ce80d404 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Sun, 29 Jan 2023 17:38:24 +0300 Subject: [PATCH 27/38] ui: Create new app themes (#274) * ui: Create new app themes * fix: Groups selection page * ui: Chagne text colors and fix router rebuilding * ui: Update lks livestream elements style * fix: Pop all routes to first --- lib/data/models/app_settings_model.dart | 4 + .../app_settings_repository_impl.dart | 3 + lib/domain/entities/app_settings.dart | 3 + lib/main.dart | 78 ++-- lib/presentation/app_notifier.dart | 55 +++ .../bloc/app_cubit/app_cubit.dart | 1 + .../bloc/app_cubit/app_state.dart | 3 + .../update_info_bloc/update_info_bloc.dart | 1 + lib/presentation/colors.dart | 146 ++++++- lib/presentation/core/routes/routes.dart | 5 + lib/presentation/core/routes/routes.gr.dart | 368 ++++++++++++------ lib/presentation/pages/home_page.dart | 30 +- lib/presentation/pages/login/login_page.dart | 18 +- lib/presentation/pages/map/map_page.dart | 23 +- .../map/widgets/map_navigation_button.dart | 16 +- .../pages/map/widgets/map_scaling_button.dart | 14 +- .../pages/map/widgets/search_item_button.dart | 4 +- .../pages/news/news_details_page.dart | 20 +- lib/presentation/pages/news/news_page.dart | 31 +- .../pages/news/widgets/news_item.dart | 16 +- .../pages/news/widgets/stories_wrapper.dart | 8 +- .../pages/news/widgets/story_item.dart | 6 +- .../pages/news/widgets/tags_widgets.dart | 8 +- .../pages/onboarding/onboarding_page.dart | 15 +- .../pages/onboarding/widgets/indicator.dart | 4 +- .../pages/onboarding/widgets/next_button.dart | 12 +- .../pages/profile/about_app_page.dart | 112 +++--- .../pages/profile/profile_announces_page.dart | 81 ++-- .../profile/profile_attendance_page.dart | 26 +- .../pages/profile/profile_detail_page.dart | 10 +- .../pages/profile/profile_lectors_page.dart | 26 +- .../pages/profile/profile_page.dart | 46 ++- .../pages/profile/profile_scores_page.dart | 42 +- .../pages/profile/profile_settings_page.dart | 106 +++++ .../profile/widgets/attendance_card.dart | 20 +- .../profile/widgets/bottom_error_info.dart | 35 +- .../profile/widgets/lector_search_card.dart | 16 +- .../pages/profile/widgets/member_info.dart | 4 +- .../profile/widgets/scores_chart_modal.dart | 4 +- .../pages/schedule/groups_select_page.dart | 191 +++++---- .../pages/schedule/schedule_page.dart | 57 ++- .../schedule/widgets/empty_lesson_card.dart | 16 +- .../pages/schedule/widgets/lesson_card.dart | 28 +- .../schedule/widgets/schedule_page_view.dart | 30 +- .../widgets/schedule_settings_drawer.dart | 8 +- .../widgets/schedule_settings_modal.dart | 34 +- lib/presentation/theme.dart | 225 ++++++++--- lib/presentation/typography.dart | 55 +++ .../widgets/badged_container.dart | 10 +- .../widgets/buttons/app_settings_button.dart | 2 + .../widgets/buttons/colorful_button.dart | 4 +- .../widgets/buttons/icon_button.dart | 62 +-- .../widgets/buttons/primary_button.dart | 6 +- .../widgets/buttons/primary_tab_button.dart | 12 +- .../widgets/buttons/select_date_button.dart | 8 +- .../buttons/select_range_date_button.dart | 54 ++- .../widgets/buttons/settings_button.dart | 6 +- .../widgets/buttons/text_outlined_button.dart | 14 +- lib/presentation/widgets/container_label.dart | 4 +- lib/presentation/widgets/copy_text_block.dart | 4 +- .../widgets/forms/labelled_input.dart | 24 +- .../widgets/fullscreen_image.dart | 6 +- .../widgets/settings_switch_button.dart | 6 +- .../widgets/update_info_modal.dart | 20 +- lib/service_locator.dart | 9 + pubspec.yaml | 10 +- 66 files changed, 1496 insertions(+), 829 deletions(-) create mode 100644 lib/presentation/app_notifier.dart create mode 100644 lib/presentation/pages/profile/profile_settings_page.dart create mode 100644 lib/presentation/typography.dart diff --git a/lib/data/models/app_settings_model.dart b/lib/data/models/app_settings_model.dart index 691b5419..61b44af6 100644 --- a/lib/data/models/app_settings_model.dart +++ b/lib/data/models/app_settings_model.dart @@ -6,9 +6,11 @@ class AppSettingsModel extends AppSettings { const AppSettingsModel({ required bool onboardingShown, required String lastUpdateVersion, + required String theme, }) : super( onboardingShown: onboardingShown, lastUpdateVersion: lastUpdateVersion, + theme: theme, ); factory AppSettingsModel.fromRawJson(String str) => @@ -18,6 +20,7 @@ class AppSettingsModel extends AppSettings { return AppSettingsModel( onboardingShown: json["onboarding_shown"], lastUpdateVersion: json["last_update_version"] ?? '', + theme: json["theme"] ?? 'dark', ); } @@ -26,5 +29,6 @@ class AppSettingsModel extends AppSettings { Map toJson() => { "onboarding_shown": onboardingShown, "last_update_version": lastUpdateVersion, + "theme": theme, }; } diff --git a/lib/data/repositories/app_settings_repository_impl.dart b/lib/data/repositories/app_settings_repository_impl.dart index 9814e534..0f01156b 100644 --- a/lib/data/repositories/app_settings_repository_impl.dart +++ b/lib/data/repositories/app_settings_repository_impl.dart @@ -15,11 +15,13 @@ class AppSettingsRepositoryImpl implements AppSettingsRepository { Future getSettings() async { try { final settings = await localDataSource.getSettingsFromCache(); + return settings; } on CacheException { const newLocalSettings = AppSettingsModel( onboardingShown: false, lastUpdateVersion: '', + theme: 'dark', ); await localDataSource.setSettingsToCache(newLocalSettings); return newLocalSettings; @@ -32,6 +34,7 @@ class AppSettingsRepositoryImpl implements AppSettingsRepository { AppSettingsModel( onboardingShown: settings.onboardingShown, lastUpdateVersion: settings.lastUpdateVersion, + theme: settings.theme, ), ); } diff --git a/lib/domain/entities/app_settings.dart b/lib/domain/entities/app_settings.dart index c9817c79..1300b2e0 100644 --- a/lib/domain/entities/app_settings.dart +++ b/lib/domain/entities/app_settings.dart @@ -3,15 +3,18 @@ import 'package:equatable/equatable.dart'; class AppSettings extends Equatable { final bool onboardingShown; final String lastUpdateVersion; // Version on last update info from strapi + final String theme; // Theme type const AppSettings({ required this.onboardingShown, required this.lastUpdateVersion, + required this.theme, }); @override List get props => [ onboardingShown, lastUpdateVersion, + theme, ]; } diff --git a/lib/main.dart b/lib/main.dart index 0fa9c465..2e27e9ec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -22,17 +22,18 @@ import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/stories_bloc/stories_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/update_info_bloc/update_info_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:intl/intl_standalone.dart'; import 'package:rtu_mirea_app/service_locator.dart' as dependency_injection; import 'package:url_strategy/url_strategy.dart'; +import 'presentation/app_notifier.dart'; import 'service_locator.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'firebase_options.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:provider/provider.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -63,12 +64,19 @@ Future main() async { setPathUrlStrategy(); - findSystemLocale().then((_) => runApp(const App())); + findSystemLocale().then( + (_) => runApp(ChangeNotifierProvider( + create: (context) => getIt(), + child: const App(), + )), + ); } class App extends StatelessWidget { const App({Key? key}) : super(key: key); + static final appRouter = AppRouter(); + @override Widget build(BuildContext context) { // blocking the orientation of the application to @@ -78,14 +86,6 @@ class App extends StatelessWidget { DeviceOrientation.portraitUp, ]); - // deleting the system status bar color - SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - systemNavigationBarColor: DarkThemeColors.background01 - )); - - final appRouter = AppRouter(); - return MultiBlocProvider( providers: [ BlocProvider( @@ -112,34 +112,36 @@ class App extends StatelessWidget { lazy: false, // We need to init it as soon as possible ), ], - child: AdaptiveTheme( - light: lightTheme, - dark: darkTheme, - initial: AdaptiveThemeMode.dark, - builder: (theme, darkTheme) => MaterialApp.router( - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en'), - Locale('ru'), - ], - locale: const Locale('ru'), - debugShowCheckedModeBanner: false, - title: 'Приложение РТУ МИРЭА', - theme: theme, - routerDelegate: appRouter.delegate( - navigatorObservers: () => [ - FirebaseAnalyticsObserver( - analytics: FirebaseAnalytics.instance, - ), + child: Consumer( + builder: (BuildContext context, AppNotifier value, Widget? child) { + return MaterialApp.router( + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, ], - ), - routeInformationProvider: appRouter.routeInfoProvider(), - routeInformationParser: appRouter.defaultRouteParser(), - ), + supportedLocales: const [ + Locale('en'), + Locale('ru'), + ], + locale: const Locale('ru'), + debugShowCheckedModeBanner: false, + title: 'Приложение РТУ МИРЭА', + routerDelegate: appRouter.delegate( + navigatorObservers: () => [ + FirebaseAnalyticsObserver( + analytics: FirebaseAnalytics.instance, + ), + AutoRouteObserver(), + ], + ), + routeInformationProvider: appRouter.routeInfoProvider(), + routeInformationParser: appRouter.defaultRouteParser(), + themeMode: AppTheme.themeMode, + theme: AppTheme.theme, + darkTheme: AppTheme.darkTheme, + ); + }, ), ); } diff --git a/lib/presentation/app_notifier.dart b/lib/presentation/app_notifier.dart new file mode 100644 index 00000000..85bca45c --- /dev/null +++ b/lib/presentation/app_notifier.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:rtu_mirea_app/domain/entities/app_settings.dart'; +import 'package:rtu_mirea_app/domain/usecases/get_app_settings.dart'; +import 'package:rtu_mirea_app/domain/usecases/set_app_settings.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; + +class AppNotifier extends ChangeNotifier { + final GetAppSettings getAppSettings; + final SetAppSettings setAppSettings; + + AppNotifier({required this.getAppSettings, required this.setAppSettings}) { + init(); + } + + init() async { + final settings = await getAppSettings(); + + AppThemeType themeType = AppThemeType.values.firstWhere( + (element) => element.toString() == settings.theme, + orElse: () => AppThemeType.dark, + ); + + _changeTheme(themeType); + + notifyListeners(); + } + + updateTheme(AppThemeType themeType) { + _changeTheme(themeType); + + notifyListeners(); + + updateInStorage(themeType); + } + + Future updateInStorage(AppThemeType themeType) async { + final settings = await getAppSettings(); + + await setAppSettings( + SetAppSettingsParams( + AppSettings( + onboardingShown: settings.onboardingShown, + lastUpdateVersion: settings.lastUpdateVersion, + theme: themeType.toString(), + ), + ), + ); + } + + void _changeTheme(AppThemeType themeType) { + AppTheme.changeThemeType(themeType); + + // Reset custom theme here if needed + } +} diff --git a/lib/presentation/bloc/app_cubit/app_cubit.dart b/lib/presentation/bloc/app_cubit/app_cubit.dart index e5e054f7..67a32fc9 100644 --- a/lib/presentation/bloc/app_cubit/app_cubit.dart +++ b/lib/presentation/bloc/app_cubit/app_cubit.dart @@ -22,6 +22,7 @@ class AppCubit extends Cubit { AppSettings( onboardingShown: true, lastUpdateVersion: settings.lastUpdateVersion, + theme: settings.theme, ), ), ); diff --git a/lib/presentation/bloc/app_cubit/app_state.dart b/lib/presentation/bloc/app_cubit/app_state.dart index 2d262401..bbbf8206 100644 --- a/lib/presentation/bloc/app_cubit/app_state.dart +++ b/lib/presentation/bloc/app_cubit/app_state.dart @@ -7,10 +7,13 @@ abstract class AppState extends Equatable { List get props => []; } +// Initial state of the app. class AppInitial extends AppState {} +// Onboarding state of the app. It is shown only once after the first launch. class AppOnboarding extends AppState {} +// State of the app when new updates are available. class AppUpdatesOnboarding extends AppState {} class AppClean extends AppState {} diff --git a/lib/presentation/bloc/update_info_bloc/update_info_bloc.dart b/lib/presentation/bloc/update_info_bloc/update_info_bloc.dart index 97fc4719..1aecc3c0 100644 --- a/lib/presentation/bloc/update_info_bloc/update_info_bloc.dart +++ b/lib/presentation/bloc/update_info_bloc/update_info_bloc.dart @@ -61,6 +61,7 @@ class UpdateInfoBloc extends Bloc { AppSettings( onboardingShown: settings.onboardingShown, lastUpdateVersion: event.versionToSave, + theme: settings.theme, ), ), ); diff --git a/lib/presentation/colors.dart b/lib/presentation/colors.dart index 2f4754be..e6623098 100644 --- a/lib/presentation/colors.dart +++ b/lib/presentation/colors.dart @@ -1,34 +1,140 @@ import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; -abstract class DarkThemeColors { +class ThemeColors { + ThemeColors(); + + // Primary actions, emphasizing navigation elements, + // backgrounds, text, etc. + Color get primary => const Color(0xFF246BFD); + Color get secondary => const Color(0xFFC25FFF); + + // Backgrounds + Color get background01 => const Color(0xFF181A20); + Color get background02 => const Color(0xFF262A34); + Color get background03 => const Color(0xFF1F222A); + + // Colorful + Color get colorful01 => const Color(0xFFA06AF9); + Color get colorful02 => const Color(0xFFFBA3FF); + Color get colorful03 => const Color(0xFF8E96FF); + Color get colorful04 => const Color(0xFF94F0F0); + Color get colorful05 => const Color(0xFFA5F59C); + Color get colorful06 => const Color(0xFFFFDD72); + Color get colorful07 => const Color(0xFFFF968E); + + Color get white => const Color(0xFFFFFFFF); + + // States + Color get active => const Color(0xFFFFFFFF); + Color get deactive => const Color(0xFF5E6272); + Color get activeLightMode => const Color(0xFF200745); + Color get deactiveDarker => const Color(0xFF3A3D46); + + LinearGradient get gradient07 => + const LinearGradient(colors: [Color(0xFFBBFFE7), Color(0xFF86FFCA)]); +} + +class LightThemeColors extends ThemeColors { + LightThemeColors(); + + // Primary actions, emphasizing navigation elements, + // backgrounds, text, etc. + @override + Color get primary => const Color(0xFF246BFD); + @override + Color get secondary => const Color(0xFFC25FFF); + + // Backgrounds + @override + Color get background01 => const Color(0xFFF5F5F5); + @override + Color get background02 => const Color(0xFFFFFFFF); + @override + Color get background03 => const Color(0xFFF8F8F8); + + // Colorful + @override + Color get colorful01 => const Color(0xFFA06AF9); + @override + Color get colorful02 => const Color(0xFFFBA3FF); + @override + Color get colorful03 => const Color(0xFF8E96FF); + @override + Color get colorful04 => const Color(0xFF94F0F0); + @override + Color get colorful05 => const Color(0xFF5DCD9B); + @override + Color get colorful06 => const Color(0xFFFFDD72); + @override + Color get colorful07 => const Color(0xFFFF968E); + + @override + Color get white => const Color(0xFFFFFFFF); + + // States + @override + Color get active => const Color(0xFF0D0D0D); + @override + Color get deactive => const Color(0xFF5E6272); + @override + Color get activeLightMode => const Color(0xFF200745); + @override + Color get deactiveDarker => const Color(0xFF3A3D46); + + @override + LinearGradient get gradient07 => + const LinearGradient(colors: [Color(0xFFBBFFE7), Color(0xFF86FFCA)]); +} + +class AmoledDarkThemeColors extends ThemeColors { + AmoledDarkThemeColors(); + // Primary actions, emphasizing navigation elements, // backgrounds, text, etc. - static const primary = Color(0xFF246BFD); - static const secondary = Color(0xFFC25FFF); + @override + Color get primary => const Color(0xFF246BFD); + @override + Color get secondary => const Color(0xFFC25FFF); // Backgrounds - static const background01 = Color(0xFF181A20); - static const background02 = Color(0xFF262A34); - static const background03 = Color(0xFF1F222A); + @override + Color get background01 => const Color(0xFF000000); + @override + Color get background02 => const Color(0xFF000000); + @override + Color get background03 => const Color(0xFF000000); // Colorful - static const colorful01 = Color(0xFFA06AF9); - static const colorful02 = Color(0xFFFBA3FF); - static const colorful03 = Color(0xFF8E96FF); - static const colorful04 = Color(0xFF94F0F0); - static const colorful05 = Color(0xFFA5F59C); - static const colorful06 = Color(0xFFFFDD72); - static const colorful07 = Color(0xFFFF968E); + @override + Color get colorful01 => const Color(0xFFA06AF9); + @override + Color get colorful02 => const Color(0xFFFBA3FF); + @override + Color get colorful03 => const Color(0xFF8E96FF); + @override + Color get colorful04 => const Color(0xFF94F0F0); + @override + Color get colorful05 => const Color(0xFFA5F59C); + @override + Color get colorful06 => const Color(0xFFFFDD72); + @override + Color get colorful07 => const Color(0xFFFF968E); - static const white = Color(0xFFFFFFFF); + @override + Color get white => const Color(0xFFFFFFFF); // States - static const active = white; - static const deactive = Color(0xFF5E6272); - static const activeLightMode = Color(0xFF200745); - static const deactiveDarker = Color(0xFF3A3D46); + @override + Color get active => const Color(0xFFE5E5E5); + @override + Color get deactive => const Color(0xFF5E6272); + @override + Color get activeLightMode => const Color(0xFF200745); + @override + Color get deactiveDarker => const Color(0xFF3A3D46); - static const gradient07 = - LinearGradient(colors: [Color(0xFFBBFFE7), Color(0xFF86FFCA)]); + @override + LinearGradient get gradient07 => + const LinearGradient(colors: [Color(0xFFBBFFE7), Color(0xFF86FFCA)]); } diff --git a/lib/presentation/core/routes/routes.dart b/lib/presentation/core/routes/routes.dart index b18d563a..00c2f073 100644 --- a/lib/presentation/core/routes/routes.dart +++ b/lib/presentation/core/routes/routes.dart @@ -16,6 +16,7 @@ import 'package:rtu_mirea_app/presentation/pages/profile/profile_detail_page.dar import 'package:rtu_mirea_app/presentation/pages/profile/profile_lectors_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_scores_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_page.dart'; +import 'package:rtu_mirea_app/presentation/pages/profile/profile_settings_page.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/groups_select_page.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart'; @@ -98,6 +99,10 @@ import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart'; path: 'scores', page: ProfileScoresPage, ), + AutoRoute( + path: 'settings', + page: ProfileSettingsPage, + ), ], ), ], diff --git a/lib/presentation/core/routes/routes.gr.dart b/lib/presentation/core/routes/routes.gr.dart index 30f2a6aa..b05cc3f5 100644 --- a/lib/presentation/core/routes/routes.gr.dart +++ b/lib/presentation/core/routes/routes.gr.dart @@ -11,302 +11,400 @@ // ignore_for_file: type=lint // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i18; -import 'package:auto_route/empty_router_widgets.dart' as _i4; -import 'package:flutter/material.dart' as _i19; -import 'package:rtu_mirea_app/domain/entities/news_item.dart' as _i22; -import 'package:rtu_mirea_app/domain/entities/story.dart' as _i21; -import 'package:rtu_mirea_app/domain/entities/user.dart' as _i23; -import 'package:rtu_mirea_app/presentation/core/routes/routes.dart' as _i20; -import 'package:rtu_mirea_app/presentation/pages/home_page.dart' as _i1; -import 'package:rtu_mirea_app/presentation/pages/login/login_page.dart' as _i11; -import 'package:rtu_mirea_app/presentation/pages/map/map_page.dart' as _i5; +import 'package:auto_route/auto_route.dart' as _i19; +import 'package:auto_route/empty_router_widgets.dart' deferred as _i4; +import 'package:flutter/material.dart' as _i20; +import 'package:rtu_mirea_app/domain/entities/news_item.dart' as _i23; +import 'package:rtu_mirea_app/domain/entities/story.dart' as _i22; +import 'package:rtu_mirea_app/domain/entities/user.dart' as _i24; +import 'package:rtu_mirea_app/presentation/core/routes/routes.dart' as _i21; +import 'package:rtu_mirea_app/presentation/pages/home_page.dart' + deferred as _i1; +import 'package:rtu_mirea_app/presentation/pages/login/login_page.dart' + deferred as _i11; +import 'package:rtu_mirea_app/presentation/pages/map/map_page.dart' + deferred as _i5; import 'package:rtu_mirea_app/presentation/pages/news/news_details_page.dart' - as _i9; -import 'package:rtu_mirea_app/presentation/pages/news/news_page.dart' as _i8; + deferred as _i9; +import 'package:rtu_mirea_app/presentation/pages/news/news_page.dart' + deferred as _i8; import 'package:rtu_mirea_app/presentation/pages/news/widgets/stories_wrapper.dart' - as _i3; + deferred as _i3; import 'package:rtu_mirea_app/presentation/pages/onboarding/onboarding_page.dart' - as _i2; + deferred as _i2; import 'package:rtu_mirea_app/presentation/pages/profile/about_app_page.dart' - as _i12; + deferred as _i12; import 'package:rtu_mirea_app/presentation/pages/profile/profile_announces_page.dart' - as _i13; + deferred as _i13; import 'package:rtu_mirea_app/presentation/pages/profile/profile_attendance_page.dart' - as _i14; + deferred as _i14; import 'package:rtu_mirea_app/presentation/pages/profile/profile_detail_page.dart' - as _i15; + deferred as _i15; import 'package:rtu_mirea_app/presentation/pages/profile/profile_lectors_page.dart' - as _i16; + deferred as _i16; import 'package:rtu_mirea_app/presentation/pages/profile/profile_page.dart' - as _i10; + deferred as _i10; import 'package:rtu_mirea_app/presentation/pages/profile/profile_scores_page.dart' - as _i17; + deferred as _i17; +import 'package:rtu_mirea_app/presentation/pages/profile/profile_settings_page.dart' + deferred as _i18; import 'package:rtu_mirea_app/presentation/pages/schedule/groups_select_page.dart' - as _i7; + deferred as _i7; import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart' - as _i6; + deferred as _i6; -class AppRouter extends _i18.RootStackRouter { - AppRouter([_i19.GlobalKey<_i19.NavigatorState>? navigatorKey]) +class AppRouter extends _i19.RootStackRouter { + AppRouter([_i20.GlobalKey<_i20.NavigatorState>? navigatorKey]) : super(navigatorKey); @override - final Map pagesMap = { + final Map pagesMap = { HomeRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i1.HomePage(), + child: _i19.DeferredWidget( + _i1.loadLibrary, + () => _i1.HomePage(), + ), ); }, OnBoardingRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i2.OnBoardingPage(), + child: _i19.DeferredWidget( + _i2.loadLibrary, + () => _i2.OnBoardingPage(), + ), ); }, StoriesWrapperRoute.name: (routeData) { final args = routeData.argsAs(); - return _i18.CustomPage( + return _i19.CustomPage( routeData: routeData, - child: _i3.StoriesWrapper( - key: args.key, - stories: args.stories, - storyIndex: args.storyIndex, + child: _i19.DeferredWidget( + _i3.loadLibrary, + () => _i3.StoriesWrapper( + key: args.key, + stories: args.stories, + storyIndex: args.storyIndex, + ), ), - customRouteBuilder: _i20.transparentRoute, + customRouteBuilder: _i21.transparentRoute, opaque: false, barrierDismissible: false, ); }, ScheduleRouter.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i4.EmptyRouterPage(), + child: _i19.DeferredWidget( + _i4.loadLibrary, + () => _i4.EmptyRouterPage(), + ), ); }, NewsRouter.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i4.EmptyRouterPage(), + child: _i19.DeferredWidget( + _i4.loadLibrary, + () => _i4.EmptyRouterPage(), + ), ); }, MapRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i5.MapPage(), + child: _i19.DeferredWidget( + _i5.loadLibrary, + () => _i5.MapPage(), + ), ); }, ProfileRouter.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i4.EmptyRouterPage(), + child: _i19.DeferredWidget( + _i4.loadLibrary, + () => _i4.EmptyRouterPage(), + ), ); }, ScheduleRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i6.SchedulePage(), + child: _i19.DeferredWidget( + _i6.loadLibrary, + () => _i6.SchedulePage(), + ), ); }, GroupsSelectRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i7.GroupsSelectPage(), + child: _i19.DeferredWidget( + _i7.loadLibrary, + () => _i7.GroupsSelectPage(), + ), ); }, NewsRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i8.NewsPage(), + child: _i19.DeferredWidget( + _i8.loadLibrary, + () => _i8.NewsPage(), + ), ); }, NewsDetailsRoute.name: (routeData) { final args = routeData.argsAs(); - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: _i9.NewsDetailsPage( - key: args.key, - newsItem: args.newsItem, + child: _i19.DeferredWidget( + _i9.loadLibrary, + () => _i9.NewsDetailsPage( + key: args.key, + newsItem: args.newsItem, + ), ), ); }, ProfileRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i10.ProfilePage(), + child: _i19.DeferredWidget( + _i10.loadLibrary, + () => _i10.ProfilePage(), + ), ); }, LoginRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i11.LoginPage(), + child: _i19.DeferredWidget( + _i11.loadLibrary, + () => _i11.LoginPage(), + ), ); }, AboutAppRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i12.AboutAppPage(), + child: _i19.DeferredWidget( + _i12.loadLibrary, + () => _i12.AboutAppPage(), + ), ); }, ProfileAnnouncesRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i13.ProfileAnnouncesPage(), + child: _i19.DeferredWidget( + _i13.loadLibrary, + () => _i13.ProfileAnnouncesPage(), + ), ); }, ProfileAttendanceRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i14.ProfileAttendancePage(), + child: _i19.DeferredWidget( + _i14.loadLibrary, + () => _i14.ProfileAttendancePage(), + ), ); }, ProfileDetailRoute.name: (routeData) { final args = routeData.argsAs(); - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: _i15.ProfileDetailPage( - key: args.key, - user: args.user, + child: _i19.DeferredWidget( + _i15.loadLibrary, + () => _i15.ProfileDetailPage( + key: args.key, + user: args.user, + ), ), ); }, ProfileLectrosRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( routeData: routeData, - child: const _i16.ProfileLectrosPage(), + child: _i19.DeferredWidget( + _i16.loadLibrary, + () => _i16.ProfileLectrosPage(), + ), ); }, ProfileScoresRoute.name: (routeData) { - return _i18.AdaptivePage( + return _i19.AdaptivePage( + routeData: routeData, + child: _i19.DeferredWidget( + _i17.loadLibrary, + () => _i17.ProfileScoresPage(), + ), + ); + }, + ProfileSettingsRoute.name: (routeData) { + return _i19.AdaptivePage( routeData: routeData, - child: const _i17.ProfileScoresPage(), + child: _i19.DeferredWidget( + _i18.loadLibrary, + () => _i18.ProfileSettingsPage(), + ), ); }, }; @override - List<_i18.RouteConfig> get routes => [ - _i18.RouteConfig( + List<_i19.RouteConfig> get routes => [ + _i19.RouteConfig( HomeRoute.name, path: '/', + deferredLoading: true, children: [ - _i18.RouteConfig( + _i19.RouteConfig( '#redirect', path: '', parent: HomeRoute.name, redirectTo: 'schedule', fullMatch: true, ), - _i18.RouteConfig( + _i19.RouteConfig( ScheduleRouter.name, path: 'schedule', parent: HomeRoute.name, + deferredLoading: true, children: [ - _i18.RouteConfig( + _i19.RouteConfig( ScheduleRoute.name, path: '', parent: ScheduleRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( GroupsSelectRoute.name, path: 'select-group', parent: ScheduleRouter.name, + deferredLoading: true, ), ], ), - _i18.RouteConfig( + _i19.RouteConfig( NewsRouter.name, path: 'news', parent: HomeRoute.name, + deferredLoading: true, children: [ - _i18.RouteConfig( + _i19.RouteConfig( NewsRoute.name, path: '', parent: NewsRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( NewsDetailsRoute.name, path: 'details', parent: NewsRouter.name, + deferredLoading: true, ), ], ), - _i18.RouteConfig( + _i19.RouteConfig( MapRoute.name, path: 'map', parent: HomeRoute.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( ProfileRouter.name, path: 'profile', parent: HomeRoute.name, + deferredLoading: true, children: [ - _i18.RouteConfig( + _i19.RouteConfig( ProfileRoute.name, path: '', parent: ProfileRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( LoginRoute.name, path: 'login', parent: ProfileRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( AboutAppRoute.name, path: 'about', parent: ProfileRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( ProfileAnnouncesRoute.name, path: 'announces', parent: ProfileRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( ProfileAttendanceRoute.name, path: 'attendance', parent: ProfileRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( ProfileDetailRoute.name, path: 'details', parent: ProfileRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( ProfileLectrosRoute.name, path: 'lectors', parent: ProfileRouter.name, + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( ProfileScoresRoute.name, path: 'scores', parent: ProfileRouter.name, + deferredLoading: true, + ), + _i19.RouteConfig( + ProfileSettingsRoute.name, + path: 'settings', + parent: ProfileRouter.name, + deferredLoading: true, ), ], ), ], ), - _i18.RouteConfig( + _i19.RouteConfig( OnBoardingRoute.name, path: '/onboarding', + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( StoriesWrapperRoute.name, path: '/story', + deferredLoading: true, ), - _i18.RouteConfig( + _i19.RouteConfig( '*#redirect', path: '*', redirectTo: '/', fullMatch: true, + deferredLoading: true, ), ]; } /// generated route for /// [_i1.HomePage] -class HomeRoute extends _i18.PageRouteInfo { - const HomeRoute({List<_i18.PageRouteInfo>? children}) +class HomeRoute extends _i19.PageRouteInfo { + const HomeRoute({List<_i19.PageRouteInfo>? children}) : super( HomeRoute.name, path: '/', @@ -318,7 +416,7 @@ class HomeRoute extends _i18.PageRouteInfo { /// generated route for /// [_i2.OnBoardingPage] -class OnBoardingRoute extends _i18.PageRouteInfo { +class OnBoardingRoute extends _i19.PageRouteInfo { const OnBoardingRoute() : super( OnBoardingRoute.name, @@ -330,10 +428,10 @@ class OnBoardingRoute extends _i18.PageRouteInfo { /// generated route for /// [_i3.StoriesWrapper] -class StoriesWrapperRoute extends _i18.PageRouteInfo { +class StoriesWrapperRoute extends _i19.PageRouteInfo { StoriesWrapperRoute({ - _i19.Key? key, - required List<_i21.Story> stories, + _i20.Key? key, + required List<_i22.Story> stories, required int storyIndex, }) : super( StoriesWrapperRoute.name, @@ -355,9 +453,9 @@ class StoriesWrapperRouteArgs { required this.storyIndex, }); - final _i19.Key? key; + final _i20.Key? key; - final List<_i21.Story> stories; + final List<_i22.Story> stories; final int storyIndex; @@ -369,8 +467,8 @@ class StoriesWrapperRouteArgs { /// generated route for /// [_i4.EmptyRouterPage] -class ScheduleRouter extends _i18.PageRouteInfo { - const ScheduleRouter({List<_i18.PageRouteInfo>? children}) +class ScheduleRouter extends _i19.PageRouteInfo { + const ScheduleRouter({List<_i19.PageRouteInfo>? children}) : super( ScheduleRouter.name, path: 'schedule', @@ -382,8 +480,8 @@ class ScheduleRouter extends _i18.PageRouteInfo { /// generated route for /// [_i4.EmptyRouterPage] -class NewsRouter extends _i18.PageRouteInfo { - const NewsRouter({List<_i18.PageRouteInfo>? children}) +class NewsRouter extends _i19.PageRouteInfo { + const NewsRouter({List<_i19.PageRouteInfo>? children}) : super( NewsRouter.name, path: 'news', @@ -395,7 +493,7 @@ class NewsRouter extends _i18.PageRouteInfo { /// generated route for /// [_i5.MapPage] -class MapRoute extends _i18.PageRouteInfo { +class MapRoute extends _i19.PageRouteInfo { const MapRoute() : super( MapRoute.name, @@ -407,8 +505,8 @@ class MapRoute extends _i18.PageRouteInfo { /// generated route for /// [_i4.EmptyRouterPage] -class ProfileRouter extends _i18.PageRouteInfo { - const ProfileRouter({List<_i18.PageRouteInfo>? children}) +class ProfileRouter extends _i19.PageRouteInfo { + const ProfileRouter({List<_i19.PageRouteInfo>? children}) : super( ProfileRouter.name, path: 'profile', @@ -420,7 +518,7 @@ class ProfileRouter extends _i18.PageRouteInfo { /// generated route for /// [_i6.SchedulePage] -class ScheduleRoute extends _i18.PageRouteInfo { +class ScheduleRoute extends _i19.PageRouteInfo { const ScheduleRoute() : super( ScheduleRoute.name, @@ -432,7 +530,7 @@ class ScheduleRoute extends _i18.PageRouteInfo { /// generated route for /// [_i7.GroupsSelectPage] -class GroupsSelectRoute extends _i18.PageRouteInfo { +class GroupsSelectRoute extends _i19.PageRouteInfo { const GroupsSelectRoute() : super( GroupsSelectRoute.name, @@ -444,7 +542,7 @@ class GroupsSelectRoute extends _i18.PageRouteInfo { /// generated route for /// [_i8.NewsPage] -class NewsRoute extends _i18.PageRouteInfo { +class NewsRoute extends _i19.PageRouteInfo { const NewsRoute() : super( NewsRoute.name, @@ -456,10 +554,10 @@ class NewsRoute extends _i18.PageRouteInfo { /// generated route for /// [_i9.NewsDetailsPage] -class NewsDetailsRoute extends _i18.PageRouteInfo { +class NewsDetailsRoute extends _i19.PageRouteInfo { NewsDetailsRoute({ - _i19.Key? key, - required _i22.NewsItem newsItem, + _i20.Key? key, + required _i23.NewsItem newsItem, }) : super( NewsDetailsRoute.name, path: 'details', @@ -478,9 +576,9 @@ class NewsDetailsRouteArgs { required this.newsItem, }); - final _i19.Key? key; + final _i20.Key? key; - final _i22.NewsItem newsItem; + final _i23.NewsItem newsItem; @override String toString() { @@ -490,7 +588,7 @@ class NewsDetailsRouteArgs { /// generated route for /// [_i10.ProfilePage] -class ProfileRoute extends _i18.PageRouteInfo { +class ProfileRoute extends _i19.PageRouteInfo { const ProfileRoute() : super( ProfileRoute.name, @@ -502,7 +600,7 @@ class ProfileRoute extends _i18.PageRouteInfo { /// generated route for /// [_i11.LoginPage] -class LoginRoute extends _i18.PageRouteInfo { +class LoginRoute extends _i19.PageRouteInfo { const LoginRoute() : super( LoginRoute.name, @@ -514,7 +612,7 @@ class LoginRoute extends _i18.PageRouteInfo { /// generated route for /// [_i12.AboutAppPage] -class AboutAppRoute extends _i18.PageRouteInfo { +class AboutAppRoute extends _i19.PageRouteInfo { const AboutAppRoute() : super( AboutAppRoute.name, @@ -526,7 +624,7 @@ class AboutAppRoute extends _i18.PageRouteInfo { /// generated route for /// [_i13.ProfileAnnouncesPage] -class ProfileAnnouncesRoute extends _i18.PageRouteInfo { +class ProfileAnnouncesRoute extends _i19.PageRouteInfo { const ProfileAnnouncesRoute() : super( ProfileAnnouncesRoute.name, @@ -538,7 +636,7 @@ class ProfileAnnouncesRoute extends _i18.PageRouteInfo { /// generated route for /// [_i14.ProfileAttendancePage] -class ProfileAttendanceRoute extends _i18.PageRouteInfo { +class ProfileAttendanceRoute extends _i19.PageRouteInfo { const ProfileAttendanceRoute() : super( ProfileAttendanceRoute.name, @@ -550,10 +648,10 @@ class ProfileAttendanceRoute extends _i18.PageRouteInfo { /// generated route for /// [_i15.ProfileDetailPage] -class ProfileDetailRoute extends _i18.PageRouteInfo { +class ProfileDetailRoute extends _i19.PageRouteInfo { ProfileDetailRoute({ - _i19.Key? key, - required _i23.User user, + _i20.Key? key, + required _i24.User user, }) : super( ProfileDetailRoute.name, path: 'details', @@ -572,9 +670,9 @@ class ProfileDetailRouteArgs { required this.user, }); - final _i19.Key? key; + final _i20.Key? key; - final _i23.User user; + final _i24.User user; @override String toString() { @@ -584,7 +682,7 @@ class ProfileDetailRouteArgs { /// generated route for /// [_i16.ProfileLectrosPage] -class ProfileLectrosRoute extends _i18.PageRouteInfo { +class ProfileLectrosRoute extends _i19.PageRouteInfo { const ProfileLectrosRoute() : super( ProfileLectrosRoute.name, @@ -596,7 +694,7 @@ class ProfileLectrosRoute extends _i18.PageRouteInfo { /// generated route for /// [_i17.ProfileScoresPage] -class ProfileScoresRoute extends _i18.PageRouteInfo { +class ProfileScoresRoute extends _i19.PageRouteInfo { const ProfileScoresRoute() : super( ProfileScoresRoute.name, @@ -605,3 +703,15 @@ class ProfileScoresRoute extends _i18.PageRouteInfo { static const String name = 'ProfileScoresRoute'; } + +/// generated route for +/// [_i18.ProfileSettingsPage] +class ProfileSettingsRoute extends _i19.PageRouteInfo { + const ProfileSettingsRoute() + : super( + ProfileSettingsRoute.name, + path: 'settings', + ); + + static const String name = 'ProfileSettingsRoute'; +} diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index d9c2959e..288b55d1 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -2,8 +2,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/app_cubit/app_cubit.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:salomon_bottom_bar/salomon_bottom_bar.dart'; class HomePage extends StatelessWidget { @@ -11,25 +11,21 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { + AutoRouter.of(context); // <-- this is needed to initialize the router return BlocConsumer(builder: (context, state) { if (state is AppClean) { - return AutoTabsRouter( + return AutoTabsScaffold( routes: const [ NewsRouter(), ScheduleRouter(), MapRoute(), ProfileRouter() ], - navigatorObservers: () => [HeroController()], - builder: (context, child, animation) { - final tabsRouter = AutoTabsRouter.of(context); - - return Scaffold( - body: child, - bottomNavigationBar: AppBottomNavigationBar( - index: tabsRouter.activeIndex, - onClick: tabsRouter.setActiveIndex, - ), + navigatorObservers: () => [HeroController(), AutoRouteObserver()], + bottomNavigationBuilder: (context, tabsRouter) { + return AppBottomNavigationBar( + index: tabsRouter.activeIndex, + onClick: tabsRouter.setActiveIndex, ); }, ); @@ -56,7 +52,7 @@ class AppBottomNavigationBar extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - color: DarkThemeColors.background01, + color: AppTheme.colors.background01, child: SalomonBottomBar( margin: const EdgeInsets.symmetric( horizontal: 10, @@ -68,22 +64,22 @@ class AppBottomNavigationBar extends StatelessWidget { SalomonBottomBarItem( icon: const Icon(Icons.library_books_rounded), title: const Text("Новости"), - selectedColor: DarkThemeColors.primary, + selectedColor: AppTheme.colors.primary, ), SalomonBottomBarItem( icon: const Icon(Icons.calendar_today_rounded), title: const Text("Расписание"), - selectedColor: DarkThemeColors.primary, + selectedColor: AppTheme.colors.primary, ), SalomonBottomBarItem( icon: const Icon(Icons.map_rounded), title: const Text("Карта"), - selectedColor: DarkThemeColors.primary, + selectedColor: AppTheme.colors.primary, ), SalomonBottomBarItem( icon: const Icon(Icons.person), title: const Text("Профиль"), - selectedColor: DarkThemeColors.primary, + selectedColor: AppTheme.colors.primary, ), ], ), diff --git a/lib/presentation/pages/login/login_page.dart b/lib/presentation/pages/login/login_page.dart index f9347c9f..fb9a40fc 100644 --- a/lib/presentation/pages/login/login_page.dart +++ b/lib/presentation/pages/login/login_page.dart @@ -2,10 +2,10 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/forms/labelled_input.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class LoginPage extends StatefulWidget { const LoginPage({Key? key}) : super(key: key); @@ -38,20 +38,20 @@ class _LoginPageState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ const SizedBox(height: 40), - Text("Вход", style: DarkTextTheme.h4), + Text("Вход", style: AppTextStyle.h4), const SizedBox(height: 13), RichText( text: TextSpan( text: 'Используйте ваши данные от ', - style: DarkTextTheme.bodyRegular - .copyWith(color: DarkThemeColors.deactive), + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.deactive), children: [ TextSpan( - text: 'lk.mirea.ru', style: DarkTextTheme.bodyBold), + text: 'lk.mirea.ru', style: AppTextStyle.bodyBold), TextSpan( text: " для входа.", - style: DarkTextTheme.bodyRegular - .copyWith(color: DarkThemeColors.deactive)), + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.deactive)), ], ), ), @@ -81,8 +81,8 @@ class _LoginPageState extends State { padding: const EdgeInsets.only(top: 10), child: Text( state.cause, - style: DarkTextTheme.bodyRegular - .copyWith(color: DarkThemeColors.colorful07), + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.colorful07), ), ); } diff --git a/lib/presentation/pages/map/map_page.dart b/lib/presentation/pages/map/map_page.dart index 21442671..2fddb35d 100644 --- a/lib/presentation/pages/map/map_page.dart +++ b/lib/presentation/pages/map/map_page.dart @@ -4,12 +4,11 @@ import 'package:flutter_svg/svg.dart'; import 'package:material_floating_search_bar/material_floating_search_bar.dart'; import 'package:photo_view/photo_view.dart'; import 'package:rtu_mirea_app/presentation/bloc/map_cubit/map_cubit.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/map/widgets/map_scaling_button.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:implicitly_animated_reorderable_list/implicitly_animated_reorderable_list.dart'; import 'package:implicitly_animated_reorderable_list/transitions.dart'; - +import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'widgets/map_navigation_button.dart'; import 'widgets/search_item_button.dart'; @@ -57,7 +56,7 @@ class _MapPageState extends State { Container( height: double.infinity, width: double.infinity, - color: DarkThemeColors.background01, + color: AppTheme.colors.background01, ), _buildMap(), Align( @@ -83,11 +82,11 @@ class _MapPageState extends State { // ignore: unused_element Widget _buildSearchBar() { return FloatingSearchBar( - accentColor: DarkThemeColors.primary, - iconColor: DarkThemeColors.white, - backgroundColor: DarkThemeColors.background02, + accentColor: AppTheme.colors.primary, + iconColor: AppTheme.colors.white, + backgroundColor: AppTheme.colors.background02, hint: 'Что будем искать, Милорд?', - hintStyle: DarkTextTheme.titleS.copyWith(color: DarkThemeColors.deactive), + hintStyle: AppTextStyle.titleS.copyWith(color: AppTheme.colors.deactive), borderRadius: const BorderRadius.all(Radius.circular(12)), onQueryChanged: (query) { context.read().setSearchQuery(query); @@ -96,7 +95,7 @@ class _MapPageState extends State { return Padding( padding: const EdgeInsets.only(top: 16.0), child: Material( - color: DarkThemeColors.background03, + color: AppTheme.colors.background03, elevation: 4.0, borderRadius: BorderRadius.circular(8), child: BlocBuilder( @@ -152,7 +151,7 @@ class _MapPageState extends State { maxScale: maxScale, minScale: minScale, backgroundDecoration: - const BoxDecoration(color: DarkThemeColors.background01), + BoxDecoration(color: AppTheme.colors.background01), child: floors[state.floor], ), ); @@ -161,7 +160,7 @@ class _MapPageState extends State { Widget _buildScalingButton() { return Container( decoration: BoxDecoration( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, borderRadius: BorderRadius.circular(12), ), child: MapScalingButton( @@ -180,7 +179,7 @@ class _MapPageState extends State { Widget _buildNavigation() { return Container( decoration: BoxDecoration( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, borderRadius: BorderRadius.circular(12), ), child: Column( diff --git a/lib/presentation/pages/map/widgets/map_navigation_button.dart b/lib/presentation/pages/map/widgets/map_navigation_button.dart index 6b4d7679..c5094eb7 100644 --- a/lib/presentation/pages/map/widgets/map_navigation_button.dart +++ b/lib/presentation/pages/map/widgets/map_navigation_button.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/map_cubit/map_cubit.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class MapNavigationButton extends StatelessWidget { const MapNavigationButton( @@ -22,15 +23,22 @@ class MapNavigationButton extends StatelessWidget { style: ButtonStyle( backgroundColor: MaterialStateProperty.all( state.floor == floor - ? DarkThemeColors.background03 - : DarkThemeColors.background02, + ? AppTheme.colors.background03 + : AppTheme.colors.background02, ), shadowColor: MaterialStateProperty.all(Colors.transparent), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ))), - child: Text(floor.toString()), + child: Text( + floor.toString(), + style: AppTextStyle.buttonS.copyWith( + color: state.floor == floor + ? AppTheme.colors.active + : AppTheme.colors.active.withOpacity(0.5), + ), + ), onPressed: () => onClick(), ), ), diff --git a/lib/presentation/pages/map/widgets/map_scaling_button.dart b/lib/presentation/pages/map/widgets/map_scaling_button.dart index 689f9a99..ca3eec07 100644 --- a/lib/presentation/pages/map/widgets/map_scaling_button.dart +++ b/lib/presentation/pages/map/widgets/map_scaling_button.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:photo_view/photo_view.dart'; import 'package:rtu_mirea_app/presentation/bloc/map_cubit/map_cubit.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class MapScalingButton extends StatelessWidget { const MapScalingButton( @@ -36,15 +37,20 @@ class MapScalingButton extends StatelessWidget { child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all( - DarkThemeColors.background02), + AppTheme.colors.background02), shadowColor: MaterialStateProperty.all(Colors.transparent), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ))), child: FittedBox( - fit: BoxFit.fitWidth, - child: Text("${_calculateScalePercentage()}%")), + fit: BoxFit.fitWidth, + child: Text( + "${_calculateScalePercentage()}%", + style: + AppTextStyle.buttonS.copyWith(color: AppTheme.colors.active), + ), + ), onPressed: () => onClick(), ), ), diff --git a/lib/presentation/pages/map/widgets/search_item_button.dart b/lib/presentation/pages/map/widgets/search_item_button.dart index a1d5e89b..a87a5518 100644 --- a/lib/presentation/pages/map/widgets/search_item_button.dart +++ b/lib/presentation/pages/map/widgets/search_item_button.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class SearchItemButton extends StatelessWidget { const SearchItemButton({Key? key, required this.room, required this.onClick}) @@ -22,7 +22,7 @@ class SearchItemButton extends StatelessWidget { children: [ Text( room['t'], - style: DarkTextTheme.buttonS, + style: AppTextStyle.buttonS, textAlign: TextAlign.left, ), ], diff --git a/lib/presentation/pages/news/news_details_page.dart b/lib/presentation/pages/news/news_details_page.dart index f5b94e63..3c664305 100644 --- a/lib/presentation/pages/news/news_details_page.dart +++ b/lib/presentation/pages/news/news_details_page.dart @@ -5,11 +5,11 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'package:rtu_mirea_app/common/utils/strapi_utils.dart'; import 'package:rtu_mirea_app/domain/entities/news_item.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/news/widgets/tags_widgets.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/images_horizontal_slider.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class NewsDetailsPage extends StatelessWidget { const NewsDetailsPage({Key? key, required this.newsItem}) : super(key: key); @@ -18,7 +18,7 @@ class NewsDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ @@ -44,7 +44,7 @@ class NewsDetailsPage extends StatelessWidget { body: SafeArea( bottom: false, child: Container( - color: DarkThemeColors.background01, + color: AppTheme.colors.background01, child: ListView( padding: EdgeInsets.zero, children: [ @@ -56,7 +56,7 @@ class NewsDetailsPage extends StatelessWidget { children: [ Text( newsItem.title, - style: DarkTextTheme.h5, + style: AppTextStyle.h5, ), const SizedBox(height: 24), _NewsItemInfo(tags: newsItem.tags, date: newsItem.date), @@ -67,7 +67,7 @@ class NewsDetailsPage extends StatelessWidget { data: newsItem.text, style: { "body": Style( - fontStyle: DarkTextTheme.bodyRegular.fontStyle), + fontStyle: AppTextStyle.bodyRegular.fontStyle), }, customRenders: { // iframeRenderer to display the YouTube video player @@ -132,14 +132,14 @@ class _NewsItemInfo extends StatelessWidget { children: [ Text( "Дата", - style: DarkTextTheme.body - .copyWith(color: DarkThemeColors.deactive), + style: AppTextStyle.body + .copyWith(color: AppTheme.colors.deactive), ), const SizedBox(height: 4), Text( DateFormat.MMMd('ru_RU').format(date).toString(), - style: DarkTextTheme.titleM - .copyWith(color: DarkThemeColors.colorful02), + style: AppTextStyle.titleM + .copyWith(color: AppTheme.colors.colorful02), ), ], ), diff --git a/lib/presentation/pages/news/news_page.dart b/lib/presentation/pages/news/news_page.dart index 1343b1e2..25fa882c 100644 --- a/lib/presentation/pages/news/news_page.dart +++ b/lib/presentation/pages/news/news_page.dart @@ -6,14 +6,14 @@ import 'package:rtu_mirea_app/domain/entities/news_item.dart'; import 'package:rtu_mirea_app/domain/entities/story.dart'; import 'package:rtu_mirea_app/presentation/bloc/news_bloc/news_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/stories_bloc/stories_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/app_settings_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_tab_button.dart'; import 'package:shimmer/shimmer.dart'; import 'widgets/news_item.dart'; import 'widgets/story_item.dart'; import 'widgets/tags_widgets.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class NewsPage extends StatefulWidget { const NewsPage({Key? key}) : super(key: key); @@ -45,7 +45,7 @@ class _NewsPageState extends State { builder: (ctx) => Material( child: Container( padding: const EdgeInsets.all(8), - color: DarkThemeColors.background03, + color: AppTheme.colors.background03, child: Padding( padding: const EdgeInsets.only(top: 4, bottom: 16, left: 24, right: 24), @@ -152,11 +152,10 @@ class _NewsPageState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: AppTheme.colors.background01, appBar: AppBar( title: const Text("Новости"), - backgroundColor: DarkThemeColors.background01, ), - backgroundColor: DarkThemeColors.background01, body: NestedScrollView( controller: _scrollController, headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { @@ -236,7 +235,7 @@ class _NewsPageState extends State { child: Text( 'Произошла ошибка при загрузке новостей.', textAlign: TextAlign.center, - style: DarkTextTheme.title, + style: AppTextStyle.title, ), ), ); @@ -297,7 +296,7 @@ class _ShimmerNewsCardLoading extends StatelessWidget { return Container( margin: const EdgeInsets.only(bottom: 24, left: 16, right: 16), decoration: BoxDecoration( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, borderRadius: BorderRadius.circular(16), ), child: Padding( @@ -307,12 +306,12 @@ class _ShimmerNewsCardLoading extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Shimmer.fromColors( - baseColor: DarkThemeColors.background01, - highlightColor: DarkThemeColors.background02, + baseColor: AppTheme.colors.background01, + highlightColor: AppTheme.colors.background02, child: Container( height: 175, decoration: BoxDecoration( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, borderRadius: BorderRadius.circular(16), ), ), @@ -323,25 +322,25 @@ class _ShimmerNewsCardLoading extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Shimmer.fromColors( - baseColor: DarkThemeColors.background01, - highlightColor: DarkThemeColors.background02, + baseColor: AppTheme.colors.background01, + highlightColor: AppTheme.colors.background02, child: Container( height: 18, decoration: BoxDecoration( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, borderRadius: BorderRadius.circular(16), ), ), ), const SizedBox(height: 4), Shimmer.fromColors( - baseColor: DarkThemeColors.background01, - highlightColor: DarkThemeColors.background02, + baseColor: AppTheme.colors.background01, + highlightColor: AppTheme.colors.background02, child: Container( height: 18, width: MediaQuery.of(context).size.width * 0.4, decoration: BoxDecoration( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, borderRadius: BorderRadius.circular(16), ), ), diff --git a/lib/presentation/pages/news/widgets/news_item.dart b/lib/presentation/pages/news/widgets/news_item.dart index 2fe3f4aa..22d760b9 100644 --- a/lib/presentation/pages/news/widgets/news_item.dart +++ b/lib/presentation/pages/news/widgets/news_item.dart @@ -5,11 +5,11 @@ import 'package:flutter/material.dart'; import 'package:rtu_mirea_app/common/utils/strapi_utils.dart'; import 'package:rtu_mirea_app/domain/entities/news_item.dart'; import 'package:intl/intl.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:rtu_mirea_app/presentation/pages/news/widgets/tags_widgets.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class NewsItemWidget extends StatelessWidget { final NewsItem newsItem; @@ -33,7 +33,7 @@ class NewsItemWidget extends StatelessWidget { margin: const EdgeInsets.only(bottom: 24, left: 16, right: 16), width: MediaQuery.of(context).size.width, decoration: BoxDecoration( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, borderRadius: BorderRadius.circular(16), ), child: Padding( @@ -60,13 +60,13 @@ class NewsItemWidget extends StatelessWidget { borderRadius: BorderRadius.circular(16), ), child: Shimmer.fromColors( - baseColor: DarkThemeColors.background03, + baseColor: AppTheme.colors.background03, highlightColor: - DarkThemeColors.background03.withOpacity(0.5), + AppTheme.colors.background03.withOpacity(0.5), child: Container( height: double.infinity, width: double.infinity, - color: DarkThemeColors.background03, + color: AppTheme.colors.background03, ), ), ); @@ -110,13 +110,13 @@ class NewsItemWidget extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 16), child: Text(newsItem.title, - textAlign: TextAlign.start, style: DarkTextTheme.titleM), + textAlign: TextAlign.start, style: AppTextStyle.titleM), ), const SizedBox(height: 4), Text((DateFormat.yMMMd('ru_RU').format(newsItem.date).toString()), textAlign: TextAlign.start, - style: DarkTextTheme.captionL - .copyWith(color: DarkThemeColors.secondary)), + style: AppTextStyle.captionL + .copyWith(color: AppTheme.colors.secondary)), newsItem.tags.isNotEmpty ? const SizedBox(height: 16) : Container(), diff --git a/lib/presentation/pages/news/widgets/stories_wrapper.dart b/lib/presentation/pages/news/widgets/stories_wrapper.dart index a8703cb5..ce45cb0c 100644 --- a/lib/presentation/pages/news/widgets/stories_wrapper.dart +++ b/lib/presentation/pages/news/widgets/stories_wrapper.dart @@ -5,9 +5,9 @@ import 'package:flutter/material.dart'; import 'package:rtu_mirea_app/common/utils/utils.dart'; import 'package:story/story_page_view/story_page_view.dart'; import 'package:rtu_mirea_app/domain/entities/story.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_button.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class StoriesWrapper extends StatefulWidget { const StoriesWrapper({ @@ -96,7 +96,7 @@ class _StoriesWrapperState extends State { Material( type: MaterialType.transparency, child: - Text(author.name, style: DarkTextTheme.bodyBold), + Text(author.name, style: AppTextStyle.bodyBold), ), ], ), @@ -115,7 +115,7 @@ class _StoriesWrapperState extends State { children: [ Text( page.title!, - style: DarkTextTheme.h4, + style: AppTextStyle.h4, ), const SizedBox(height: 16) ], @@ -123,7 +123,7 @@ class _StoriesWrapperState extends State { if (page.text != null) Text( page.text!, - style: DarkTextTheme.bodyBold, + style: AppTextStyle.bodyBold, ) ], ), diff --git a/lib/presentation/pages/news/widgets/story_item.dart b/lib/presentation/pages/news/widgets/story_item.dart index c2e63868..91e37742 100644 --- a/lib/presentation/pages/news/widgets/story_item.dart +++ b/lib/presentation/pages/news/widgets/story_item.dart @@ -1,9 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:rtu_mirea_app/domain/entities/story.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class StoryWidget extends StatelessWidget { const StoryWidget({Key? key, required this.stories, required this.storyIndex}) @@ -38,7 +38,7 @@ class StoryWidget extends StatelessWidget { : stories[storyIndex].preview.formats!.thumbnail.url) : NetworkImage(stories[storyIndex].preview.url), colorFilter: ColorFilter.mode( - DarkThemeColors.background02.withOpacity(0.15), + AppTheme.colors.background02.withOpacity(0.15), BlendMode.dstOut), ), ), @@ -46,7 +46,7 @@ class StoryWidget extends StatelessWidget { type: MaterialType.transparency, child: Text( stories[storyIndex].title, - style: DarkTextTheme.chip, + style: AppTextStyle.chip, ), ), ), diff --git a/lib/presentation/pages/news/widgets/tags_widgets.dart b/lib/presentation/pages/news/widgets/tags_widgets.dart index fb793ea9..0af7e0a8 100644 --- a/lib/presentation/pages/news/widgets/tags_widgets.dart +++ b/lib/presentation/pages/news/widgets/tags_widgets.dart @@ -1,7 +1,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class Tags extends StatelessWidget { final bool isClickable; @@ -45,7 +45,7 @@ class Tags extends StatelessWidget { child: Container( decoration: BoxDecoration( border: Border.all( - color: DarkThemeColors.colorful05, width: 2), + color: AppTheme.colors.colorful05, width: 2), borderRadius: BorderRadius.circular(16), ), child: Padding( @@ -53,8 +53,8 @@ class Tags extends StatelessWidget { vertical: 4, horizontal: 8), child: Text( element, - style: DarkTextTheme.body - .copyWith(color: DarkThemeColors.colorful05), + style: AppTextStyle.body + .copyWith(color: AppTheme.colors.colorful05), ), ), ), diff --git a/lib/presentation/pages/onboarding/onboarding_page.dart b/lib/presentation/pages/onboarding/onboarding_page.dart index 1567f2d4..3b63c9f0 100644 --- a/lib/presentation/pages/onboarding/onboarding_page.dart +++ b/lib/presentation/pages/onboarding/onboarding_page.dart @@ -3,12 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:rtu_mirea_app/presentation/bloc/app_cubit/app_cubit.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; - +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'widgets/indicator.dart'; import 'widgets/next_button.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; /// OnBoarding screen that greets new users class OnBoardingPage extends StatefulWidget { @@ -118,10 +117,10 @@ class _OnBoardingPageState extends State { children: [ Text( titlesTexts[index], - style: DarkTextTheme.h4, + style: AppTextStyle.h4, ), const SizedBox(height: 8.0), - Text(contentTexts[index], style: DarkTextTheme.bodyL), + Text(contentTexts[index], style: AppTextStyle.bodyL), ], ), ), @@ -147,7 +146,7 @@ class _OnBoardingPageState extends State { ); return Scaffold( - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -223,7 +222,9 @@ class _PageIndicatorsState extends State { }, child: Text( "Пропустить", - style: DarkTextTheme.buttonS, + style: AppTextStyle.buttonS.copyWith( + color: AppTheme.colors.active, + ), ), ), Row( diff --git a/lib/presentation/pages/onboarding/widgets/indicator.dart b/lib/presentation/pages/onboarding/widgets/indicator.dart index ab792eaf..9a7710d3 100644 --- a/lib/presentation/pages/onboarding/widgets/indicator.dart +++ b/lib/presentation/pages/onboarding/widgets/indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class IndicatorPageView extends StatelessWidget { const IndicatorPageView({Key? key, required this.isActive}) : super(key: key); @@ -14,7 +14,7 @@ class IndicatorPageView extends StatelessWidget { height: isActive ? 15.0 : 11.0, width: isActive ? 15.0 : 11.0, decoration: BoxDecoration( - color: isActive ? Colors.white : DarkThemeColors.active, + color: isActive ? Colors.white : AppTheme.colors.active, borderRadius: const BorderRadius.all(Radius.circular(12.0)), boxShadow: [ isActive diff --git a/lib/presentation/pages/onboarding/widgets/next_button.dart b/lib/presentation/pages/onboarding/widgets/next_button.dart index 8c95b5c9..87d2c6be 100644 --- a/lib/presentation/pages/onboarding/widgets/next_button.dart +++ b/lib/presentation/pages/onboarding/widgets/next_button.dart @@ -2,8 +2,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:provider/src/provider.dart'; import 'package:rtu_mirea_app/presentation/bloc/app_cubit/app_cubit.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; /// Get next button to open next page @@ -28,9 +28,9 @@ class NextPageViewButton extends StatelessWidget { } }, style: ElevatedButton.styleFrom( - foregroundColor: DarkThemeColors.primary.withOpacity(0.25), + foregroundColor: AppTheme.colors.primary.withOpacity(0.25), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), - backgroundColor: DarkThemeColors.primary, + backgroundColor: AppTheme.colors.primary, shadowColor: const Color(0x7f000000), elevation: 8.0, ), @@ -42,9 +42,11 @@ class NextPageViewButton extends StatelessWidget { child: isLastPage ? Text( "Начать!", - style: DarkTextTheme.buttonS, + style: AppTextStyle.buttonS.copyWith( + color: AppTheme.colors.white, + ), ) - : const Icon(Icons.arrow_forward_ios, color: DarkThemeColors.white), + : Icon(Icons.arrow_forward_ios, color: AppTheme.colors.active), ), ); } diff --git a/lib/presentation/pages/profile/about_app_page.dart b/lib/presentation/pages/profile/about_app_page.dart index 521c740b..447cb649 100644 --- a/lib/presentation/pages/profile/about_app_page.dart +++ b/lib/presentation/pages/profile/about_app_page.dart @@ -3,13 +3,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rtu_mirea_app/presentation/bloc/about_app_bloc/about_app_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/icon_button.dart'; import 'package:rtu_mirea_app/service_locator.dart'; import 'package:url_launcher/url_launcher_string.dart'; - +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'widgets/member_info.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class AboutAppPage extends StatelessWidget { const AboutAppPage({Key? key}) : super(key: key); @@ -19,9 +18,9 @@ class AboutAppPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: const Text("О приложении"), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, ), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, body: SafeArea( bottom: false, child: SingleChildScrollView( @@ -33,24 +32,25 @@ class AboutAppPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text('Open Source', style: DarkTextTheme.h4), + Text('Open Source', style: AppTextStyle.h4), Container( padding: const EdgeInsets.only( left: 8, right: 8, top: 4, bottom: 4), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(4)), - color: DarkThemeColors.primary, + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(4)), + color: AppTheme.colors.primary, ), child: Text( getIt().version, - style: DarkTextTheme.buttonS, + style: AppTextStyle.buttonS, ), ), ]), const SizedBox(height: 8), Text( 'Это приложение и все относящиеся к нему сервисы являются 100% бесплатными и Open Source продуктами. Мы с огромным удовольствием примем любые ваши предложения и сообщения, а также мы рады любому вашему участию в проекте!', - style: DarkTextTheme.bodyRegular, + style: AppTextStyle.bodyRegular, ), const SizedBox(height: 8), RichText( @@ -58,12 +58,14 @@ class AboutAppPage extends StatelessWidget { children: [ TextSpan( text: 'Карта для приложения взята из сервиса ', - style: DarkTextTheme.bodyRegular, + style: AppTextStyle.bodyRegular.copyWith( + color: AppTheme.colors.active, + ), ), TextSpan( text: 'Indoor Schemes', - style: DarkTextTheme.bodyRegular - .copyWith(color: DarkThemeColors.primary), + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.primary), recognizer: TapGestureRecognizer() ..onTap = () { launchUrlString('https://ischemes.ru/'); @@ -78,12 +80,14 @@ class AboutAppPage extends StatelessWidget { children: [ TextSpan( text: 'Все новости берутся из официального сайта ', - style: DarkTextTheme.bodyRegular, + style: AppTextStyle.bodyRegular.copyWith( + color: AppTheme.colors.active, + ), ), TextSpan( text: 'mirea.ru/news', - style: DarkTextTheme.bodyRegular - .copyWith(color: DarkThemeColors.primary), + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.primary), recognizer: TapGestureRecognizer() ..onTap = () { launchUrlString('https://mirea.ru/news/'); @@ -95,7 +99,7 @@ class AboutAppPage extends StatelessWidget { const SizedBox(height: 8), Text( 'Связаться с нами вы можете с помощью email: contact@mirea.ninja', - style: DarkTextTheme.bodyRegular, + style: AppTextStyle.bodyRegular, ), const SizedBox(height: 8), RichText( @@ -103,12 +107,14 @@ class AboutAppPage extends StatelessWidget { children: [ TextSpan( text: 'Powered by ', - style: DarkTextTheme.bodyRegular, + style: AppTextStyle.bodyRegular.copyWith( + color: AppTheme.colors.active, + ), ), TextSpan( text: 'Mirea Ninja', - style: DarkTextTheme.bodyRegular - .copyWith(color: DarkThemeColors.primary), + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.primary), recognizer: TapGestureRecognizer() ..onTap = () { launchUrlString("https://mirea.ninja/"); @@ -121,29 +127,43 @@ class AboutAppPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SocialIconButton( - assetImage: const AssetImage('assets/icons/github.png'), - onClick: () { - launchUrlString( - 'https://github.com/Ninja-Official/rtu-mirea-mobile'); - }), + SizedBox( + height: 40, + width: 90, + child: SocialIconButton( + assetImage: const AssetImage('assets/icons/github.png'), + onClick: () { + launchUrlString( + 'https://github.com/Ninja-Official/rtu-mirea-mobile'); + }), + ), const SizedBox(width: 12), - SocialIconButton( - assetImage: const AssetImage('assets/icons/patreon.png'), - onClick: () { - launchUrlString('https://www.patreon.com/mireaninja'); - }), + SizedBox( + height: 40, + width: 90, + child: SocialIconButton( + assetImage: + const AssetImage('assets/icons/patreon.png'), + onClick: () { + launchUrlString('https://www.patreon.com/mireaninja'); + }), + ), const SizedBox(width: 12), - SocialIconButton( - assetImage: const AssetImage('assets/icons/telegram.png'), - onClick: () { - launchUrlString( - 'https://t.me/joinchat/LyM7jcoRXUhmOGM6'); - }), + SizedBox( + height: 40, + width: 90, + child: SocialIconButton( + assetImage: + const AssetImage('assets/icons/telegram.png'), + onClick: () { + launchUrlString( + 'https://t.me/joinchat/LyM7jcoRXUhmOGM6'); + }), + ), ], ), const SizedBox(height: 24), - Text('Контрибьюторы', style: DarkTextTheme.h4), + Text('Контрибьюторы', style: AppTextStyle.h4), const SizedBox(height: 16), BlocBuilder( buildWhen: (prevState, currentState) { @@ -154,9 +174,9 @@ class AboutAppPage extends StatelessWidget { }, builder: (context, state) { if (state is AboutAppMembersLoading) { - return const Center( + return Center( child: CircularProgressIndicator( - backgroundColor: DarkThemeColors.primary, + backgroundColor: AppTheme.colors.primary, strokeWidth: 5, ), ); @@ -177,7 +197,7 @@ class AboutAppPage extends StatelessWidget { return Center( child: Text( 'Произошла ошибка при загрузке разработчиков. Проверьте ваше интернет-соединение.', - style: DarkTextTheme.title, + style: AppTextStyle.title, ), ); } @@ -185,7 +205,7 @@ class AboutAppPage extends StatelessWidget { }, ), const SizedBox(height: 24), - Text('Патроны', style: DarkTextTheme.h4), + Text('Патроны', style: AppTextStyle.h4), const SizedBox(height: 16), BlocBuilder( buildWhen: (prevState, currentState) { @@ -196,9 +216,9 @@ class AboutAppPage extends StatelessWidget { }, builder: (context, state) { if (state is AboutAppMembersLoading) { - return const Center( + return Center( child: CircularProgressIndicator( - backgroundColor: DarkThemeColors.primary, + backgroundColor: AppTheme.colors.primary, strokeWidth: 5, ), ); @@ -220,7 +240,7 @@ class AboutAppPage extends StatelessWidget { return Center( child: Text( 'Произошла ошибка при загрузке патронов.', - style: DarkTextTheme.title, + style: AppTextStyle.title, ), ); } diff --git a/lib/presentation/pages/profile/profile_announces_page.dart b/lib/presentation/pages/profile/profile_announces_page.dart index a73ecbdc..7e65d20a 100644 --- a/lib/presentation/pages/profile/profile_announces_page.dart +++ b/lib/presentation/pages/profile/profile_announces_page.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:rtu_mirea_app/presentation/bloc/announces_bloc/announces_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class ProfileAnnouncesPage extends StatelessWidget { const ProfileAnnouncesPage({Key? key}) : super(key: key); @@ -14,9 +14,9 @@ class ProfileAnnouncesPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: const Text("Объявления"), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, ), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, body: SafeArea( bottom: false, child: BlocBuilder( @@ -26,40 +26,51 @@ class ProfileAnnouncesPage extends StatelessWidget { } else if (state is AnnouncesLoaded) { return Padding( padding: const EdgeInsets.only(top: 24), - child: ListView.builder( + child: ListView.separated( itemCount: state.announces.length, + separatorBuilder: (context, index) => + const SizedBox(height: 16), itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - state.announces[index].name, - style: DarkTextTheme.title, - ), - const SizedBox(height: 6), - Text( - state.announces[index].date, - style: DarkTextTheme.bodyRegular - .copyWith(color: DarkThemeColors.deactive), - ), - const SizedBox(height: 8), - Html( - data: state.announces[index].text, - style: { - "body": Style( - fontStyle: DarkTextTheme.bodyRegular.fontStyle), - }, - onLinkTap: (String? url, context, attributes, element) { - if (url != null) { - launchUrlString(url); - } - }, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Card( + color: AppTheme.colors.background03, + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + state.announces[index].name, + style: AppTextStyle.title, + ), + const SizedBox(height: 6), + Text( + state.announces[index].date, + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.deactive), + ), + const SizedBox(height: 8), + Html( + data: state.announces[index].text, + style: { + "body": Style( + fontStyle: AppTextStyle.bodyRegular.fontStyle, + fontWeight: AppTextStyle.bodyRegular.fontWeight, + padding: const EdgeInsets.all(0), + margin: Margins.all(0), + ), + }, + onLinkTap: + (String? url, context, attributes, element) { + if (url != null) { + launchUrlString(url); + } + }, + ), + ], ), - const SizedBox(height: 13), - const Divider(), - const SizedBox(height: 20), - ], + ), ), ), ), diff --git a/lib/presentation/pages/profile/profile_attendance_page.dart b/lib/presentation/pages/profile/profile_attendance_page.dart index ade2f76e..44dd8745 100644 --- a/lib/presentation/pages/profile/profile_attendance_page.dart +++ b/lib/presentation/pages/profile/profile_attendance_page.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/attendance_bloc/attendance_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/widgets/attendance_card.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:intl/intl.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/select_range_date_button.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class ProfileAttendancePage extends StatefulWidget { const ProfileAttendancePage({Key? key}) : super(key: key); @@ -39,9 +39,7 @@ class _ProfileAttendancePageState extends State { return Scaffold( appBar: AppBar( title: const Text("Посещаемость"), - backgroundColor: DarkThemeColors.background01, ), - backgroundColor: DarkThemeColors.background01, body: SafeArea( bottom: false, child: BlocBuilder( @@ -60,7 +58,7 @@ class _ProfileAttendancePageState extends State { return Center( child: Text( "Произошла ошибка при попытке загрузить посещаемость. Повторите попытку позже", - style: DarkTextTheme.body), + style: AppTextStyle.body), ); } return Column( @@ -68,7 +66,9 @@ class _ProfileAttendancePageState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Промежуток: ', style: DarkTextTheme.body), + Text('Промежуток: ', + style: AppTextStyle.body + .copyWith(color: AppTheme.colors.active)), const SizedBox(width: 16), SelectRangeDateButton( initialRange: PickerDateRange( @@ -77,10 +77,12 @@ class _ProfileAttendancePageState extends State { text: "с ${_getTextDates(_getFirstAndLastWeekDaysText())[0]} по ${_getTextDates(_getFirstAndLastWeekDaysText())[1]}", onDateSelected: (date) { - context.read().add(LoadAttendance( - token: authState.token, - startDate: _getTextDates(date)[0], - endDate: _getTextDates(date)[1])); + context.read().add( + LoadAttendance( + token: authState.token, + startDate: _getTextDates(date)[0], + endDate: _getTextDates(date)[1]), + ); }, ), ], @@ -96,7 +98,7 @@ class _ProfileAttendancePageState extends State { children: [ const SizedBox(height: 8), Text('Дней посещено: ${state.visitsCount}', - style: DarkTextTheme.body), + style: AppTextStyle.body), const SizedBox(height: 8), Expanded( child: ListView.builder( @@ -125,7 +127,7 @@ class _ProfileAttendancePageState extends State { const EdgeInsets.symmetric(horizontal: 24), child: Text( "Ничего не найдено. Выберите другой промежуток времени", - style: DarkTextTheme.body, + style: AppTextStyle.body, ), ), ), diff --git a/lib/presentation/pages/profile/profile_detail_page.dart b/lib/presentation/pages/profile/profile_detail_page.dart index 77adbb86..8f952f7d 100644 --- a/lib/presentation/pages/profile/profile_detail_page.dart +++ b/lib/presentation/pages/profile/profile_detail_page.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:rtu_mirea_app/domain/entities/user.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/badged_container.dart'; import 'package:rtu_mirea_app/presentation/widgets/copy_text_block.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class ProfileDetailPage extends StatelessWidget { const ProfileDetailPage({ @@ -19,9 +19,9 @@ class ProfileDetailPage extends StatelessWidget { return Scaffold( appBar: AppBar( title: const Text("Детали профиля"), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, ), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, body: SafeArea( bottom: false, child: SingleChildScrollView( @@ -43,7 +43,7 @@ class ProfileDetailPage extends StatelessWidget { Center( child: Text( '${user.lastName} ${user.name} ${user.secondName}', - style: DarkTextTheme.h6, + style: AppTextStyle.h6, ), ), const SizedBox(height: 20), diff --git a/lib/presentation/pages/profile/profile_lectors_page.dart b/lib/presentation/pages/profile/profile_lectors_page.dart index aa05db51..d1f4fa0c 100644 --- a/lib/presentation/pages/profile/profile_lectors_page.dart +++ b/lib/presentation/pages/profile/profile_lectors_page.dart @@ -4,8 +4,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:material_floating_search_bar/material_floating_search_bar.dart'; import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/employee_bloc/employee_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/widgets/lector_search_card.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; final GlobalKey _searchBarKey = @@ -38,10 +38,10 @@ class _ProfileLectrosPageState extends State { return Scaffold( appBar: AppBar( title: const Text("Преподаватели"), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, automaticallyImplyLeading: false, ), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, body: SafeArea( bottom: false, child: BlocBuilder( @@ -52,11 +52,12 @@ class _ProfileLectrosPageState extends State { return FloatingSearchBar( key: _searchBarKey, controller: _controller, - backdropColor: DarkThemeColors.background01, - shadowColor: DarkThemeColors.background03, - accentColor: DarkThemeColors.primary, - iconColor: DarkThemeColors.white, - backgroundColor: DarkThemeColors.background02, + elevation: 0, + backdropColor: AppTheme.colors.background01, + shadowColor: AppTheme.colors.background03, + accentColor: AppTheme.colors.primary, + iconColor: AppTheme.colors.active, + backgroundColor: AppTheme.colors.background02, hint: 'Начните вводить фамилию', body: _controller.isOpen == false || _controller.isHidden ? Column( @@ -66,12 +67,13 @@ class _ProfileLectrosPageState extends State { size: 85), const SizedBox(height: 24), Text('Здесь появятся результаты поиска', - style: DarkTextTheme.body) + style: AppTextStyle.body.copyWith( + color: AppTheme.colors.deactive)), ], ) : Container(), - hintStyle: DarkTextTheme.titleS - .copyWith(color: DarkThemeColors.deactive), + hintStyle: AppTextStyle.titleS + .copyWith(color: AppTheme.colors.deactive), borderRadius: const BorderRadius.all(Radius.circular(12)), scrollPadding: const EdgeInsets.only(top: 16, bottom: 56), transitionDuration: const Duration(milliseconds: 800), @@ -104,7 +106,7 @@ class _ProfileLectrosPageState extends State { return ClipRRect( borderRadius: BorderRadius.circular(8), child: Material( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, elevation: 4.0, child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/presentation/pages/profile/profile_page.dart b/lib/presentation/pages/profile/profile_page.dart index 525a3275..1a3a6963 100644 --- a/lib/presentation/pages/profile/profile_page.dart +++ b/lib/presentation/pages/profile/profile_page.dart @@ -2,18 +2,18 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/colorful_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/icon_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/settings_button.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import '../../bloc/announces_bloc/announces_bloc.dart'; import '../../bloc/profile_bloc/profile_bloc.dart'; -import '../../theme.dart'; import '../../widgets/buttons/text_outlined_button.dart'; import '../../widgets/container_label.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class ProfilePage extends StatefulWidget { const ProfilePage({Key? key}) : super(key: key); @@ -28,9 +28,8 @@ class _ProfilePageState extends State { return Scaffold( appBar: AppBar( title: const Text("Профиль"), - backgroundColor: DarkThemeColors.background01, ), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, body: SafeArea( bottom: false, child: LayoutBuilder( @@ -63,18 +62,18 @@ class _ProfilePageState extends State { const EdgeInsets.only(top: 13, bottom: 4), child: Text( '${profileState.user.name} ${profileState.user.lastName}', - style: DarkTextTheme.h5, + style: AppTextStyle.h5, ), ), ShaderMask( shaderCallback: (bounds) => - DarkThemeColors.gradient07.createShader( + AppTheme.colors.gradient07.createShader( Rect.fromLTWH( 0, 0, bounds.width, bounds.height), ), child: Text( profileState.user.login, - style: DarkTextTheme.titleS, + style: AppTextStyle.titleS, ), ), const SizedBox(height: 12), @@ -97,12 +96,13 @@ class _ProfilePageState extends State { assetImage: const AssetImage( 'assets/icons/gerb.ico'), onClick: () { - launchUrl(Uri.parse( - profileState.user.authShortlink != - null - ? "https://lk.mirea.ru/auth/link/?url=${profileState.user.authShortlink!}" - : "https://lk.mirea.ru/auth", - )); + launchUrlString( + profileState.user.authShortlink != + null + ? "https://lk.mirea.ru/auth/link/?url=${profileState.user.authShortlink!}" + : "https://lk.mirea.ru/auth", + mode: LaunchMode + .externalApplication); }, text: "Вход в ЛКС", ), @@ -156,12 +156,20 @@ class _ProfilePageState extends State { context.router.push(const AboutAppRoute()), ), const SizedBox(height: 8), + SettingsButton( + text: 'Настройки', + icon: Icons.settings_rounded, + onClick: () => { + context.router + .push(const ProfileSettingsRoute()), + }), + const SizedBox(height: 8), ColorfulButton( text: 'Выйти', onClick: () => context .read() .add(AuthLogOut()), - backgroundColor: DarkThemeColors.colorful07), + backgroundColor: AppTheme.colors.colorful07), ], ); } else if (profileState is ProfileLoading) { @@ -223,13 +231,19 @@ class _InitialProfileStatePage extends StatelessWidget { // builder: (context) => const BottomErrorInfo(), // ); }, - backgroundColor: DarkThemeColors.colorful03), + backgroundColor: AppTheme.colors.colorful03), const SizedBox(height: 8), SettingsButton( text: 'О приложении', icon: Icons.apps_rounded, onClick: () => context.router.push(const AboutAppRoute()), ), + const SizedBox(height: 8), + SettingsButton( + text: 'Настройки', + icon: Icons.settings_rounded, + onClick: () => context.router.push(const ProfileSettingsRoute()), + ), ], ); } diff --git a/lib/presentation/pages/profile/profile_scores_page.dart b/lib/presentation/pages/profile/profile_scores_page.dart index 9a5b0f89..bd4bc252 100644 --- a/lib/presentation/pages/profile/profile_scores_page.dart +++ b/lib/presentation/pages/profile/profile_scores_page.dart @@ -3,17 +3,17 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/domain/entities/score.dart'; import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/widgets/scores_chart_modal.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_tab_button.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; Color getColorByResult(String result) { result = result.toLowerCase(); if (result.contains("неуваж")) { - return DarkThemeColors.colorful07; + return AppTheme.colors.colorful07; } if (result.contains("зач")) { @@ -21,9 +21,9 @@ Color getColorByResult(String result) { } else if (result.contains("отл")) { return Colors.green; } else if (result.contains("хор")) { - return DarkThemeColors.colorful05; + return AppTheme.colors.colorful05; } else if (result.contains("удовл")) { - return DarkThemeColors.colorful06; + return AppTheme.colors.colorful06; } else { return Colors.white; } @@ -48,9 +48,9 @@ class _ProfileScoresPageState extends State { return Scaffold( appBar: AppBar( title: const Text("Зачётная книжка"), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, ), - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, body: SafeArea( bottom: false, child: BlocBuilder( @@ -103,7 +103,7 @@ class _ProfileScoresPageState extends State { children: [ Text( "Всего предметов: ${state.scores[state.selectedSemester]!.length}", - style: DarkTextTheme.body, + style: AppTextStyle.body, ), Row(children: [ IconButton( @@ -148,7 +148,7 @@ class _ProfileScoresPageState extends State { return Center( child: Text( "Произошла ошибка при попытке загрузить посещаемость. Повторите попытку позже", - style: DarkTextTheme.body), + style: AppTextStyle.body), ); } return Container(); @@ -176,10 +176,11 @@ class _ScoresCardListView extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Card( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), + elevation: 0, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 13), child: Column( @@ -187,19 +188,18 @@ class _ScoresCardListView extends StatelessWidget { children: [ Text( score.type, - style: DarkTextTheme.body.copyWith( + style: AppTextStyle.body.copyWith( color: score.type.toLowerCase() == "экзамен" - ? DarkThemeColors.colorful04 - : DarkThemeColors.colorful02), + ? AppTheme.colors.colorful04 + : AppTheme.colors.colorful02), ), const SizedBox(height: 9), Text( score.subjectName, - style: DarkTextTheme.headline, + style: AppTextStyle.titleM, ), - const Divider( - color: DarkThemeColors.deactiveDarker, - thickness: 1, + Divider( + color: AppTheme.colors.deactiveDarker, height: 30, ), Row( @@ -211,7 +211,7 @@ class _ScoresCardListView extends StatelessWidget { const SizedBox(width: 10), Text( score.result, - style: DarkTextTheme.body + style: AppTextStyle.body .copyWith(color: getColorByResult(score.result)), ), ], @@ -226,7 +226,7 @@ class _ScoresCardListView extends StatelessWidget { Expanded( child: Text( score.comission ?? "", - style: DarkTextTheme.body, + style: AppTextStyle.body, ), ), ], @@ -238,7 +238,7 @@ class _ScoresCardListView extends StatelessWidget { Icons.calendar_month, ), const SizedBox(width: 10), - Text(score.year, style: DarkTextTheme.body), + Text(score.year, style: AppTextStyle.body), ], ), const SizedBox(height: 12), @@ -248,7 +248,7 @@ class _ScoresCardListView extends StatelessWidget { Icons.calendar_month, ), const SizedBox(width: 10), - Text(score.date, style: DarkTextTheme.body), + Text(score.date, style: AppTextStyle.body), ], ), ], diff --git a/lib/presentation/pages/profile/profile_settings_page.dart b/lib/presentation/pages/profile/profile_settings_page.dart new file mode 100644 index 00000000..7c426035 --- /dev/null +++ b/lib/presentation/pages/profile/profile_settings_page.dart @@ -0,0 +1,106 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:rtu_mirea_app/presentation/app_notifier.dart'; +import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; + +class ProfileSettingsPage extends StatelessWidget { + const ProfileSettingsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final router = AutoRouter.of(context); + + void rebuildRouterStack(StackRouter router) { + final tabsRouter = AutoTabsRouter.of(context); + + // Rebuild current router stack + router.popUntil((route) => false); + router.replaceAll([const ProfileRoute()]); + + final currentTabIndex = tabsRouter.activeIndex; + + // Rebuild tabs router stack + for (var i = 0; i < tabsRouter.pageCount; i++) { + if (i == currentTabIndex) continue; + final route = tabsRouter.stackRouterOfIndex(i); + final routeName = route?.current.name; + if (routeName == null) continue; + + route?.popUntil((route) => false); + route?.pushNamed(routeName); + } + } + + return Scaffold( + appBar: AppBar( + title: const Text("Настройки"), + ), + body: SafeArea( + bottom: false, + child: ListView( + children: [ + const SizedBox(height: 24), + ListTile( + title: Text("Тема", style: AppTextStyle.body), + leading: Icon(Icons.brightness_6, color: AppTheme.colors.active), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showDialog( + context: context, + builder: (context) => SimpleDialog( + title: Text("Выбор темы", style: AppTextStyle.titleS), + contentPadding: const EdgeInsets.all(16), + backgroundColor: AppTheme.colors.background02, + elevation: 0, + children: [ + ListTile( + title: Text("Светлая", style: AppTextStyle.body), + onTap: () { + context + .read() + .updateTheme(AppThemeType.light); + // Close dialog + Navigator.pop(context); + rebuildRouterStack(router); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + trailing: + AppTheme.defaultThemeType == AppThemeType.light + ? const Icon(Icons.check) + : null, + ), + const SizedBox(height: 8), + ListTile( + title: Text("Тёмная", style: AppTextStyle.body), + onTap: () { + context + .read() + .updateTheme(AppThemeType.dark); + // Close dialog + Navigator.pop(context); + rebuildRouterStack(router); + }, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + trailing: AppTheme.defaultThemeType == AppThemeType.dark + ? const Icon(Icons.check) + : null, + ), + ], + ), + ); + }, + ), + const Divider(), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/profile/widgets/attendance_card.dart b/lib/presentation/pages/profile/widgets/attendance_card.dart index 26c31982..7213e159 100644 --- a/lib/presentation/pages/profile/widgets/attendance_card.dart +++ b/lib/presentation/pages/profile/widgets/attendance_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; class AttendanceCard extends StatelessWidget { @@ -19,7 +19,7 @@ class AttendanceCard extends StatelessWidget { height: 80, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, ), padding: const EdgeInsets.all(20), child: Row( @@ -30,9 +30,9 @@ class AttendanceCard extends StatelessWidget { Container( width: 40, height: 40, - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, - color: DarkThemeColors.background03, + color: AppTheme.colors.background03, ), alignment: Alignment.center, child: Container( @@ -47,9 +47,9 @@ class AttendanceCard extends StatelessWidget { colors: [Color(0xff99db7e), Color(0xff6da95b)], ), ) - : const BoxDecoration( + : BoxDecoration( shape: BoxShape.circle, - color: DarkThemeColors.colorful02, + color: AppTheme.colors.colorful02, ), alignment: Alignment.center, child: type == "Вход" @@ -65,14 +65,14 @@ class AttendanceCard extends StatelessWidget { children: [ Text( type, - style: DarkTextTheme.bodyBold, + style: AppTextStyle.bodyBold, ), Text( '$date, $time', - style: DarkTextTheme.captionL.copyWith( + style: AppTextStyle.captionL.copyWith( color: type == "Вход" - ? DarkThemeColors.colorful05 - : DarkThemeColors.colorful02), + ? AppTheme.colors.colorful05 + : AppTheme.colors.colorful02), ), ], ), diff --git a/lib/presentation/pages/profile/widgets/bottom_error_info.dart b/lib/presentation/pages/profile/widgets/bottom_error_info.dart index 8d15de40..80ff35b4 100644 --- a/lib/presentation/pages/profile/widgets/bottom_error_info.dart +++ b/lib/presentation/pages/profile/widgets/bottom_error_info.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; - -import '../../../colors.dart'; -import '../../../theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class BottomErrorInfo extends StatelessWidget { const BottomErrorInfo({Key? key}) : super(key: key); @@ -11,17 +10,17 @@ class BottomErrorInfo extends StatelessWidget { return SafeArea( child: Container( height: MediaQuery.of(context).size.height * 0.95, - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( colors: [ - DarkThemeColors.secondary, - DarkThemeColors.deactive, - DarkThemeColors.background01 + AppTheme.colors.secondary, + AppTheme.colors.deactive, + AppTheme.colors.background01 ], - begin: Alignment(-1, -1), - end: Alignment(-1, 1), + begin: const Alignment(-1, -1), + end: const Alignment(-1, 1), ), - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( topLeft: Radius.circular(25.0), topRight: Radius.circular(25.0), ), @@ -30,9 +29,9 @@ class BottomErrorInfo extends StatelessWidget { padding: const EdgeInsets.all(3.0), child: Container( padding: const EdgeInsets.symmetric(horizontal: 24), - decoration: const BoxDecoration( - color: DarkThemeColors.background01, - borderRadius: BorderRadius.only( + decoration: BoxDecoration( + color: AppTheme.colors.background01, + borderRadius: const BorderRadius.only( topLeft: Radius.circular(25.0), topRight: Radius.circular(25.0)), ), @@ -49,14 +48,14 @@ class BottomErrorInfo extends StatelessWidget { ), Text( "Профиль теперь недоступен", - style: DarkTextTheme.h5, + style: AppTextStyle.h5, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( "Разработчики, отвечающие за API ЛКС, отключили возможность производить аутентификацию и получать данные своего аккаунта. Пожалуйста, используйте lk.mirea.ru", - style: DarkTextTheme.captionL - .copyWith(color: DarkThemeColors.deactive), + style: AppTextStyle.captionL + .copyWith(color: AppTheme.colors.deactive), textAlign: TextAlign.center, ), const SizedBox(height: 32), @@ -66,7 +65,7 @@ class BottomErrorInfo extends StatelessWidget { child: ElevatedButton( style: ButtonStyle( backgroundColor: - MaterialStateProperty.all(DarkThemeColors.primary), + MaterialStateProperty.all(AppTheme.colors.primary), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(24.0), @@ -78,7 +77,7 @@ class BottomErrorInfo extends StatelessWidget { }, child: Text( 'Понятно!', - style: DarkTextTheme.buttonS, + style: AppTextStyle.buttonS, ), ), ), diff --git a/lib/presentation/pages/profile/widgets/lector_search_card.dart b/lib/presentation/pages/profile/widgets/lector_search_card.dart index e9ab2bee..603e1c99 100644 --- a/lib/presentation/pages/profile/widgets/lector_search_card.dart +++ b/lib/presentation/pages/profile/widgets/lector_search_card.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rtu_mirea_app/domain/entities/employee.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class LectorSearchCard extends StatelessWidget { const LectorSearchCard({Key? key, required this.employee}) : super(key: key); @@ -15,14 +15,14 @@ class LectorSearchCard extends StatelessWidget { return Container( padding: const EdgeInsets.all(8), width: double.infinity, - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Text( '${employee.name} ${employee.secondName} ${employee.lastName}', - style: DarkTextTheme.bodyBold, + style: AppTextStyle.bodyBold, ), SizedBox( height: 30, @@ -41,14 +41,14 @@ class LectorSearchCard extends StatelessWidget { }, child: Text( employee.email, - style: DarkTextTheme.bodyRegular - .copyWith(color: DarkThemeColors.colorful02), + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.colorful02), ), ), ), const SizedBox(height: 8), - Text(employee.institute, style: DarkTextTheme.bodyRegular), - Text(employee.department, style: DarkTextTheme.bodyRegular), + Text(employee.institute, style: AppTextStyle.bodyRegular), + Text(employee.department, style: AppTextStyle.bodyRegular), ], ), ); diff --git a/lib/presentation/pages/profile/widgets/member_info.dart b/lib/presentation/pages/profile/widgets/member_info.dart index 39670aab..7f3134bc 100644 --- a/lib/presentation/pages/profile/widgets/member_info.dart +++ b/lib/presentation/pages/profile/widgets/member_info.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class MemberInfo extends StatelessWidget { const MemberInfo( @@ -31,7 +31,7 @@ class MemberInfo extends StatelessWidget { const SizedBox(height: 8), Text( username, - style: DarkTextTheme.bodyBold, + style: AppTextStyle.bodyBold, ), ]), onTap: () { diff --git a/lib/presentation/pages/profile/widgets/scores_chart_modal.dart b/lib/presentation/pages/profile/widgets/scores_chart_modal.dart index 4b7c22f7..5ed0ace1 100644 --- a/lib/presentation/pages/profile/widgets/scores_chart_modal.dart +++ b/lib/presentation/pages/profile/widgets/scores_chart_modal.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:rtu_mirea_app/domain/entities/score.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class ScoresChartModal extends StatefulWidget { const ScoresChartModal({Key? key, required this.scores}) : super(key: key); @@ -72,7 +72,7 @@ class _ScoresChartModalState extends State { @override Widget build(BuildContext context) { return SfCartesianChart( - backgroundColor: DarkThemeColors.background02, + backgroundColor: AppTheme.colors.background02, plotAreaBorderWidth: 0, title: ChartTitle(text: 'Средний балл успеваемости'), legend: diff --git a/lib/presentation/pages/schedule/groups_select_page.dart b/lib/presentation/pages/schedule/groups_select_page.dart index 7e3db520..64c02fc0 100644 --- a/lib/presentation/pages/schedule/groups_select_page.dart +++ b/lib/presentation/pages/schedule/groups_select_page.dart @@ -1,13 +1,11 @@ -import 'dart:convert'; - import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class GroupsSelectPage extends StatefulWidget { const GroupsSelectPage({Key? key}) : super(key: key); @@ -101,9 +99,9 @@ class _GroupsSelectPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, appBar: AppBar( - backgroundColor: DarkThemeColors.background01, + backgroundColor: AppTheme.colors.background01, title: const Text('Выбор группы'), ), body: SafeArea( @@ -126,60 +124,26 @@ class _GroupsSelectPageState extends State { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, children: [ const SizedBox(height: 16), - Container( - height: 48, - decoration: BoxDecoration( - color: DarkThemeColors.background02, - borderRadius: BorderRadius.circular(12), - ), - child: TextField( - onChanged: (value) { - setState(() { - _filteredGroups = groups - .where((group) => - group.toLowerCase().contains(value)) - .toList(); - }); - }, - style: DarkTextTheme.titleS.copyWith( - color: DarkThemeColors.deactive, - ), - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - border: InputBorder.none, - hintText: 'Поиск', - hintStyle: DarkTextTheme.titleS.copyWith( - color: DarkThemeColors.deactive, - ), - prefixIcon: Padding( - padding: const EdgeInsets.only(right: 8, left: 16), - child: SvgPicture.asset( - 'assets/icons/search.svg', - color: Colors.white, - width: 24, - height: 24, - ), - ), - prefixIconConstraints: const BoxConstraints( - maxWidth: 48, - maxHeight: 48, - ), - ), - inputFormatters: [ - _GroupTextFormatter(), - ], - ), - ), + _GroupTextField(onChanged: (value) { + setState(() { + _filteredGroups = groups + .where((group) => group + .toUpperCase() + .contains(value.toUpperCase())) + .toList(); + }); + }), const SizedBox(height: 16), if (_filteredGroups.isNotEmpty) Expanded( child: ListView.separated( - itemCount: groups.length, + shrinkWrap: true, + itemCount: _filteredGroups.length, separatorBuilder: (context, index) => const SizedBox( height: 10, ), @@ -215,6 +179,56 @@ class _GroupsSelectPageState extends State { } } +class _GroupTextField extends StatelessWidget { + const _GroupTextField({Key? key, required this.onChanged}) : super(key: key); + + final Function(String) onChanged; + + @override + Widget build(BuildContext context) { + return Container( + height: 48, + decoration: BoxDecoration( + color: AppTheme.colors.background02, + borderRadius: BorderRadius.circular(12), + ), + child: TextField( + onChanged: (value) => onChanged(value), + style: AppTextStyle.titleS.copyWith( + color: AppTheme.colors.active, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + border: InputBorder.none, + hintText: 'Поиск', + hintStyle: AppTextStyle.titleS.copyWith( + color: AppTheme.colors.deactive, + ), + prefixIcon: Padding( + padding: const EdgeInsets.only(right: 8, left: 16), + child: SvgPicture.asset( + 'assets/icons/search.svg', + color: AppTheme.colors.active, + width: 24, + height: 24, + ), + ), + prefixIconConstraints: const BoxConstraints( + maxWidth: 48, + maxHeight: 48, + ), + ), + inputFormatters: [ + _GroupTextFormatter(), + ], + ), + ); + } +} + class _GroupListTile extends StatelessWidget { const _GroupListTile({ Key? key, @@ -231,48 +245,59 @@ class _GroupListTile extends StatelessWidget { @override Widget build(BuildContext context) { - return Ink( + return Container( + clipBehavior: Clip.antiAlias, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: DarkThemeColors.background03, + //color: AppTheme.colors.background03, ), - child: ListTile( + child: Card( + color: AppTheme.colors.background02, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - leading: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: color, + borderOnForeground: true, + margin: const EdgeInsets.all(0), + child: ListTile( + shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - child: Center( - child: Text( - institute, - style: DarkTextTheme.titleS.copyWith( - color: Colors.white, + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(10), + ), + child: Center( + child: Text( + institute, + style: AppTextStyle.titleS.copyWith( + color: institute == 'ИПТИП' + ? Colors.black.withOpacity(0.8) + : Colors.white.withOpacity(0.8), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, ), ), + title: Text( + group, + style: AppTextStyle.titleM, + ), + trailing: const Icon( + Icons.chevron_right, + size: 24, + ), + onTap: () { + context + .read() + .add(ScheduleSetActiveGroupEvent(group: group)); + context.router.pop(); + }, ), - title: Text( - group, - style: DarkTextTheme.titleM, - ), - trailing: const Icon( - Icons.chevron_right, - size: 24, - ), - onTap: () { - context - .read() - .add(ScheduleSetActiveGroupEvent(group: group)); - context.router.pop(); - }, ), ); } diff --git a/lib/presentation/pages/schedule/schedule_page.dart b/lib/presentation/pages/schedule/schedule_page.dart index 4ad095fd..c1b3f3d4 100644 --- a/lib/presentation/pages/schedule/schedule_page.dart +++ b/lib/presentation/pages/schedule/schedule_page.dart @@ -4,13 +4,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rtu_mirea_app/domain/entities/schedule.dart'; import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_drawer.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_modal.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/colorful_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/settings_switch_button.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'widgets/schedule_page_view.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class SchedulePage extends StatefulWidget { const SchedulePage({Key? key}) : super(key: key); @@ -52,8 +52,8 @@ class _SchedulePageState extends State { border: Border.all( width: 1.5, color: schedule.isRemote - ? DarkThemeColors.colorful05 - : DarkThemeColors.colorful06, + ? AppTheme.colors.colorful05 + : AppTheme.colors.colorful06, ), ), child: Row( @@ -62,15 +62,15 @@ class _SchedulePageState extends State { children: [ Text( group, - style: DarkTextTheme.buttonL, + style: AppTextStyle.buttonL, ), Row( mainAxisSize: MainAxisSize.min, children: [ if (!schedule.isRemote) Text('кэш', - style: DarkTextTheme.buttonS - .copyWith(color: DarkThemeColors.colorful06)), + style: AppTextStyle.buttonS + .copyWith(color: AppTheme.colors.colorful06)), RawMaterialButton( onPressed: () { context.read().add(ScheduleUpdateEvent( @@ -81,8 +81,8 @@ class _SchedulePageState extends State { const BoxConstraints(minWidth: 36.0, minHeight: 36.0), child: Icon(Icons.refresh_rounded, color: schedule.isRemote - ? DarkThemeColors.colorful05 - : DarkThemeColors.colorful06), + ? AppTheme.colors.colorful05 + : AppTheme.colors.colorful06), ), ], ), @@ -97,7 +97,7 @@ class _SchedulePageState extends State { padding: const EdgeInsets.only(left: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - border: Border.all(color: DarkThemeColors.deactive), + border: Border.all(color: AppTheme.colors.deactive), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -105,7 +105,7 @@ class _SchedulePageState extends State { children: [ Text( group, - style: DarkTextTheme.buttonL, + style: AppTextStyle.buttonL, ), Row( children: [ @@ -169,7 +169,6 @@ class _SchedulePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: DarkThemeColors.background01, endDrawer: ScheduleSettingsDrawer( builder: (_) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -244,7 +243,7 @@ class _SchedulePageState extends State { ), const SizedBox(width: 20), Text("Добавить группу", - style: DarkTextTheme.buttonL), + style: AppTextStyle.buttonL), ], ), ), @@ -269,8 +268,8 @@ class _SchedulePageState extends State { const SizedBox(height: 20), Text( "Группы".toUpperCase(), - style: DarkTextTheme.chip.copyWith( - color: DarkThemeColors.deactiveDarker), + style: AppTextStyle.chip.copyWith( + color: AppTheme.colors.deactiveDarker), textAlign: TextAlign.left, ), const SizedBox(height: 10), @@ -318,8 +317,8 @@ class _SchedulePageState extends State { const SizedBox(height: 20), Text( "Группы".toUpperCase(), - style: DarkTextTheme.chip.copyWith( - color: DarkThemeColors.deactiveDarker), + style: AppTextStyle.chip.copyWith( + color: AppTheme.colors.deactiveDarker), textAlign: TextAlign.left, ), const SizedBox(height: 10), @@ -336,12 +335,10 @@ class _SchedulePageState extends State { ), ), appBar: AppBar( - backgroundColor: DarkThemeColors.background01, - elevation: 0, title: const Text('Расписание'), ), body: Container( - color: DarkThemeColors.background01, + color: AppTheme.colors.background01, child: SafeArea( child: BlocConsumer( listener: (context, state) { @@ -372,9 +369,9 @@ class _SchedulePageState extends State { } }); - return const Center( + return Center( child: CircularProgressIndicator( - backgroundColor: DarkThemeColors.primary, + backgroundColor: AppTheme.colors.primary, strokeWidth: 5, ), ); @@ -385,14 +382,14 @@ class _SchedulePageState extends State { children: [ Text( 'Упс!', - style: DarkTextTheme.h3, + style: AppTextStyle.h3, ), const SizedBox( height: 24, ), Text( state.errorMessage, - style: DarkTextTheme.bodyBold, + style: AppTextStyle.bodyBold, ) ], ); @@ -419,9 +416,9 @@ class _NoActiveGroupFoundMessage extends StatelessWidget { future: Future.delayed(const Duration(milliseconds: 600)), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( + return Center( child: CircularProgressIndicator( - backgroundColor: DarkThemeColors.primary, + backgroundColor: AppTheme.colors.primary, strokeWidth: 5, ), ); @@ -443,15 +440,15 @@ class _NoActiveGroupFoundMessage extends StatelessWidget { ), Text( "Не установлена активная группа", - style: DarkTextTheme.h5, + style: AppTextStyle.h5, ), const SizedBox( height: 8, ), Text( "Скачайте расписание по крайней мере для одной группы, чтобы отобразить календарь.", - style: DarkTextTheme.captionL.copyWith( - color: DarkThemeColors.deactive, + style: AppTextStyle.captionL.copyWith( + color: AppTheme.colors.deactive, ), ), const SizedBox( @@ -460,7 +457,7 @@ class _NoActiveGroupFoundMessage extends StatelessWidget { ColorfulButton( text: "Настроить", onClick: onTap, - backgroundColor: DarkThemeColors.primary, + backgroundColor: AppTheme.colors.primary, ), ], ), diff --git a/lib/presentation/pages/schedule/widgets/empty_lesson_card.dart b/lib/presentation/pages/schedule/widgets/empty_lesson_card.dart index 8983399c..d05520e6 100644 --- a/lib/presentation/pages/schedule/widgets/empty_lesson_card.dart +++ b/lib/presentation/pages/schedule/widgets/empty_lesson_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; class EmptyLessonCard extends StatelessWidget { @@ -16,7 +16,7 @@ class EmptyLessonCard extends StatelessWidget { Widget build(BuildContext context) { return Card( shadowColor: Colors.transparent, - color: DarkThemeColors.background03, + color: AppTheme.colors.background03, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), @@ -34,14 +34,14 @@ class EmptyLessonCard extends StatelessWidget { children: [ Text( timeStart, - style: DarkTextTheme.bodyBold.copyWith( - color: DarkThemeColors.deactive, fontSize: 12), + style: AppTextStyle.bodyBold.copyWith( + color: AppTheme.colors.deactive, fontSize: 12), ), const SizedBox(height: 5), Text( timeEnd, - style: DarkTextTheme.bodyBold.copyWith( - color: DarkThemeColors.deactive, fontSize: 12), + style: AppTextStyle.bodyBold.copyWith( + color: AppTheme.colors.deactive, fontSize: 12), ) ], ), @@ -51,13 +51,13 @@ class EmptyLessonCard extends StatelessWidget { Container( width: 25, height: 1, - color: DarkThemeColors.deactive, + color: AppTheme.colors.deactive, ), const SizedBox(height: 20), Container( width: 25, height: 1, - color: DarkThemeColors.deactive, + color: AppTheme.colors.deactive, ), ], ), diff --git a/lib/presentation/pages/schedule/widgets/lesson_card.dart b/lib/presentation/pages/schedule/widgets/lesson_card.dart index 3fa1c36c..3c228585 100644 --- a/lib/presentation/pages/schedule/widgets/lesson_card.dart +++ b/lib/presentation/pages/schedule/widgets/lesson_card.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; class LessonCard extends StatelessWidget { @@ -22,13 +22,13 @@ class LessonCard extends StatelessWidget { static Color getColorByType(String lessonType) { if (lessonType.contains('лк') || lessonType.contains('лек')) { - return DarkThemeColors.colorful01; + return AppTheme.colors.colorful01; } else if (lessonType.contains('лб') || lessonType.contains('лаб')) { - return DarkThemeColors.colorful07; + return AppTheme.colors.colorful07; } else if (lessonType.contains('с/р')) { - return DarkThemeColors.colorful02; + return AppTheme.colors.colorful02; } else { - return DarkThemeColors.colorful03; + return AppTheme.colors.colorful03; } } @@ -36,7 +36,7 @@ class LessonCard extends StatelessWidget { Widget build(BuildContext context) { return Card( shadowColor: Colors.transparent, - color: DarkThemeColors.background03, + color: AppTheme.colors.background03, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), @@ -54,16 +54,16 @@ class LessonCard extends StatelessWidget { children: [ Text( timeStart, - style: DarkTextTheme.bodyBold.copyWith( - color: DarkThemeColors.deactive, fontSize: 12), + style: AppTextStyle.bodyBold.copyWith( + color: AppTheme.colors.deactive, fontSize: 12), ), const SizedBox( height: 5, ), Text( timeEnd, - style: DarkTextTheme.bodyBold.copyWith( - color: DarkThemeColors.deactive, fontSize: 12), + style: AppTextStyle.bodyBold.copyWith( + color: AppTheme.colors.deactive, fontSize: 12), ) ], ), @@ -74,7 +74,7 @@ class LessonCard extends StatelessWidget { children: [ Text( room != '' ? '$name, $room' : name, - style: DarkTextTheme.titleM, + style: AppTextStyle.titleM, maxLines: 8, overflow: TextOverflow.ellipsis, ), @@ -83,8 +83,8 @@ class LessonCard extends StatelessWidget { ), Text( teacher, - style: DarkTextTheme.body - .copyWith(color: DarkThemeColors.deactive), + style: AppTextStyle.body + .copyWith(color: AppTheme.colors.deactive), ), ], ), @@ -100,7 +100,7 @@ class LessonCard extends StatelessWidget { color: getColorByType(type)), height: 24, // width: 10 * 7, - child: Text(type, style: DarkTextTheme.chip), + child: Text(type, style: AppTextStyle.chip), ), ), ], diff --git a/lib/presentation/pages/schedule/widgets/schedule_page_view.dart b/lib/presentation/pages/schedule/widgets/schedule_page_view.dart index e4e754e0..80620bcb 100644 --- a/lib/presentation/pages/schedule/widgets/schedule_page_view.dart +++ b/lib/presentation/pages/schedule/widgets/schedule_page_view.dart @@ -5,12 +5,12 @@ import 'package:rtu_mirea_app/domain/entities/lesson.dart'; import 'package:rtu_mirea_app/domain/entities/schedule.dart'; import 'package:rtu_mirea_app/domain/entities/schedule_settings.dart'; import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/empty_lesson_card.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:intl/intl.dart'; import 'lesson_card.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class SchedulePageView extends StatefulWidget { const SchedulePageView({Key? key, required this.schedule}) : super(key: key); @@ -82,7 +82,7 @@ class _SchedulePageViewState extends State { image: AssetImage('assets/images/Saly-18.png'), height: 225.0, ), - Text('Пар нет!', style: DarkTextTheme.title), + Text('Пар нет!', style: AppTextStyle.title), ], ); } @@ -217,26 +217,26 @@ class _SchedulePageViewState extends State { startingDayOfWeek: StartingDayOfWeek.monday, headerStyle: HeaderStyle( formatButtonShowsNext: false, - titleTextStyle: DarkTextTheme.captionL, - formatButtonTextStyle: DarkTextTheme.buttonS, + titleTextStyle: AppTextStyle.captionL, + formatButtonTextStyle: AppTextStyle.buttonS, titleTextFormatter: (DateTime date, dynamic locale) { String dateStr = DateFormat.yMMMM(locale).format(date); String weekStr = _selectedWeek.toString(); return '$dateStr\nвыбрана $weekStr неделя'; }, - formatButtonDecoration: const BoxDecoration( + formatButtonDecoration: BoxDecoration( border: Border.fromBorderSide( - BorderSide(color: DarkThemeColors.deactive)), - borderRadius: BorderRadius.all(Radius.circular(12.0))), + BorderSide(color: AppTheme.colors.deactive)), + borderRadius: const BorderRadius.all(Radius.circular(12.0))), ), - calendarStyle: const CalendarStyle( - rangeHighlightColor: DarkThemeColors.secondary, + calendarStyle: CalendarStyle( + rangeHighlightColor: AppTheme.colors.secondary, ), daysOfWeekStyle: DaysOfWeekStyle( weekdayStyle: - DarkTextTheme.body.copyWith(color: DarkThemeColors.deactive), - weekendStyle: DarkTextTheme.body - .copyWith(color: DarkThemeColors.deactiveDarker), + AppTextStyle.body.copyWith(color: AppTheme.colors.deactive), + weekendStyle: AppTextStyle.body + .copyWith(color: AppTheme.colors.deactiveDarker), ), focusedDay: _focusedDay, availableCalendarFormats: const { @@ -310,7 +310,7 @@ class _SchedulePageViewState extends State { // set up the AlertDialog AlertDialog alert = AlertDialog( contentPadding: const EdgeInsets.fromLTRB(8.0, 20.0, 8.0, 8.0), - backgroundColor: DarkThemeColors.background02, + backgroundColor: AppTheme.colors.background02, title: const Text("Выберите неделю"), content: Wrap( spacing: 4.0, @@ -322,7 +322,7 @@ class _SchedulePageViewState extends State { ElevatedButton( style: ElevatedButton.styleFrom( foregroundColor: Colors.white, - backgroundColor: DarkThemeColors.primary, + backgroundColor: AppTheme.colors.primary, shadowColor: Colors.transparent, ), onPressed: () { diff --git a/lib/presentation/pages/schedule/widgets/schedule_settings_drawer.dart b/lib/presentation/pages/schedule/widgets/schedule_settings_drawer.dart index de017751..05b36781 100644 --- a/lib/presentation/pages/schedule/widgets/schedule_settings_drawer.dart +++ b/lib/presentation/pages/schedule/widgets/schedule_settings_drawer.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; class ScheduleSettingsDrawer extends StatelessWidget { @@ -18,13 +18,13 @@ class ScheduleSettingsDrawer extends StatelessWidget { bottom: 16, ), decoration: BoxDecoration( - color: DarkThemeColors.background01, + color: AppTheme.colors.background01, borderRadius: BorderRadius.circular(16), ), clipBehavior: Clip.antiAliasWithSaveLayer, child: Drawer( child: Container( - color: DarkThemeColors.background01, + color: AppTheme.colors.background01, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, @@ -32,7 +32,7 @@ class ScheduleSettingsDrawer extends StatelessWidget { Container( padding: const EdgeInsets.only( left: 16, bottom: 24, top: 24, right: 16), - child: Text("Настройки", style: DarkTextTheme.h5), + child: Text("Настройки", style: AppTextStyle.h5), ), Expanded( child: builder(context), diff --git a/lib/presentation/pages/schedule/widgets/schedule_settings_modal.dart b/lib/presentation/pages/schedule/widgets/schedule_settings_modal.dart index 24c560cc..262bac1b 100644 --- a/lib/presentation/pages/schedule/widgets/schedule_settings_modal.dart +++ b/lib/presentation/pages/schedule/widgets/schedule_settings_modal.dart @@ -1,9 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/keyboard_positioned.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class ScheduleSettingsModal extends StatelessWidget { const ScheduleSettingsModal({Key? key, required this.isFirstRun}) @@ -16,26 +16,26 @@ class ScheduleSettingsModal extends StatelessWidget { return KeyboardPositioned( child: Container( height: MediaQuery.of(context).size.height * 0.85, - decoration: const BoxDecoration( + decoration: BoxDecoration( gradient: LinearGradient( colors: [ - DarkThemeColors.secondary, - DarkThemeColors.deactive, - DarkThemeColors.background01 + AppTheme.colors.secondary, + AppTheme.colors.deactive, + AppTheme.colors.background01 ], - begin: Alignment(-1, -1), - end: Alignment(-1, 1), + begin: const Alignment(-1, -1), + end: const Alignment(-1, 1), ), - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( topLeft: Radius.circular(25.0), topRight: Radius.circular(25.0)), ), child: Padding( padding: const EdgeInsets.all(3.0), child: Container( padding: const EdgeInsets.symmetric(horizontal: 24), - decoration: const BoxDecoration( - color: DarkThemeColors.background01, - borderRadius: BorderRadius.only( + decoration: BoxDecoration( + color: AppTheme.colors.background01, + borderRadius: const BorderRadius.only( topLeft: Radius.circular(25.0), topRight: Radius.circular(25.0)), ), @@ -51,13 +51,13 @@ class ScheduleSettingsModal extends StatelessWidget { ), Text( "Настройте расписание", - style: DarkTextTheme.h5, + style: AppTextStyle.h5, ), const SizedBox(height: 8), Text( "Кажется, что это ваш первый запуск. Установите вашу учебную группу, чтобы начать пользоваться расписанием", - style: DarkTextTheme.captionL - .copyWith(color: DarkThemeColors.deactive), + style: AppTextStyle.captionL + .copyWith(color: AppTheme.colors.deactive), textAlign: TextAlign.center, ), const SizedBox(height: 32), @@ -67,7 +67,7 @@ class ScheduleSettingsModal extends StatelessWidget { child: ElevatedButton( style: ButtonStyle( backgroundColor: - MaterialStateProperty.all(DarkThemeColors.primary), + MaterialStateProperty.all(AppTheme.colors.primary), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(24.0), @@ -82,7 +82,7 @@ class ScheduleSettingsModal extends StatelessWidget { }, child: Text( 'Начать', - style: DarkTextTheme.buttonS, + style: AppTextStyle.buttonS, ), ), ), diff --git a/lib/presentation/theme.dart b/lib/presentation/theme.dart index 48f564cd..459551a2 100644 --- a/lib/presentation/theme.dart +++ b/lib/presentation/theme.dart @@ -1,69 +1,172 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; -abstract class DarkTextTheme { - static const headline = TextStyle( - color: Colors.white, - fontFamily: "Montserrat", - fontWeight: FontWeight.w600, - letterSpacing: 0, +enum AppThemeType { light, dark, black } + +/// [AppTheme] is a class that contains all the theme data for the app. +/// +/// Every time the theme is changed, the [AppTheme.theme] is updated. +/// The [AppTheme.theme] is used to get the current theme data. +/// To change the theme, use [AppTheme.changeThemeType] method and pass the new +/// theme type ([AppThemeType]) as an argument. +class AppTheme { + static ThemeColors darkThemeColors = ThemeColors(); + static ThemeColors lightThemeColors = LightThemeColors(); + static ThemeColors blackThemeColors = AmoledDarkThemeColors(); + + static AppThemeType defaultThemeType = AppThemeType.dark; + static ThemeMode defaultThemeMode = ThemeMode.dark; + + static ThemeData theme = AppTheme.getDataByThemeType(); + + static final darkTheme = ThemeData.dark().copyWith( + textTheme: ThemeData.dark().textTheme.apply( + bodyColor: darkThemeColors.active, + displayColor: darkThemeColors.active, + ), + scaffoldBackgroundColor: darkThemeColors.background01, + backgroundColor: darkThemeColors.background01, + appBarTheme: AppBarTheme( + titleSpacing: 24, + backgroundColor: darkThemeColors.background01, + shadowColor: Colors.transparent, + titleTextStyle: + AppTextStyle.title.copyWith(color: darkThemeColors.active), + iconTheme: IconThemeData(color: blackThemeColors.active), + ), + bottomNavigationBarTheme: + ThemeData.dark().bottomNavigationBarTheme.copyWith( + type: BottomNavigationBarType.shifting, + backgroundColor: darkThemeColors.background03, + selectedItemColor: darkThemeColors.active, + unselectedItemColor: darkThemeColors.deactive, + selectedLabelStyle: AppTextStyle.captionL, + unselectedLabelStyle: AppTextStyle.captionS, + ), + pageTransitionsTheme: const PageTransitionsTheme(builders: { + TargetPlatform.iOS: NoShadowCupertinoPageTransitionsBuilder(), + TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), + }), ); - static const inter = TextStyle( - color: Colors.white, - fontFamily: 'Inter', - letterSpacing: 0, + static final lightTheme = ThemeData.light().copyWith( + textTheme: ThemeData.light().textTheme.apply( + bodyColor: lightThemeColors.active, + displayColor: lightThemeColors.active, + ), + scaffoldBackgroundColor: lightThemeColors.background01, + backgroundColor: lightThemeColors.background01, + appBarTheme: AppBarTheme( + titleSpacing: 24, + backgroundColor: lightThemeColors.background01, + shadowColor: Colors.transparent, + titleTextStyle: + AppTextStyle.title.copyWith(color: lightThemeColors.active), + iconTheme: IconThemeData(color: lightThemeColors.active), + ), + bottomNavigationBarTheme: + ThemeData.light().bottomNavigationBarTheme.copyWith( + type: BottomNavigationBarType.shifting, + backgroundColor: lightThemeColors.background03, + selectedItemColor: lightThemeColors.active, + unselectedItemColor: lightThemeColors.deactive, + selectedLabelStyle: AppTextStyle.captionL, + unselectedLabelStyle: AppTextStyle.captionS, + ), + pageTransitionsTheme: const PageTransitionsTheme(builders: { + TargetPlatform.iOS: NoShadowCupertinoPageTransitionsBuilder(), + TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), + }), ); - static final h0 = headline.copyWith(fontSize: 60); - static final h1 = headline.copyWith(fontSize: 48); - static final h2 = headline.copyWith(fontSize: 40); - static final h3 = headline.copyWith(fontSize: 36); - static final h4 = headline.copyWith(fontSize: 32); - static final h5 = headline.copyWith(fontSize: 24); - static final h6 = headline.copyWith(fontSize: 20); - static final title = headline.copyWith(fontSize: 18); - - static final titleM = - inter.copyWith(fontSize: 16, fontWeight: FontWeight.w600); - static final titleS = - inter.copyWith(fontSize: 14, fontWeight: FontWeight.w600); - static final buttonL = - inter.copyWith(fontSize: 16, fontWeight: FontWeight.w700); - static final buttonS = - inter.copyWith(fontSize: 14, fontWeight: FontWeight.w700); - static final tab = inter.copyWith(fontSize: 14, fontWeight: FontWeight.w600); - static final bodyL = - inter.copyWith(fontSize: 14, fontWeight: FontWeight.w500); - static final body = inter.copyWith(fontSize: 13, fontWeight: FontWeight.w500); - static final bodyBold = - inter.copyWith(fontSize: 14, fontWeight: FontWeight.w700); - static final bodyRegular = inter.copyWith(fontSize: 13); - static final captionL = - inter.copyWith(fontSize: 12, fontWeight: FontWeight.w500); - static final captionS = - inter.copyWith(fontSize: 11, fontWeight: FontWeight.w500); - static final chip = inter.copyWith( - fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: 0.50); -} + static final blackTheme = ThemeData.dark().copyWith( + textTheme: ThemeData.dark().textTheme.apply( + bodyColor: blackThemeColors.active, + displayColor: blackThemeColors.active, + ), + scaffoldBackgroundColor: blackThemeColors.background01, + backgroundColor: blackThemeColors.background01, + appBarTheme: AppBarTheme( + titleSpacing: 24, + backgroundColor: blackThemeColors.background01, + shadowColor: Colors.transparent, + titleTextStyle: AppTextStyle.title, + iconTheme: IconThemeData(color: blackThemeColors.active), + ), + bottomNavigationBarTheme: + ThemeData.dark().bottomNavigationBarTheme.copyWith( + type: BottomNavigationBarType.shifting, + backgroundColor: blackThemeColors.background03, + selectedItemColor: blackThemeColors.active, + unselectedItemColor: blackThemeColors.deactive, + selectedLabelStyle: AppTextStyle.captionL, + unselectedLabelStyle: AppTextStyle.captionS, + ), + pageTransitionsTheme: const PageTransitionsTheme(builders: { + TargetPlatform.iOS: NoShadowCupertinoPageTransitionsBuilder(), + TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), + }), + ); + + static ThemeData getDataByThemeType({AppThemeType? themeType}) { + themeType ??= defaultThemeType; + + switch (themeType) { + case AppThemeType.light: + return lightTheme; + case AppThemeType.black: + return blackTheme; + default: + return darkTheme; + } + } -final darkTheme = ThemeData.dark().copyWith( - scaffoldBackgroundColor: DarkThemeColors.background01, - backgroundColor: DarkThemeColors.background01, - appBarTheme: AppBarTheme( - titleSpacing: 24, - backgroundColor: DarkThemeColors.background01, - shadowColor: Colors.transparent, - titleTextStyle: DarkTextTheme.title, - ), - bottomNavigationBarTheme: ThemeData.dark().bottomNavigationBarTheme.copyWith( - type: BottomNavigationBarType.shifting, - backgroundColor: DarkThemeColors.background03, - selectedItemColor: DarkThemeColors.active, - unselectedItemColor: DarkThemeColors.deactive, - selectedLabelStyle: DarkTextTheme.captionL, - unselectedLabelStyle: DarkTextTheme.captionS, - ), -); - -final lightTheme = darkTheme; + static ThemeMode getThemeModeByType({AppThemeType? themeType}) { + themeType ??= defaultThemeType; + + switch (themeType) { + case AppThemeType.light: + return ThemeMode.light; + case AppThemeType.black: + return ThemeMode.dark; + default: + return ThemeMode.dark; + } + } + + static void changeThemeType(AppThemeType? themeType) { + defaultThemeType = themeType ?? AppThemeType.light; + defaultThemeMode = getThemeModeByType(themeType: defaultThemeType); + theme = AppTheme.getDataByThemeType(); + + // deleting the system status bar color and updating navigation bar color + // overlay + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + systemNavigationBarColor: AppTheme.colors.background01)); + } + + static ThemeColors getColorsByMode({AppThemeType? themeType}) { + themeType ??= defaultThemeType; + + switch (themeType) { + case AppThemeType.light: + return LightThemeColors(); + case AppThemeType.black: + return AmoledDarkThemeColors(); + default: + return ThemeColors(); + } + } + + /// Returns the current theme data. If the theme is changed, the data will be + /// updated. + static ThemeMode get themeMode => getThemeModeByType(); + + /// Returns the current theme colors. If the theme is changed, the colors + /// will be updated. + static ThemeColors get colors => getColorsByMode(); +} diff --git a/lib/presentation/typography.dart b/lib/presentation/typography.dart new file mode 100644 index 00000000..39b27554 --- /dev/null +++ b/lib/presentation/typography.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +abstract class AppTextStyle { + static const _headline = TextStyle( + fontFamily: "Montserrat", + fontWeight: FontWeight.w600, + letterSpacing: 0, + decoration: TextDecoration.none, + textBaseline: TextBaseline.alphabetic, + ); + + static const _inter = TextStyle( + fontFamily: 'Inter', + letterSpacing: 0, + decoration: TextDecoration.none, + textBaseline: TextBaseline.alphabetic, + ); + + // When height is non-null, the line height of the span of text will be a + // multiple of font Size and be exactly fontSize * height logical pixels tall. + + // For example, if want to have height 24.0, with font-size 20.0, we should + // have height property 1.2 + static final h0 = _headline.copyWith(fontSize: 60, height: 1); + static final h1 = _headline.copyWith(fontSize: 48, height: 1.1); + static final h2 = _headline.copyWith(fontSize: 40, height: 1.2); + static final h3 = _headline.copyWith(fontSize: 36, height: 1.1); + static final h4 = _headline.copyWith(fontSize: 32, height: 1.3); + static final h5 = _headline.copyWith(fontSize: 24, height: 1.3); + static final h6 = _headline.copyWith(fontSize: 20, height: 1.2); + static final title = _headline.copyWith(fontSize: 18, height: 1.3); + + static final titleM = + _inter.copyWith(fontSize: 16, fontWeight: FontWeight.w600); + static final titleS = + _inter.copyWith(fontSize: 14, fontWeight: FontWeight.w600); + static final buttonL = + _inter.copyWith(fontSize: 16, fontWeight: FontWeight.w700); + static final buttonS = + _inter.copyWith(fontSize: 14, fontWeight: FontWeight.w700); + static final tab = _inter.copyWith(fontSize: 14, fontWeight: FontWeight.w600); + static final bodyL = + _inter.copyWith(fontSize: 14, fontWeight: FontWeight.w500); + static final body = + _inter.copyWith(fontSize: 13, fontWeight: FontWeight.w500); + static final bodyBold = + _inter.copyWith(fontSize: 14, fontWeight: FontWeight.w700); + static final bodyRegular = _inter.copyWith(fontSize: 13); + static final captionL = + _inter.copyWith(fontSize: 12, fontWeight: FontWeight.w500); + static final captionS = + _inter.copyWith(fontSize: 11, fontWeight: FontWeight.w500); + static final chip = _inter.copyWith( + fontSize: 10, fontWeight: FontWeight.w700, letterSpacing: 0.50); +} diff --git a/lib/presentation/widgets/badged_container.dart b/lib/presentation/widgets/badged_container.dart index 25cada46..4bee33a6 100644 --- a/lib/presentation/widgets/badged_container.dart +++ b/lib/presentation/widgets/badged_container.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class BadgedContainer extends StatelessWidget { final String label; @@ -17,7 +17,7 @@ class BadgedContainer extends StatelessWidget { width: double.infinity, height: 90, child: Card( - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, margin: const EdgeInsets.all(0), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: InkWell( @@ -33,10 +33,10 @@ class BadgedContainer extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, - style: DarkTextTheme.bodyBold - .copyWith(color: DarkThemeColors.deactive)), + style: AppTextStyle.bodyBold + .copyWith(color: AppTheme.colors.deactive)), const SizedBox(height: 5), - Text(text, style: DarkTextTheme.title) + Text(text, style: AppTextStyle.title) ], ) ], diff --git a/lib/presentation/widgets/buttons/app_settings_button.dart b/lib/presentation/widgets/buttons/app_settings_button.dart index 9b92f8dc..ab7d435e 100644 --- a/lib/presentation/widgets/buttons/app_settings_button.dart +++ b/lib/presentation/widgets/buttons/app_settings_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class AppSettingsButton extends StatelessWidget { final VoidCallback onClick; @@ -19,6 +20,7 @@ class AppSettingsButton extends StatelessWidget { 'assets/icons/filter.svg', width: 20, height: 20, + color: AppTheme.colors.active, ), ), ); diff --git a/lib/presentation/widgets/buttons/colorful_button.dart b/lib/presentation/widgets/buttons/colorful_button.dart index 55afebc8..f77b746f 100644 --- a/lib/presentation/widgets/buttons/colorful_button.dart +++ b/lib/presentation/widgets/buttons/colorful_button.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class ColorfulButton extends StatelessWidget { const ColorfulButton( @@ -29,7 +29,7 @@ class ColorfulButton extends StatelessWidget { child: Center( child: Text( text, - style: DarkTextTheme.buttonL, + style: AppTextStyle.buttonL, ), ), onTap: () { diff --git a/lib/presentation/widgets/buttons/icon_button.dart b/lib/presentation/widgets/buttons/icon_button.dart index c1404b93..05e7a39a 100644 --- a/lib/presentation/widgets/buttons/icon_button.dart +++ b/lib/presentation/widgets/buttons/icon_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class SocialIconButton extends StatelessWidget { const SocialIconButton({ @@ -14,41 +15,40 @@ class SocialIconButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Expanded( - child: ElevatedButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.transparent), - shadowColor: MaterialStateProperty.all(Colors.transparent), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24.0), - side: const BorderSide(color: DarkThemeColors.deactive), - ), + return ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.transparent), + shadowColor: MaterialStateProperty.all(Colors.transparent), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24.0), + side: BorderSide(color: AppTheme.colors.deactive, width: 1), ), ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: text != null - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(text!), - const SizedBox(width: 8), - Image( - image: assetImage, - height: 16.0, - ), - ], - ) - : Image( + ), + child: text != null + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + text!, + style: AppTextStyle.buttonS + .copyWith(color: AppTheme.colors.active), + ), + const SizedBox(width: 8), + Image( image: assetImage, height: 16.0, ), - ), - onPressed: () { - onClick(); - }, - ), + ], + ) + : Image( + image: assetImage, + height: 16.0, + ), + onPressed: () { + onClick(); + }, ); } } diff --git a/lib/presentation/widgets/buttons/primary_button.dart b/lib/presentation/widgets/buttons/primary_button.dart index 4f91159c..4750b1d9 100644 --- a/lib/presentation/widgets/buttons/primary_button.dart +++ b/lib/presentation/widgets/buttons/primary_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class PrimaryButton extends StatelessWidget { const PrimaryButton({Key? key, required this.text, required this.onClick}) @@ -16,7 +16,7 @@ class PrimaryButton extends StatelessWidget { const BoxConstraints.tightFor(width: double.infinity, height: 48), child: ElevatedButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(DarkThemeColors.primary), + backgroundColor: MaterialStateProperty.all(AppTheme.colors.primary), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(24.0), @@ -28,7 +28,7 @@ class PrimaryButton extends StatelessWidget { }, child: Text( text, - style: DarkTextTheme.buttonS, + style: AppTextStyle.buttonS, ), ), ); diff --git a/lib/presentation/widgets/buttons/primary_tab_button.dart b/lib/presentation/widgets/buttons/primary_tab_button.dart index 16325cc9..e83d402a 100644 --- a/lib/presentation/widgets/buttons/primary_tab_button.dart +++ b/lib/presentation/widgets/buttons/primary_tab_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class PrimaryTabButton extends StatelessWidget { final String text; @@ -31,9 +31,9 @@ class PrimaryTabButton extends StatelessWidget { }, style: ButtonStyle( backgroundColor: notifier.value == itemIndex - ? MaterialStateProperty.all(DarkThemeColors.primary) + ? MaterialStateProperty.all(AppTheme.colors.primary) : MaterialStateProperty.all( - DarkThemeColors.background01), + AppTheme.colors.background01), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(50.0), @@ -42,10 +42,10 @@ class PrimaryTabButton extends StatelessWidget { shadowColor: MaterialStateProperty.all(Colors.transparent), ), child: Text(text, - style: DarkTextTheme.tab.copyWith( + style: AppTextStyle.tab.copyWith( color: notifier.value == itemIndex - ? DarkThemeColors.active - : DarkThemeColors.deactive, + ? AppTheme.colors.active + : AppTheme.colors.deactive, )), ); }, diff --git a/lib/presentation/widgets/buttons/select_date_button.dart b/lib/presentation/widgets/buttons/select_date_button.dart index ed66357c..75d0c833 100644 --- a/lib/presentation/widgets/buttons/select_date_button.dart +++ b/lib/presentation/widgets/buttons/select_date_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:intl/intl.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class SelectDateButton extends StatefulWidget { const SelectDateButton({ @@ -57,7 +57,7 @@ class _SelectDateButtonState extends State { shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(24.0), - side: const BorderSide(color: DarkThemeColors.deactive), + side: BorderSide(color: AppTheme.colors.deactive), ), ), ), @@ -68,7 +68,7 @@ class _SelectDateButtonState extends State { children: [ Text( _selectedTextDate, - style: DarkTextTheme.captionL, + style: AppTextStyle.captionL, ), const SizedBox(width: 8), const Icon( diff --git a/lib/presentation/widgets/buttons/select_range_date_button.dart b/lib/presentation/widgets/buttons/select_range_date_button.dart index 8754efeb..bcd27060 100644 --- a/lib/presentation/widgets/buttons/select_range_date_button.dart +++ b/lib/presentation/widgets/buttons/select_range_date_button.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:syncfusion_flutter_datepicker/datepicker.dart'; import 'package:intl/intl.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class SelectRangeDateButton extends StatefulWidget { const SelectRangeDateButton({ @@ -63,7 +63,7 @@ class _SelectRangeDateButtonState extends State { shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(24.0), - side: const BorderSide(color: DarkThemeColors.deactive), + side: BorderSide(color: AppTheme.colors.deactive), ), ), ), @@ -74,12 +74,15 @@ class _SelectRangeDateButtonState extends State { children: [ Text( _selectedTextDate, - style: DarkTextTheme.captionL, + style: AppTextStyle.captionL.copyWith( + color: AppTheme.colors.deactive, + ), ), const SizedBox(width: 8), - const Icon( + Icon( Icons.calendar_today_rounded, size: 18, + color: AppTheme.colors.deactive, ), ], ), @@ -93,23 +96,36 @@ class _SelectRangeDateButtonState extends State { child: Container( constraints: const BoxConstraints(maxHeight: 360), child: SfDateRangePicker( + monthViewSettings: DateRangePickerMonthViewSettings( + firstDayOfWeek: 1, + viewHeaderStyle: DateRangePickerViewHeaderStyle( + backgroundColor: AppTheme.colors.background02, + textStyle: AppTextStyle.bodyBold.copyWith( + color: AppTheme.colors.deactive, + ), + ), + ), + view: DateRangePickerView.month, + headerHeight: 60, rangeSelectionColor: - DarkThemeColors.colorful03.withOpacity(0.7), - headerStyle: - DateRangePickerHeaderStyle(textStyle: DarkTextTheme.titleS), + AppTheme.colors.colorful03.withOpacity(0.7), + headerStyle: DateRangePickerHeaderStyle( + textStyle: AppTextStyle.titleS + .copyWith(color: AppTheme.colors.active), + ), monthCellStyle: DateRangePickerMonthCellStyle( - textStyle: DarkTextTheme.body, - todayTextStyle: DarkTextTheme.body - .copyWith(color: DarkThemeColors.colorful03), + textStyle: AppTextStyle.body.copyWith( + color: AppTheme.colors.deactive, + ), + todayTextStyle: AppTextStyle.body + .copyWith(color: AppTheme.colors.colorful03), ), - selectionTextStyle: DarkTextTheme.bodyBold, - rangeTextStyle: DarkTextTheme.body, - todayHighlightColor: DarkThemeColors.colorful03, - startRangeSelectionColor: DarkThemeColors.colorful03, - endRangeSelectionColor: DarkThemeColors.colorful03, - selectionColor: DarkThemeColors.primary, - monthViewSettings: - const DateRangePickerMonthViewSettings(firstDayOfWeek: 1), + selectionTextStyle: AppTextStyle.bodyBold, + rangeTextStyle: AppTextStyle.body, + todayHighlightColor: AppTheme.colors.colorful03, + startRangeSelectionColor: AppTheme.colors.colorful03, + endRangeSelectionColor: AppTheme.colors.colorful03, + selectionColor: AppTheme.colors.primary, toggleDaySelection: true, showActionButtons: true, initialSelectedRange: _initialRange, @@ -121,7 +137,7 @@ class _SelectRangeDateButtonState extends State { Navigator.of(context).pop(); } }, - backgroundColor: DarkThemeColors.background02, + backgroundColor: AppTheme.colors.background02, showNavigationArrow: true, minDate: widget.firstDate, maxDate: widget.lastDate, diff --git a/lib/presentation/widgets/buttons/settings_button.dart b/lib/presentation/widgets/buttons/settings_button.dart index 529f13db..e6f4c1d0 100644 --- a/lib/presentation/widgets/buttons/settings_button.dart +++ b/lib/presentation/widgets/buttons/settings_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class SettingsButton extends StatelessWidget { const SettingsButton( @@ -19,7 +19,7 @@ class SettingsButton extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), - color: DarkThemeColors.background02, + color: AppTheme.colors.background02, shadowColor: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(12), @@ -35,7 +35,7 @@ class SettingsButton extends StatelessWidget { ), Text( text, - style: DarkTextTheme.buttonL, + style: AppTextStyle.buttonL, ), ], ), diff --git a/lib/presentation/widgets/buttons/text_outlined_button.dart b/lib/presentation/widgets/buttons/text_outlined_button.dart index e2b60615..1e9b9a49 100644 --- a/lib/presentation/widgets/buttons/text_outlined_button.dart +++ b/lib/presentation/widgets/buttons/text_outlined_button.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class TextOutlinedButton extends StatelessWidget { final String content; @@ -18,17 +19,18 @@ class TextOutlinedButton extends StatelessWidget { onPressed: onPressed, style: ButtonStyle( backgroundColor: - MaterialStateProperty.all(DarkThemeColors.background01), + MaterialStateProperty.all(AppTheme.colors.background01), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(50.0), - side: - const BorderSide(color: DarkThemeColors.primary, width: 2)), + side: BorderSide(color: AppTheme.colors.primary, width: 2)), ), ), child: Center( - child: Text(content, - style: const TextStyle(fontSize: 17, color: Colors.white)), + child: Text( + content, + style: AppTextStyle.buttonS.copyWith(color: AppTheme.colors.active), + ), ), ), ); diff --git a/lib/presentation/widgets/container_label.dart b/lib/presentation/widgets/container_label.dart index 623228ac..cd1c63a4 100644 --- a/lib/presentation/widgets/container_label.dart +++ b/lib/presentation/widgets/container_label.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class ContainerLabel extends StatelessWidget { final String label; @@ -15,7 +15,7 @@ class ContainerLabel extends StatelessWidget { alignment: Alignment.centerLeft, child: Text( label, - style: DarkTextTheme.titleM.copyWith(color: DarkThemeColors.deactive), + style: AppTextStyle.titleM.copyWith(color: AppTheme.colors.deactive), ), ); } diff --git a/lib/presentation/widgets/copy_text_block.dart b/lib/presentation/widgets/copy_text_block.dart index fef79509..336af983 100644 --- a/lib/presentation/widgets/copy_text_block.dart +++ b/lib/presentation/widgets/copy_text_block.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'container_label.dart'; @@ -26,7 +26,7 @@ class CopyTextBlockWithLabel extends StatelessWidget { Expanded( child: Text( text, - style: DarkTextTheme.titleM, + style: AppTextStyle.titleM, ), ), InkWell( diff --git a/lib/presentation/widgets/forms/labelled_input.dart b/lib/presentation/widgets/forms/labelled_input.dart index 910baca9..18ba56f8 100644 --- a/lib/presentation/widgets/forms/labelled_input.dart +++ b/lib/presentation/widgets/forms/labelled_input.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class LabelledInput extends StatefulWidget { final String label; @@ -37,8 +37,8 @@ class _LabelledInputState extends State { Text( widget.label.toUpperCase(), textAlign: TextAlign.left, - style: DarkTextTheme.chip - .copyWith(color: DarkThemeColors.deactiveDarker), + style: + AppTextStyle.chip.copyWith(color: AppTheme.colors.deactiveDarker), ), TextField( controller: widget.controller, @@ -48,7 +48,7 @@ class _LabelledInputState extends State { ] else if (widget.placeholder == "Email") AutofillHints.email, ], - style: DarkTextTheme.title, + style: AppTextStyle.title, onTap: () {}, keyboardType: widget.keyboardType, obscureText: (widget.placeholder == 'Пароль' || @@ -73,24 +73,24 @@ class _LabelledInputState extends State { ? FontAwesomeIcons.eye : FontAwesomeIcons.eyeSlash, size: 15.0, - color: DarkThemeColors.deactiveDarker, + color: AppTheme.colors.deactiveDarker, )) : InkWell( onTap: () { widget.controller.text = ""; }, - child: const Icon(FontAwesomeIcons.solidCircleXmark, - size: 15, color: DarkThemeColors.deactiveDarker), + child: Icon(FontAwesomeIcons.solidCircleXmark, + size: 15, color: AppTheme.colors.deactiveDarker), ), hintText: widget.placeholder, hintStyle: - DarkTextTheme.titleM.copyWith(color: DarkThemeColors.deactive), - focusedBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: DarkThemeColors.colorful05), + AppTextStyle.titleM.copyWith(color: AppTheme.colors.deactive), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: AppTheme.colors.colorful05), ), filled: false, - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: DarkThemeColors.deactiveDarker), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: AppTheme.colors.deactiveDarker), ), border: const UnderlineInputBorder( borderSide: BorderSide(color: Colors.green), diff --git a/lib/presentation/widgets/fullscreen_image.dart b/lib/presentation/widgets/fullscreen_image.dart index 53252a81..6ed97836 100644 --- a/lib/presentation/widgets/fullscreen_image.dart +++ b/lib/presentation/widgets/fullscreen_image.dart @@ -1,7 +1,7 @@ import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; class FullScreenImage extends StatefulWidget { const FullScreenImage({Key? key, required this.imageUrl}) : super(key: key); @@ -42,7 +42,7 @@ class _FullScreenImageState extends State child: PhotoView.customChild( initialScale: 1.0, backgroundDecoration: - const BoxDecoration(color: DarkThemeColors.background01), + BoxDecoration(color: AppTheme.colors.background01), child: ExtendedImage.network( widget.imageUrl, width: MediaQuery.of(context).size.width, @@ -66,7 +66,7 @@ class _FullScreenImageState extends State SlideTransition( position: offsetAnimation, child: Container( - color: DarkThemeColors.background01, + color: AppTheme.colors.background01, height: 85, child: AppBar( title: const Text("Просмотр изображения"), diff --git a/lib/presentation/widgets/settings_switch_button.dart b/lib/presentation/widgets/settings_switch_button.dart index 96c0ea26..221e06ca 100644 --- a/lib/presentation/widgets/settings_switch_button.dart +++ b/lib/presentation/widgets/settings_switch_button.dart @@ -1,8 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; class SettingsSwitchButton extends StatefulWidget { const SettingsSwitchButton({ @@ -41,7 +41,7 @@ class _SettingsSwitchButtonState extends State { children: [ widget.svgPicture, const SizedBox(width: 20), - Text(widget.text, style: DarkTextTheme.buttonL), + Text(widget.text, style: AppTextStyle.buttonL), ], ), Padding( @@ -49,7 +49,7 @@ class _SettingsSwitchButtonState extends State { child: ValueListenableBuilder( valueListenable: _switchValueNotifier, builder: (context, hasError, child) => CupertinoSwitch( - activeColor: DarkThemeColors.primary, + activeColor: AppTheme.colors.primary, value: _switchValueNotifier.value, onChanged: (value) { _switchValueNotifier.value = value; diff --git a/lib/presentation/widgets/update_info_modal.dart b/lib/presentation/widgets/update_info_modal.dart index ca79cace..e75000cb 100644 --- a/lib/presentation/widgets/update_info_modal.dart +++ b/lib/presentation/widgets/update_info_modal.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/domain/entities/update_info.dart'; import 'package:rtu_mirea_app/presentation/bloc/update_info_bloc/update_info_bloc.dart'; -import 'package:rtu_mirea_app/presentation/colors.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_button.dart'; abstract class UpdateInfoDialog { @@ -54,9 +54,9 @@ class _UpdateInfo extends StatelessWidget { decoration: BoxDecoration( border: Border.all( width: 2.5, - color: DarkThemeColors.secondary, + color: AppTheme.colors.secondary, ), - color: DarkThemeColors.background03, + color: AppTheme.colors.background03, borderRadius: const BorderRadius.all( Radius.circular(25.0), ), @@ -69,22 +69,22 @@ class _UpdateInfo extends StatelessWidget { children: [ Text( 'Вышла новая версия ${data.appVersion}', - style: DarkTextTheme.captionS.copyWith( - color: DarkThemeColors.deactive, + style: AppTextStyle.captionS.copyWith( + color: AppTheme.colors.deactive, ), ), const Padding(padding: EdgeInsets.only(top: 16)), Text( data.title, textAlign: TextAlign.center, - style: DarkTextTheme.h5, + style: AppTextStyle.h5, ), if (data.description != null) const Padding(padding: EdgeInsets.only(top: 24)), if (data.description != null) Text( data.description!, - style: DarkTextTheme.bodyL, + style: AppTextStyle.bodyL, ), const Padding(padding: EdgeInsets.only(top: 24)), Container( @@ -99,8 +99,8 @@ class _UpdateInfo extends StatelessWidget { children: [ Text( data.text, - style: DarkTextTheme.body.copyWith( - color: DarkThemeColors.deactive, + style: AppTextStyle.body.copyWith( + color: AppTheme.colors.deactive, ), ), ], @@ -112,7 +112,7 @@ class _UpdateInfo extends StatelessWidget { PrimaryButton( text: 'Класс!', onClick: () => Navigator.pop(context), - // backgroundColor: DarkThemeColors.primary, + // backgroundColor: AppTheme.colors.primary, ), ], ), diff --git a/lib/service_locator.dart b/lib/service_locator.dart index c76aa340..3c9b6fba 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -52,6 +52,7 @@ import 'package:rtu_mirea_app/domain/usecases/log_out.dart'; import 'package:rtu_mirea_app/domain/usecases/set_active_group.dart'; import 'package:rtu_mirea_app/domain/usecases/set_app_settings.dart'; import 'package:rtu_mirea_app/domain/usecases/set_schedule_settings.dart'; +import 'package:rtu_mirea_app/presentation/app_notifier.dart'; import 'package:rtu_mirea_app/presentation/bloc/about_app_bloc/about_app_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/announces_bloc/announces_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/app_cubit/app_cubit.dart'; @@ -72,6 +73,14 @@ import 'data/repositories/schedule_repository_impl.dart'; final getIt = GetIt.instance; Future setup() async { + // Global app notifier + getIt.registerFactory( + () => AppNotifier( + getAppSettings: getIt(), + setAppSettings: getIt(), + ), + ); + // BloC / Cubit getIt.registerFactory( () => ScheduleBloc( diff --git a/pubspec.yaml b/pubspec.yaml index 8fd1a0f4..9b26d2cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,10 @@ dependencies: flutter_localizations: sdk: flutter + # A wrapper around InheritedWidget to make them easier to use and more reusable. + # See https://pub.dev/packages/provider + provider: ^6.0.5 + # Contains code to deal with internationalized/localized messages, date and number formatting # and parsing, bi-directional text, and other internationalization issues. # See https://pub.dev/packages/intl @@ -49,10 +53,6 @@ dependencies: # See https://pub.dev/packages/flutter_svg flutter_svg: ^1.0.0 - # Allows dynamically change between light and dark theme. - # See https://pub.dev/packages/adaptive_theme - adaptive_theme: ^3.1.1 - # Helps to implement value based equality without explicitly # override == and hashCode. # See https://pub.dev/packages/equatable @@ -139,7 +139,7 @@ dependencies: syncfusion_flutter_charts: ^20.1.57 syncfusion_flutter_gauges: ^20.1.57 - auto_route: ^5.0.1 + auto_route: ^5.0.3 url_launcher: ^6.0.17 From 4ad5ada2e0d61a0e353b65e72731c2637cbe591c Mon Sep 17 00:00:00 2001 From: Yaroslav Smirnov Date: Tue, 31 Jan 2023 13:28:35 +0300 Subject: [PATCH 28/38] feature: Include resit exams in a scores chart (#276) * Include resit exams in a scores chart * Change default case value * Ignore zachets in grade chart --- .../pages/profile/widgets/scores_chart_modal.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/presentation/pages/profile/widgets/scores_chart_modal.dart b/lib/presentation/pages/profile/widgets/scores_chart_modal.dart index 5ed0ace1..dce7721c 100644 --- a/lib/presentation/pages/profile/widgets/scores_chart_modal.dart +++ b/lib/presentation/pages/profile/widgets/scores_chart_modal.dart @@ -23,8 +23,10 @@ class _ScoresChartModalState extends State { return 4; case "удовлетворительно": return 3; - default: + case "зачтено": return -1; + default: + return 0; } } @@ -39,7 +41,7 @@ class _ScoresChartModalState extends State { double average = 0; for (final score in scores) { final scoreValue = _getScoreByName(score.result); - + if (scoreValue != -1) { count++; average += scoreValue; From 4cac1b98b48e6522401f1a6f609fdf51bd0da6a5 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Tue, 31 Jan 2023 14:10:28 +0300 Subject: [PATCH 29/38] fix: Remove discontinued dependency (#277) --- lib/presentation/pages/map/map_page.dart | 132 +++++++++++------------ pubspec.yaml | 3 - 2 files changed, 65 insertions(+), 70 deletions(-) diff --git a/lib/presentation/pages/map/map_page.dart b/lib/presentation/pages/map/map_page.dart index 2fddb35d..837321b4 100644 --- a/lib/presentation/pages/map/map_page.dart +++ b/lib/presentation/pages/map/map_page.dart @@ -1,16 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:material_floating_search_bar/material_floating_search_bar.dart'; import 'package:photo_view/photo_view.dart'; import 'package:rtu_mirea_app/presentation/bloc/map_cubit/map_cubit.dart'; import 'package:rtu_mirea_app/presentation/pages/map/widgets/map_scaling_button.dart'; -import 'package:implicitly_animated_reorderable_list/implicitly_animated_reorderable_list.dart'; -import 'package:implicitly_animated_reorderable_list/transitions.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; -import 'package:rtu_mirea_app/presentation/typography.dart'; import 'widgets/map_navigation_button.dart'; -import 'widgets/search_item_button.dart'; class MapPage extends StatefulWidget { const MapPage({Key? key}) : super(key: key); @@ -79,68 +74,71 @@ class _MapPageState extends State { ); } - // ignore: unused_element - Widget _buildSearchBar() { - return FloatingSearchBar( - accentColor: AppTheme.colors.primary, - iconColor: AppTheme.colors.white, - backgroundColor: AppTheme.colors.background02, - hint: 'Что будем искать, Милорд?', - hintStyle: AppTextStyle.titleS.copyWith(color: AppTheme.colors.deactive), - borderRadius: const BorderRadius.all(Radius.circular(12)), - onQueryChanged: (query) { - context.read().setSearchQuery(query); - }, - builder: (context, transition) { - return Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Material( - color: AppTheme.colors.background03, - elevation: 4.0, - borderRadius: BorderRadius.circular(8), - child: BlocBuilder( - buildWhen: (prevState, currentState) => - currentState is MapSearchFoundUpdated, - builder: (context, state) { - if (state is MapFloorLoaded) return Container(); - return ImplicitlyAnimatedList>( - shrinkWrap: true, - items: (state as MapSearchFoundUpdated) - .foundRooms - .take(6) - .toList(), - physics: const NeverScrollableScrollPhysics(), - areItemsTheSame: (a, b) => a == b, - itemBuilder: (context, animation, room, i) { - return SizeFadeTransition( - animation: animation, - child: SearchItemButton( - room: room, - onClick: () { - // todo: change position - context.read().goToFloor(room['floor']); - }), - ); - }, - updateItemBuilder: (context, animation, room) { - return FadeTransition( - opacity: animation, - child: SearchItemButton( - room: room, - onClick: () { - // todo: change position - context.read().goToFloor(room['floor']); - }), - ); - }, - ); - }, - ), - ), - ); - }, - ); - } + // TODO: implement search bar without using [ImplicitlyAnimatedList]. + // Package implicitly_animated_reorderable_list is DISCONTINUED and + // project compilation fails because of it. + + // Widget _buildSearchBar() { + // return FloatingSearchBar( + // accentColor: AppTheme.colors.primary, + // iconColor: AppTheme.colors.white, + // backgroundColor: AppTheme.colors.background02, + // hint: 'Что будем искать, Милорд?', + // hintStyle: AppTextStyle.titleS.copyWith(color: AppTheme.colors.deactive), + // borderRadius: const BorderRadius.all(Radius.circular(12)), + // onQueryChanged: (query) { + // context.read().setSearchQuery(query); + // }, + // builder: (context, transition) { + // return Padding( + // padding: const EdgeInsets.only(top: 16.0), + // child: Material( + // color: AppTheme.colors.background03, + // elevation: 4.0, + // borderRadius: BorderRadius.circular(8), + // child: BlocBuilder( + // buildWhen: (prevState, currentState) => + // currentState is MapSearchFoundUpdated, + // builder: (context, state) { + // if (state is MapFloorLoaded) return Container(); + // return ImplicitlyAnimatedList>( + // shrinkWrap: true, + // items: (state as MapSearchFoundUpdated) + // .foundRooms + // .take(6) + // .toList(), + // physics: const NeverScrollableScrollPhysics(), + // areItemsTheSame: (a, b) => a == b, + // itemBuilder: (context, animation, room, i) { + // return SizeFadeTransition( + // animation: animation, + // child: SearchItemButton( + // room: room, + // onClick: () { + // // todo: change position + // context.read().goToFloor(room['floor']); + // }), + // ); + // }, + // updateItemBuilder: (context, animation, room) { + // return FadeTransition( + // opacity: animation, + // child: SearchItemButton( + // room: room, + // onClick: () { + // // todo: change position + // context.read().goToFloor(room['floor']); + // }), + // ); + // }, + // ); + // }, + // ), + // ), + // ); + // }, + // ); + // } Widget _buildMap() { return BlocBuilder( diff --git a/pubspec.yaml b/pubspec.yaml index 9b26d2cb..c1d76cb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -130,9 +130,6 @@ dependencies: # See https://pub.dev/packages/cached_network_image cached_network_image: ^3.2.0 - # ListView that implicitly animates between the changes of two lists - # See https://pub.dev/packages/implicitly_animated_reorderable_list - implicitly_animated_reorderable_list: ^0.4.2 syncfusion_flutter_datagrid: ^20.1.57 syncfusion_flutter_core: ^20.1.57 syncfusion_flutter_sliders: ^20.1.57 From 7754bb6285d79db212832c338342f38ab65a2214 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Wed, 8 Feb 2023 23:02:05 +0300 Subject: [PATCH 30/38] feature: Add web support (#278) * feature: Add web support * dev: Upgrade workflows Flutter --- .github/workflows/main.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/tests.yml | 2 +- .../repositories/forum_repository_impl.dart | 4 +- .../repositories/github_repository_impl.dart | 4 +- .../repositories/news_repository_impl.dart | 4 +- .../schedule_repository_impl.dart | 4 +- .../repositories/strapi_repository_impl.dart | 4 +- .../repositories/user_repository_impl.dart | 4 +- lib/firebase_options.dart | 46 +++-- lib/main.dart | 45 +++-- lib/presentation/core/routes/routes.gr.dart | 175 +++++------------- lib/presentation/pages/news/news_page.dart | 2 +- .../widgets/buttons/settings_button.dart | 2 +- lib/service_locator.dart | 7 +- pubspec.yaml | 9 +- 16 files changed, 132 insertions(+), 184 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5fc4a8e5..98fa33d7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.3.10' + flutter-version: '3.7.0' - run: flutter pub get - name: Run Tests run: flutter test diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 641df362..bea9766d 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.3.10' + flutter-version: '3.7.0' - run: flutter pub get - name: Run Tests run: flutter test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e118238d..3698719f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: java-version: '12.x' - uses: subosito/flutter-action@v1 with: - flutter-version: '3.3.10' + flutter-version: '3.7.0' channel: 'stable' # or: 'beta', 'dev' or 'master' - run: flutter pub get - run: flutter test ./test/mirea_test.dart diff --git a/lib/data/repositories/forum_repository_impl.dart b/lib/data/repositories/forum_repository_impl.dart index 09a80250..9aad43fd 100644 --- a/lib/data/repositories/forum_repository_impl.dart +++ b/lib/data/repositories/forum_repository_impl.dart @@ -1,5 +1,5 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/data/datasources/forum_local.dart'; @@ -8,7 +8,7 @@ import 'package:rtu_mirea_app/domain/entities/forum_member.dart'; import 'package:rtu_mirea_app/domain/repositories/forum_repository.dart'; class ForumRepositoryImpl implements ForumRepository { - final InternetConnectionChecker connectionChecker; + final InternetConnectionCheckerPlus connectionChecker; final ForumRemoteData remoteDataSource; final ForumLocalData localDataSource; diff --git a/lib/data/repositories/github_repository_impl.dart b/lib/data/repositories/github_repository_impl.dart index 48c24b14..5ccaf5da 100644 --- a/lib/data/repositories/github_repository_impl.dart +++ b/lib/data/repositories/github_repository_impl.dart @@ -1,5 +1,5 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/data/datasources/github_local.dart'; @@ -10,7 +10,7 @@ import 'package:rtu_mirea_app/domain/repositories/github_repository.dart'; class GithubRepositoryImpl implements GithubRepository { final GithubRemoteData remoteDataSource; final GithubLocalData localDataSource; - final InternetConnectionChecker connectionChecker; + final InternetConnectionCheckerPlus connectionChecker; GithubRepositoryImpl({ required this.remoteDataSource, diff --git a/lib/data/repositories/news_repository_impl.dart b/lib/data/repositories/news_repository_impl.dart index 119e8f75..cf7a09aa 100644 --- a/lib/data/repositories/news_repository_impl.dart +++ b/lib/data/repositories/news_repository_impl.dart @@ -1,5 +1,5 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/data/datasources/news_remote.dart'; @@ -8,7 +8,7 @@ import 'package:rtu_mirea_app/domain/repositories/news_repository.dart'; class NewsRepositoryImpl implements NewsRepository { final NewsRemoteData remoteDataSource; - final InternetConnectionChecker connectionChecker; + final InternetConnectionCheckerPlus connectionChecker; NewsRepositoryImpl({ required this.remoteDataSource, diff --git a/lib/data/repositories/schedule_repository_impl.dart b/lib/data/repositories/schedule_repository_impl.dart index b53c9248..648ec41c 100644 --- a/lib/data/repositories/schedule_repository_impl.dart +++ b/lib/data/repositories/schedule_repository_impl.dart @@ -1,5 +1,5 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/data/datasources/schedule_local.dart'; @@ -13,7 +13,7 @@ import 'package:rtu_mirea_app/domain/repositories/schedule_repository.dart'; class ScheduleRepositoryImpl implements ScheduleRepository { final ScheduleRemoteData remoteDataSource; final ScheduleLocalData localDataSource; - final InternetConnectionChecker connectionChecker; + final InternetConnectionCheckerPlus connectionChecker; ScheduleRepositoryImpl({ required this.remoteDataSource, diff --git a/lib/data/repositories/strapi_repository_impl.dart b/lib/data/repositories/strapi_repository_impl.dart index 34022c45..6dadc66c 100644 --- a/lib/data/repositories/strapi_repository_impl.dart +++ b/lib/data/repositories/strapi_repository_impl.dart @@ -1,5 +1,5 @@ import 'package:dartz/dartz.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; @@ -11,7 +11,7 @@ import 'package:rtu_mirea_app/domain/repositories/strapi_repository.dart'; class StrapiRepositoryImpl implements StrapiRepository { final StrapiRemoteData remoteDataSource; - final InternetConnectionChecker connectionChecker; + final InternetConnectionCheckerPlus connectionChecker; final PackageInfo packageInfo; StrapiRepositoryImpl({ diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index e43b6b6f..a9613625 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -1,4 +1,4 @@ -import 'package:internet_connection_checker/internet_connection_checker.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:rtu_mirea_app/common/errors/exceptions.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:dartz/dartz.dart'; @@ -14,7 +14,7 @@ import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; class UserRepositoryImpl implements UserRepository { final UserRemoteData remoteDataSource; final UserLocalData localDataSource; - final InternetConnectionChecker connectionChecker; + final InternetConnectionCheckerPlus connectionChecker; UserRepositoryImpl( {required this.remoteDataSource, diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index d4d71cf9..a0368979 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -2,7 +2,7 @@ // ignore_for_file: lines_longer_than_80_chars import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; + show TargetPlatform, defaultTargetPlatform, kIsWeb; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -16,20 +16,30 @@ import 'package:flutter/foundation.dart' /// ``` class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { - if (kIsWeb) { - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for web - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); + try { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.fuchsia: + break; + case TargetPlatform.linux: + break; + case TargetPlatform.windows: + break; + } + } catch (e) { + // If we can't determine the platform, we'll just return the web options. + if (kIsWeb) { + return web; + } } - // ignore: missing_enum_constant_in_switch - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; + + if (kIsWeb) { + return web; } throw UnsupportedError( @@ -37,6 +47,14 @@ class DefaultFirebaseOptions { ); } + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyCTfdp1rBC6PkVjwOdZ5XM7_zzwoN0BVPM', + appId: '1:510978291920:web:0c6dc2379d80661b8c46d5', + messagingSenderId: '510978291920', + projectId: 'rtu-mirea-app', + storageBucket: 'rtu-mirea-app.appspot.com', + ); + static const FirebaseOptions android = FirebaseOptions( apiKey: 'AIzaSyCTfdp1rBC6PkVjwOdZ5XM7_zzwoN0BVPM', appId: '1:510978291920:android:0c6dc2379d80661b8c46d5', diff --git a/lib/main.dart b/lib/main.dart index 2e27e9ec..7c7e4759 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; @@ -6,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:rtu_mirea_app/common/widget_data_init.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -28,6 +28,7 @@ import 'package:intl/intl_standalone.dart'; import 'package:rtu_mirea_app/service_locator.dart' as dependency_injection; import 'package:url_strategy/url_strategy.dart'; import 'presentation/app_notifier.dart'; +import 'package:intl/intl_browser.dart' as intl_browser; import 'service_locator.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; @@ -37,22 +38,28 @@ import 'package:provider/provider.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + await dependency_injection.setup(); WidgetDataProvider.initData(); - Platform.isAndroid - ? await Firebase.initializeApp() - : await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + // Crashlytics instance must be initialized only on non-web platforms. On web + // platforms, it is throw an exception. + if (!kIsWeb) { + if (kDebugMode) { + await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false); + } else { + await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); + } + } await FirebaseAnalytics.instance.logAppOpen(); if (kDebugMode) { - // Force disable Crashlytics collection while doing every day development - await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false); - // Clear local dota var prefs = getIt(); await prefs.clear(); @@ -64,12 +71,20 @@ Future main() async { setPathUrlStrategy(); - findSystemLocale().then( - (_) => runApp(ChangeNotifierProvider( - create: (context) => getIt(), - child: const App(), - )), - ); + Intl.defaultLocale = 'ru_RU'; + + if (kIsWeb) { + await intl_browser + .findSystemLocale() + .then((value) => Intl.systemLocale = value); + } else { + Intl.systemLocale = await findSystemLocale(); + } + + runApp(ChangeNotifierProvider( + create: (context) => getIt(), + child: const App(), + )); } class App extends StatelessWidget { diff --git a/lib/presentation/core/routes/routes.gr.dart b/lib/presentation/core/routes/routes.gr.dart index b05cc3f5..67bbdf1a 100644 --- a/lib/presentation/core/routes/routes.gr.dart +++ b/lib/presentation/core/routes/routes.gr.dart @@ -12,46 +12,42 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:auto_route/auto_route.dart' as _i19; -import 'package:auto_route/empty_router_widgets.dart' deferred as _i4; +import 'package:auto_route/empty_router_widgets.dart' as _i4; import 'package:flutter/material.dart' as _i20; import 'package:rtu_mirea_app/domain/entities/news_item.dart' as _i23; import 'package:rtu_mirea_app/domain/entities/story.dart' as _i22; import 'package:rtu_mirea_app/domain/entities/user.dart' as _i24; import 'package:rtu_mirea_app/presentation/core/routes/routes.dart' as _i21; -import 'package:rtu_mirea_app/presentation/pages/home_page.dart' - deferred as _i1; -import 'package:rtu_mirea_app/presentation/pages/login/login_page.dart' - deferred as _i11; -import 'package:rtu_mirea_app/presentation/pages/map/map_page.dart' - deferred as _i5; +import 'package:rtu_mirea_app/presentation/pages/home_page.dart' as _i1; +import 'package:rtu_mirea_app/presentation/pages/login/login_page.dart' as _i11; +import 'package:rtu_mirea_app/presentation/pages/map/map_page.dart' as _i5; import 'package:rtu_mirea_app/presentation/pages/news/news_details_page.dart' - deferred as _i9; -import 'package:rtu_mirea_app/presentation/pages/news/news_page.dart' - deferred as _i8; + as _i9; +import 'package:rtu_mirea_app/presentation/pages/news/news_page.dart' as _i8; import 'package:rtu_mirea_app/presentation/pages/news/widgets/stories_wrapper.dart' - deferred as _i3; + as _i3; import 'package:rtu_mirea_app/presentation/pages/onboarding/onboarding_page.dart' - deferred as _i2; + as _i2; import 'package:rtu_mirea_app/presentation/pages/profile/about_app_page.dart' - deferred as _i12; + as _i12; import 'package:rtu_mirea_app/presentation/pages/profile/profile_announces_page.dart' - deferred as _i13; + as _i13; import 'package:rtu_mirea_app/presentation/pages/profile/profile_attendance_page.dart' - deferred as _i14; + as _i14; import 'package:rtu_mirea_app/presentation/pages/profile/profile_detail_page.dart' - deferred as _i15; + as _i15; import 'package:rtu_mirea_app/presentation/pages/profile/profile_lectors_page.dart' - deferred as _i16; + as _i16; import 'package:rtu_mirea_app/presentation/pages/profile/profile_page.dart' - deferred as _i10; + as _i10; import 'package:rtu_mirea_app/presentation/pages/profile/profile_scores_page.dart' - deferred as _i17; + as _i17; import 'package:rtu_mirea_app/presentation/pages/profile/profile_settings_page.dart' - deferred as _i18; + as _i18; import 'package:rtu_mirea_app/presentation/pages/schedule/groups_select_page.dart' - deferred as _i7; + as _i7; import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart' - deferred as _i6; + as _i6; class AppRouter extends _i19.RootStackRouter { AppRouter([_i20.GlobalKey<_i20.NavigatorState>? navigatorKey]) @@ -62,32 +58,23 @@ class AppRouter extends _i19.RootStackRouter { HomeRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i1.loadLibrary, - () => _i1.HomePage(), - ), + child: const _i1.HomePage(), ); }, OnBoardingRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i2.loadLibrary, - () => _i2.OnBoardingPage(), - ), + child: const _i2.OnBoardingPage(), ); }, StoriesWrapperRoute.name: (routeData) { final args = routeData.argsAs(); return _i19.CustomPage( routeData: routeData, - child: _i19.DeferredWidget( - _i3.loadLibrary, - () => _i3.StoriesWrapper( - key: args.key, - stories: args.stories, - storyIndex: args.storyIndex, - ), + child: _i3.StoriesWrapper( + key: args.key, + stories: args.stories, + storyIndex: args.storyIndex, ), customRouteBuilder: _i21.transparentRoute, opaque: false, @@ -97,162 +84,111 @@ class AppRouter extends _i19.RootStackRouter { ScheduleRouter.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i4.loadLibrary, - () => _i4.EmptyRouterPage(), - ), + child: const _i4.EmptyRouterPage(), ); }, NewsRouter.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i4.loadLibrary, - () => _i4.EmptyRouterPage(), - ), + child: const _i4.EmptyRouterPage(), ); }, MapRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i5.loadLibrary, - () => _i5.MapPage(), - ), + child: const _i5.MapPage(), ); }, ProfileRouter.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i4.loadLibrary, - () => _i4.EmptyRouterPage(), - ), + child: const _i4.EmptyRouterPage(), ); }, ScheduleRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i6.loadLibrary, - () => _i6.SchedulePage(), - ), + child: const _i6.SchedulePage(), ); }, GroupsSelectRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i7.loadLibrary, - () => _i7.GroupsSelectPage(), - ), + child: const _i7.GroupsSelectPage(), ); }, NewsRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i8.loadLibrary, - () => _i8.NewsPage(), - ), + child: const _i8.NewsPage(), ); }, NewsDetailsRoute.name: (routeData) { final args = routeData.argsAs(); return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i9.loadLibrary, - () => _i9.NewsDetailsPage( - key: args.key, - newsItem: args.newsItem, - ), + child: _i9.NewsDetailsPage( + key: args.key, + newsItem: args.newsItem, ), ); }, ProfileRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i10.loadLibrary, - () => _i10.ProfilePage(), - ), + child: const _i10.ProfilePage(), ); }, LoginRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i11.loadLibrary, - () => _i11.LoginPage(), - ), + child: const _i11.LoginPage(), ); }, AboutAppRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i12.loadLibrary, - () => _i12.AboutAppPage(), - ), + child: const _i12.AboutAppPage(), ); }, ProfileAnnouncesRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i13.loadLibrary, - () => _i13.ProfileAnnouncesPage(), - ), + child: const _i13.ProfileAnnouncesPage(), ); }, ProfileAttendanceRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i14.loadLibrary, - () => _i14.ProfileAttendancePage(), - ), + child: const _i14.ProfileAttendancePage(), ); }, ProfileDetailRoute.name: (routeData) { final args = routeData.argsAs(); return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i15.loadLibrary, - () => _i15.ProfileDetailPage( - key: args.key, - user: args.user, - ), + child: _i15.ProfileDetailPage( + key: args.key, + user: args.user, ), ); }, ProfileLectrosRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i16.loadLibrary, - () => _i16.ProfileLectrosPage(), - ), + child: const _i16.ProfileLectrosPage(), ); }, ProfileScoresRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i17.loadLibrary, - () => _i17.ProfileScoresPage(), - ), + child: const _i17.ProfileScoresPage(), ); }, ProfileSettingsRoute.name: (routeData) { return _i19.AdaptivePage( routeData: routeData, - child: _i19.DeferredWidget( - _i18.loadLibrary, - () => _i18.ProfileSettingsPage(), - ), + child: const _i18.ProfileSettingsPage(), ); }, }; @@ -262,7 +198,6 @@ class AppRouter extends _i19.RootStackRouter { _i19.RouteConfig( HomeRoute.name, path: '/', - deferredLoading: true, children: [ _i19.RouteConfig( '#redirect', @@ -275,19 +210,16 @@ class AppRouter extends _i19.RootStackRouter { ScheduleRouter.name, path: 'schedule', parent: HomeRoute.name, - deferredLoading: true, children: [ _i19.RouteConfig( ScheduleRoute.name, path: '', parent: ScheduleRouter.name, - deferredLoading: true, ), _i19.RouteConfig( GroupsSelectRoute.name, path: 'select-group', parent: ScheduleRouter.name, - deferredLoading: true, ), ], ), @@ -295,19 +227,16 @@ class AppRouter extends _i19.RootStackRouter { NewsRouter.name, path: 'news', parent: HomeRoute.name, - deferredLoading: true, children: [ _i19.RouteConfig( NewsRoute.name, path: '', parent: NewsRouter.name, - deferredLoading: true, ), _i19.RouteConfig( NewsDetailsRoute.name, path: 'details', parent: NewsRouter.name, - deferredLoading: true, ), ], ), @@ -315,67 +244,56 @@ class AppRouter extends _i19.RootStackRouter { MapRoute.name, path: 'map', parent: HomeRoute.name, - deferredLoading: true, ), _i19.RouteConfig( ProfileRouter.name, path: 'profile', parent: HomeRoute.name, - deferredLoading: true, children: [ _i19.RouteConfig( ProfileRoute.name, path: '', parent: ProfileRouter.name, - deferredLoading: true, ), _i19.RouteConfig( LoginRoute.name, path: 'login', parent: ProfileRouter.name, - deferredLoading: true, ), _i19.RouteConfig( AboutAppRoute.name, path: 'about', parent: ProfileRouter.name, - deferredLoading: true, ), _i19.RouteConfig( ProfileAnnouncesRoute.name, path: 'announces', parent: ProfileRouter.name, - deferredLoading: true, ), _i19.RouteConfig( ProfileAttendanceRoute.name, path: 'attendance', parent: ProfileRouter.name, - deferredLoading: true, ), _i19.RouteConfig( ProfileDetailRoute.name, path: 'details', parent: ProfileRouter.name, - deferredLoading: true, ), _i19.RouteConfig( ProfileLectrosRoute.name, path: 'lectors', parent: ProfileRouter.name, - deferredLoading: true, ), _i19.RouteConfig( ProfileScoresRoute.name, path: 'scores', parent: ProfileRouter.name, - deferredLoading: true, ), _i19.RouteConfig( ProfileSettingsRoute.name, path: 'settings', parent: ProfileRouter.name, - deferredLoading: true, ), ], ), @@ -384,19 +302,16 @@ class AppRouter extends _i19.RootStackRouter { _i19.RouteConfig( OnBoardingRoute.name, path: '/onboarding', - deferredLoading: true, ), _i19.RouteConfig( StoriesWrapperRoute.name, path: '/story', - deferredLoading: true, ), _i19.RouteConfig( '*#redirect', path: '*', redirectTo: '/', fullMatch: true, - deferredLoading: true, ), ]; } diff --git a/lib/presentation/pages/news/news_page.dart b/lib/presentation/pages/news/news_page.dart index 25fa882c..e0bb8a06 100644 --- a/lib/presentation/pages/news/news_page.dart +++ b/lib/presentation/pages/news/news_page.dart @@ -188,7 +188,7 @@ class _NewsPageState extends State { body: Builder( builder: (BuildContext context) { final innerScrollController = PrimaryScrollController.of(context); - _setupScrollController(innerScrollController!); + _setupScrollController(innerScrollController); return RefreshIndicator( onRefresh: () async { diff --git a/lib/presentation/widgets/buttons/settings_button.dart b/lib/presentation/widgets/buttons/settings_button.dart index e6f4c1d0..a1c46332 100644 --- a/lib/presentation/widgets/buttons/settings_button.dart +++ b/lib/presentation/widgets/buttons/settings_button.dart @@ -30,7 +30,7 @@ class SettingsButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16), child: Icon( icon, - color: Theme.of(context).textTheme.bodyText1?.color, + color: Theme.of(context).textTheme.bodyLarge?.color, ), ), Text( diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 3c9b6fba..5e076bca 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -1,6 +1,5 @@ import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; -import 'package:internet_connection_checker/internet_connection_checker.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:rtu_mirea_app/data/datasources/app_settings_local.dart'; @@ -67,6 +66,7 @@ import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/stories_bloc/stories_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/update_info_bloc/update_info_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'data/repositories/schedule_repository_impl.dart'; @@ -220,11 +220,10 @@ Future setup() async { // Common / Core // External Dependency - getIt.registerLazySingleton( - () => Dio(BaseOptions(connectTimeout: 30000, receiveTimeout: 30000))); + getIt.registerLazySingleton(() => Dio(BaseOptions(receiveTimeout: 20000))); final sharedPreferences = await SharedPreferences.getInstance(); getIt.registerLazySingleton(() => sharedPreferences); - getIt.registerLazySingleton(() => InternetConnectionChecker()); + getIt.registerLazySingleton(() => InternetConnectionCheckerPlus()); final PackageInfo packageInfo = await PackageInfo.fromPlatform(); getIt.registerLazySingleton(() => packageInfo); getIt.registerLazySingleton(() => LksOauth2()); diff --git a/pubspec.yaml b/pubspec.yaml index c1d76cb6..9fa8dbc2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' version: 1.2.3+9 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: @@ -35,7 +35,7 @@ dependencies: # Http client. # See https://pub.dev/packages/dio - dio: ^4.0.4 + dio: ^4.0.6 # Simple no-sql local database. # See https://pub.dev/packages/shared_preferences @@ -71,8 +71,8 @@ dependencies: material_floating_search_bar: ^0.3.6 # Internet connection checker. - # See https://pub.dev/packages/internet_connection_checker - internet_connection_checker: ^1.0.0+1 + # See https://pub.dev/packages/internet_connection_checker_plus + internet_connection_checker_plus: ^1.0.1 # Functional Programming in Dart # See https://pub.dev/packages/dartz/versions/0.10.0-nullsafety.2 @@ -169,6 +169,7 @@ dependencies: freezed_annotation: ^2.1.0 firebase_core: ^2.4.1 + firebase_core_web: ^2.1.0 firebase_analytics: ^10.1.0 firebase_crashlytics: ^3.0.11 From 6a0795180b3c5f367b0e78620f6a528b2af33222 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Wed, 15 Feb 2023 22:41:30 +0300 Subject: [PATCH 31/38] feature: Create NFC-Pass Device Connect (#279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add deferredLoading * feature: Create NFC-Pass Device Connect * feat: Create Nfc Fetcher and refactor auth bloc * Удалён AuthBloc. Его функции перенесены в Profile Bloc * Теперь в события не передаются токены доступа к апи. Управление токенами делегируется на репозиторий/remote data-source или отдельные пакеты для управления сетевыми запросами * Создан `fetchNfcCode` event для получения кода пропуск * feat: Add NFC fetching and errors feedback * Update AndroidManifest.xml --- android/app/src/main/AndroidManifest.xml | 1 + android/build.gradle | 4 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- lib/common/errors/exceptions.dart | 4 + lib/common/errors/failures.dart | 4 + lib/data/datasources/user_local.dart | 40 +- lib/data/datasources/user_remote.dart | 182 +- lib/data/models/nfc_pass_model.dart | 33 + lib/data/models/user_model.dart | 15 +- .../repositories/user_repository_impl.dart | 113 +- lib/domain/entities/nfc_pass.dart | 20 + lib/domain/entities/user.dart | 6 + lib/domain/repositories/user_repository.dart | 31 +- lib/domain/usecases/connect_nfc_pass.dart | 30 + lib/domain/usecases/fetch_nfc_code.dart | 34 + lib/domain/usecases/get_announces.dart | 16 +- lib/domain/usecases/get_attendance.dart | 7 +- lib/domain/usecases/get_employees.dart | 7 +- lib/domain/usecases/get_nfc_passes.dart | 29 + lib/domain/usecases/get_scores.dart | 16 +- lib/domain/usecases/get_user_data.dart | 15 +- lib/domain/usecases/log_in.dart | 17 +- .../usecases/send_nfc_not_exist_feedback.dart | 31 + lib/main.dart | 33 +- .../bloc/announces_bloc/announces_bloc.dart | 2 +- .../bloc/announces_bloc/announces_event.dart | 8 +- .../bloc/attendance_bloc/attendance_bloc.dart | 2 +- .../attendance_bloc/attendance_event.dart | 4 +- .../bloc/auth_bloc/auth_bloc.dart | 65 - .../bloc/auth_bloc/auth_event.dart | 22 - .../bloc/auth_bloc/auth_state.dart | 30 - .../bloc/employee_bloc/employee_bloc.dart | 3 +- .../bloc/employee_bloc/employee_event.dart | 5 +- .../nfc_feedback_bloc/nfc_feedback_bloc.dart | 35 + .../nfc_feedback_bloc.freezed.dart | 922 ++++++++ .../nfc_feedback_bloc/nfc_feedback_event.dart | 12 + .../nfc_feedback_bloc/nfc_feedback_state.dart | 9 + .../bloc/nfc_pass_bloc/nfc_pass_bloc.dart | 169 ++ .../nfc_pass_bloc/nfc_pass_bloc.freezed.dart | 1865 +++++++++++++++++ .../bloc/nfc_pass_bloc/nfc_pass_event.dart | 19 + .../bloc/nfc_pass_bloc/nfc_pass_state.dart | 15 + .../bloc/profile_bloc/profile_bloc.dart | 28 - .../bloc/profile_bloc/profile_event.dart | 14 - .../bloc/profile_bloc/profile_state.dart | 21 - .../bloc/scores_bloc/scores_bloc.dart | 2 +- .../bloc/scores_bloc/scores_event.dart | 8 +- .../bloc/user_bloc/user_bloc.dart | 100 + .../bloc/user_bloc/user_bloc.freezed.dart | 1125 ++++++++++ .../bloc/user_bloc/user_event.dart | 9 + .../bloc/user_bloc/user_state.dart | 9 + lib/presentation/core/routes/routes.dart | 5 + lib/presentation/core/routes/routes.gr.dart | 203 +- lib/presentation/pages/login/login_page.dart | 42 +- lib/presentation/pages/map/map_page.dart | 1 - .../profile/profile_attendance_page.dart | 19 +- .../pages/profile/profile_lectors_page.dart | 21 +- .../pages/profile/profile_nfc_pass_page.dart | 695 ++++++ .../pages/profile/profile_page.dart | 299 ++- .../pages/profile/profile_scores_page.dart | 20 +- lib/service_locator.dart | 52 +- pubspec.yaml | 24 +- 61 files changed, 5921 insertions(+), 653 deletions(-) create mode 100644 lib/data/models/nfc_pass_model.dart create mode 100644 lib/domain/entities/nfc_pass.dart create mode 100644 lib/domain/usecases/connect_nfc_pass.dart create mode 100644 lib/domain/usecases/fetch_nfc_code.dart create mode 100644 lib/domain/usecases/get_nfc_passes.dart create mode 100644 lib/domain/usecases/send_nfc_not_exist_feedback.dart delete mode 100644 lib/presentation/bloc/auth_bloc/auth_bloc.dart delete mode 100644 lib/presentation/bloc/auth_bloc/auth_event.dart delete mode 100644 lib/presentation/bloc/auth_bloc/auth_state.dart create mode 100644 lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.dart create mode 100644 lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.freezed.dart create mode 100644 lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_event.dart create mode 100644 lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_state.dart create mode 100644 lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart create mode 100644 lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.freezed.dart create mode 100644 lib/presentation/bloc/nfc_pass_bloc/nfc_pass_event.dart create mode 100644 lib/presentation/bloc/nfc_pass_bloc/nfc_pass_state.dart delete mode 100644 lib/presentation/bloc/profile_bloc/profile_bloc.dart delete mode 100644 lib/presentation/bloc/profile_bloc/profile_event.dart delete mode 100644 lib/presentation/bloc/profile_bloc/profile_state.dart create mode 100644 lib/presentation/bloc/user_bloc/user_bloc.dart create mode 100644 lib/presentation/bloc/user_bloc/user_bloc.freezed.dart create mode 100644 lib/presentation/bloc/user_bloc/user_event.dart create mode 100644 lib/presentation/bloc/user_bloc/user_state.dart create mode 100644 lib/presentation/pages/profile/profile_nfc_pass_page.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3aaa4d79..e0f4381a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="ninja.mirea.mireaapp"> + setTokenToCache(String token); Future getTokenFromCache(); Future removeTokenFromCache(); + + Future getNfcCodeFromCache(); + Future setNfcCodeToCache(int code); + Future removeNfcCodeFromCache(); } class UserLocalDataImpl implements UserLocalData { final SharedPreferences sharedPreferences; + final FlutterSecureStorage secureStorage; - UserLocalDataImpl({required this.sharedPreferences}); + UserLocalDataImpl({ + required this.sharedPreferences, + required this.secureStorage, + }); @override Future setTokenToCache(String token) { - return sharedPreferences.setString('auth_token', token); + return secureStorage.write(key: 'lks_access_token', value: token); } @override - Future getTokenFromCache() { - String? token = sharedPreferences.getString('auth_token'); + Future getTokenFromCache() async { + String? token = await secureStorage.read(key: 'lks_access_token'); if (token == null) throw CacheException('Auth token are not set'); return Future.value(token); } @override Future removeTokenFromCache() { - return sharedPreferences.remove('auth_token'); + return secureStorage.delete(key: 'lks_access_token'); + } + + @override + Future getNfcCodeFromCache() async { + String? value = await secureStorage.read(key: 'nfc_code'); + + if (value == null) throw CacheException('NFC code are not set'); + + return Future.value(int.parse(value)); + } + + @override + Future setNfcCodeToCache(int code) async { + await secureStorage.write(key: 'nfc_code', value: code.toString()); + } + + @override + Future removeNfcCodeFromCache() async { + await secureStorage.delete(key: 'nfc_code'); } } diff --git a/lib/data/datasources/user_remote.dart b/lib/data/datasources/user_remote.dart index 0539bcb9..0c984f19 100644 --- a/lib/data/datasources/user_remote.dart +++ b/lib/data/datasources/user_remote.dart @@ -6,22 +6,29 @@ import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:rtu_mirea_app/data/models/announce_model.dart'; import 'package:rtu_mirea_app/data/models/attendance_model.dart'; import 'package:rtu_mirea_app/data/models/employee_model.dart'; +import 'package:rtu_mirea_app/data/models/nfc_pass_model.dart'; import 'package:rtu_mirea_app/data/models/score_model.dart'; import 'package:rtu_mirea_app/data/models/user_model.dart'; abstract class UserRemoteData { Future auth(); Future logOut(); - Future getProfileData(String token); - Future> getAnnounces(String token); - Future> getEmployees(String token, String name); - Future> getAttendance( - String token, String dateStart, String dateEnd); - Future>> getScores(String token); + Future getProfileData(); + Future> getAnnounces(); + Future> getEmployees(String name); + Future> getAttendance(String dateStart, String dateEnd); + Future>> getScores(); + Future> getNfcPasses( + String code, String studentId, String deviceId); + Future getNfcCode(String code, String studentId, String deviceId); + Future connectNfcPass( + String code, String studentId, String deviceId, String deviceName); + Future sendNfcNotExistFeedback( + String fullName, String group, String personalNumber, String studentId); } class UserRemoteDataImpl implements UserRemoteData { - static const _apiUrl = 'https://lks.mirea.ninja/api/'; + static const _apiUrl = 'https://lks.mirea.ninja/api'; final Dio httpClient; final LksOauth2 lksOauth2; @@ -45,13 +52,14 @@ class UserRemoteDataImpl implements UserRemoteData { } @override - Future getProfileData(String token) async { + Future getProfileData() async { final response = await lksOauth2.oauth2Helper.get( - '$_apiUrl?action=getData&url=https://lk.mirea.ru/profile/', + '$_apiUrl/?action=getData&url=https://lk.mirea.ru/profile/', ); var jsonResponse = json.decode(response.body); - log('Status code: ${response.statusCode}, Response: ${response.body}'); + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'getProfileData'); if (jsonResponse.containsKey('errors')) { throw ServerException(jsonResponse['errors'][0]); @@ -59,17 +67,18 @@ class UserRemoteDataImpl implements UserRemoteData { if (response.statusCode == 200) { return UserModel.fromRawJson(response.body); } else { - throw ServerException('Response status code is $response.statusCode'); + throw ServerException('Response status code is ${response.statusCode}'); } } @override - Future> getAnnounces(String token) async { + Future> getAnnounces() async { final response = await lksOauth2.oauth2Helper.get( - '$_apiUrl?action=getData&url=https://lk.mirea.ru/livestream/', + '$_apiUrl/?action=getData&url=https://lk.mirea.ru/livestream/', ); - log('Status code: ${response.statusCode}, Response: ${response.body}'); + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'getAnnounces'); var jsonResponse = json.decode(response.body); if (jsonResponse.containsKey('errors')) { @@ -83,17 +92,18 @@ class UserRemoteDataImpl implements UserRemoteData { } return announces; } else { - throw ServerException('Response status code is $response.statusCode'); + throw ServerException('Response status code is ${response.statusCode}'); } } @override - Future> getEmployees(String token, String name) async { + Future> getEmployees(String name) async { final response = await lksOauth2.oauth2Helper.get( - '$_apiUrl?action=getData&url=https://lk.mirea.ru/lectors/&page=undefined&findname=$name', + '$_apiUrl/?action=getData&url=https://lk.mirea.ru/lectors/&page=undefined&findname=$name', ); - log('Status code: ${response.statusCode}, Response: ${response.body}'); + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'getEmployees'); var jsonResponse = json.decode(response.body); if (jsonResponse.containsKey('errors')) { @@ -109,17 +119,18 @@ class UserRemoteDataImpl implements UserRemoteData { } return employees; } else { - throw ServerException('Response status code is $response.statusCode'); + throw ServerException('Response status code is ${response.statusCode}'); } } @override - Future>> getScores(String token) async { + Future>> getScores() async { final response = await lksOauth2.oauth2Helper.get( - '$_apiUrl?action=getData&url=https://lk.mirea.ru/learning/scores/', + '$_apiUrl/?action=getData&url=https://lk.mirea.ru/learning/scores/', ); - log('Status code: ${response.statusCode}, Response: ${response.body}'); + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'getScores'); var jsonResponse = json.decode(response.body); if (jsonResponse.containsKey('errors')) { @@ -140,18 +151,74 @@ class UserRemoteDataImpl implements UserRemoteData { return scores; } else { - throw ServerException('Response status code is $response.statusCode'); + throw ServerException('Response status code is ${response.statusCode}'); + } + } + + @override + Future> getNfcPasses( + String code, String studentId, String deviceId) async { + final response = await lksOauth2.oauth2Helper + .get('$_apiUrl/cms/nfc-passes/$code/$studentId/$deviceId'); + + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'getScores'); + + if (response.statusCode != 200) { + try { + var jsonResponse = json.decode(response.body); + if (jsonResponse.containsKey('message')) { + throw ServerException(jsonResponse['message']); + } + } catch (e) { + throw ServerException('Response status code is ${response.statusCode}'); + } + } + + var jsonResponse = json.decode(response.body); + + List userNfcPasses = []; + userNfcPasses = List.from(jsonResponse['data'] + .map((x) => NfcPassModel.fromJson(x['attributes']))); + return userNfcPasses; + } + + @override + Future connectNfcPass( + String code, String studentId, String deviceId, String deviceName) async { + final data = { + 'code': code, + 'studentId': studentId, + 'deviceId': deviceId, + 'deviceName': deviceName, + }; + + final response = await lksOauth2.oauth2Helper.post( + '$_apiUrl/cms/nfc-passes', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: json.encode(data), + ); + + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'connectNfcPass'); + + if (response.statusCode != 200) { + throw ServerException('Response status code is ${response.statusCode}'); } } @override Future> getAttendance( - String token, String dateStart, String dateEnd) async { + String dateStart, String dateEnd) async { final response = await lksOauth2.oauth2Helper.get( - '$_apiUrl?action=getData&url=https://lk.mirea.ru/schedule/attendance/&startDate=$dateStart&endDate=$dateEnd', + '$_apiUrl/?action=getData&url=https://lk.mirea.ru/schedule/attendance/&startDate=$dateStart&endDate=$dateEnd', ); - log('Status code: ${response.statusCode}, Response: ${response.body}'); + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'getAttendance'); var jsonResponse = json.decode(response.body); if (jsonResponse.containsKey('errors')) { @@ -174,7 +241,68 @@ class UserRemoteDataImpl implements UserRemoteData { } return attendance; } else { - throw ServerException('Response status code is $response.statusCode'); + throw ServerException('Response status code is ${response.statusCode}'); + } + } + + @override + Future getNfcCode(String code, String studentId, String deviceId) async { + final response = await lksOauth2.oauth2Helper + .get('$_apiUrl/get-nfc-code/$code/$studentId/$deviceId'); + + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'getNfcCode'); + + var jsonResponse = json.decode(response.body); + + if (jsonResponse.containsKey('code')) { + return jsonResponse['code']; + } else { + // Local api error + if (jsonResponse.containsKey('message')) { + throw ServerException(jsonResponse['message']); + } + // LKS api error + else if (jsonResponse.containsKey('error') && + jsonResponse['error'] == 'StaffnodeNotExist') { + throw NfcStaffnodeNotExistException(); + } else { + throw ServerException('${jsonResponse['error']}'); + } + } + } + + @override + Future sendNfcNotExistFeedback(String fullName, String group, + String personalNumber, String studentId) async { + final data = { + 'fullName': fullName, + 'group': group, + 'personalNumber': personalNumber, + 'studentId': studentId, + }; + + final response = await lksOauth2.oauth2Helper.post( + '$_apiUrl/cms/nfc-pass-not-exist-feedback', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: json.encode(data), + ); + + log('Status code: ${response.statusCode}, Response: ${response.body}', + name: 'sendNfcNotExistFeedback'); + + if (response.statusCode != 200) { + if (response.statusCode == 400) { + if (response.body.contains("This attribute must be unique")) { + throw ServerException('Уже отправлено. Пожалуйста, подождите. ' + 'Время обработки заявки - до 7 рабочих дней.'); + } + } else { + throw ServerException('Response status code is ${response.statusCode}'); + } } } } diff --git a/lib/data/models/nfc_pass_model.dart b/lib/data/models/nfc_pass_model.dart new file mode 100644 index 00000000..f4c2e9a0 --- /dev/null +++ b/lib/data/models/nfc_pass_model.dart @@ -0,0 +1,33 @@ +import 'package:rtu_mirea_app/domain/entities/nfc_pass.dart'; + +class NfcPassModel extends NfcPass { + const NfcPassModel({ + required code, + required studentId, + required deviceName, + required deviceId, + required connected, + }) : super( + code: code, + studentId: studentId, + deviceName: deviceName, + connected: connected, + deviceId: deviceId, + ); + + factory NfcPassModel.fromJson(Map json) => NfcPassModel( + code: json["code"], + studentId: json["studentId"], + deviceName: json["deviceName"], + connected: json["connected"], + deviceId: json["deviceId"], + ); + + Map toJson() => { + "code": code, + "studentId": studentId, + "deviceName": deviceName, + "connected": connected, + "deviceId": deviceId, + }; +} diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart index 8a880504..9a83920f 100644 --- a/lib/data/models/user_model.dart +++ b/lib/data/models/user_model.dart @@ -26,6 +26,8 @@ class UserModel extends User { required department, required prodDepartment, required type, + required code, + required studentId, }) : super( id: id, login: login, @@ -49,14 +51,21 @@ class UserModel extends User { department: department, prodDepartment: prodDepartment, type: type, + code: code, + studentId: studentId, ); factory UserModel.fromRawJson(String str) => UserModel.fromJson(json.decode(str)); factory UserModel.fromJson(Map json) { - final student = json["STUDENTS"].values.first; - final eduProgram = json["EDU_PROGRAM"].values.first; + final student = json["STUDENTS"].values.firstWhere((element) => + !element["PROPERTIES"]["PERSONAL_NUMBER"]["VALUE"].contains("Д") && + !element["PROPERTIES"]["PERSONAL_NUMBER"]["VALUE"].contains("Ж")); + + final eduProgram = json["EDU_PROGRAM"] + .values + .firstWhere((element) => element["ACTIVE"] == "Y"); return UserModel( id: json["ID"], @@ -81,6 +90,8 @@ class UserModel extends User { department: eduProgram["PROPERTIES"]["DEPARTMENT"]["VALUE_TEXT"], prodDepartment: eduProgram["PROPERTIES"]["PROD_DEPARTMENT"]["VALUE_TEXT"], type: eduProgram["TYPE"], + code: student["CODE"], + studentId: student["ID"], ); } } diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index a9613625..04be8294 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -7,6 +7,7 @@ import 'package:rtu_mirea_app/data/datasources/user_remote.dart'; import 'package:rtu_mirea_app/domain/entities/announce.dart'; import 'package:rtu_mirea_app/domain/entities/attendance.dart'; import 'package:rtu_mirea_app/domain/entities/employee.dart'; +import 'package:rtu_mirea_app/domain/entities/nfc_pass.dart'; import 'package:rtu_mirea_app/domain/entities/score.dart'; import 'package:rtu_mirea_app/domain/entities/user.dart'; import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; @@ -16,12 +17,13 @@ class UserRepositoryImpl implements UserRepository { final UserLocalData localDataSource; final InternetConnectionCheckerPlus connectionChecker; - UserRepositoryImpl( - {required this.remoteDataSource, - required this.localDataSource, - required this.connectionChecker}); + UserRepositoryImpl({ + required this.remoteDataSource, + required this.localDataSource, + required this.connectionChecker, + }); @override - Future> logIn(String login, String password) async { + Future> logIn() async { if (await connectionChecker.hasConnection) { try { final authToken = await remoteDataSource.auth(); @@ -40,18 +42,18 @@ class UserRepositoryImpl implements UserRepository { @override Future> logOut() async { - // try { - // return Right(await localDataSource.removeTokenFromCache()); - // } on CacheException { - // return Future.value(const Left(CacheFailure())); - // } - return Right(await remoteDataSource.logOut()); + try { + await remoteDataSource.logOut(); + return Right(await localDataSource.removeTokenFromCache()); + } on CacheException { + return Future.value(const Left(CacheFailure())); + } } @override - Future> getUserData(String token) async { + Future> getUserData() async { try { - final user = await remoteDataSource.getProfileData(token); + final user = await remoteDataSource.getProfileData(); return Future.value(Right(user)); } on ServerException { return Future.value(const Left(ServerFailure())); @@ -62,19 +64,16 @@ class UserRepositoryImpl implements UserRepository { Future> getAuthToken() async { try { final token = await localDataSource.getTokenFromCache(); - final _ = await remoteDataSource.getProfileData(token); return Future.value(Right(token)); } on CacheException { return Future.value(const Left(CacheFailure())); - } on ServerException { - return Future.value(const Left(ServerFailure())); } } @override - Future>> getAnnounces(String token) async { + Future>> getAnnounces() async { try { - final announces = await remoteDataSource.getAnnounces(token); + final announces = await remoteDataSource.getAnnounces(); return Right(announces); } on ServerException { return Future.value(const Left(ServerFailure())); @@ -82,10 +81,9 @@ class UserRepositoryImpl implements UserRepository { } @override - Future>> getEmployees( - String token, String name) async { + Future>> getEmployees(String name) async { try { - final employees = await remoteDataSource.getEmployees(token, name); + final employees = await remoteDataSource.getEmployees(name); return Right(employees); } on ServerException { return Future.value(const Left(ServerFailure())); @@ -93,10 +91,9 @@ class UserRepositoryImpl implements UserRepository { } @override - Future>>> getScores( - String token) async { + Future>>> getScores() async { try { - final scores = await remoteDataSource.getScores(token); + final scores = await remoteDataSource.getScores(); return Right(scores); } on ServerException { return Future.value(const Left(ServerFailure())); @@ -105,13 +102,77 @@ class UserRepositoryImpl implements UserRepository { @override Future>> getAattendance( - String token, String dateStart, String dateEnd) async { + String dateStart, String dateEnd) async { try { final attendance = - await remoteDataSource.getAttendance(token, dateStart, dateEnd); + await remoteDataSource.getAttendance(dateStart, dateEnd); return Right(attendance); } on ServerException { return Future.value(const Left(ServerFailure())); } } + + @override + Future>> getNfcPasses( + String code, String studentId, String deviceId) async { + try { + final nfcPasses = + await remoteDataSource.getNfcPasses(code, studentId, deviceId); + return Right(nfcPasses); + } on ServerException { + return Future.value(const Left(ServerFailure())); + } + } + + @override + Future> connectNfcPass( + String code, String studentId, String deviceId, String deviceName) { + try { + remoteDataSource.connectNfcPass(code, studentId, deviceId, deviceName); + return Future.value(const Right(null)); + } on ServerException { + return Future.value(const Left(ServerFailure())); + } + } + + @override + Future> fetchNfcCode( + String code, String studentId, String deviceId, String deviceName) async { + final isLoggedIn = await getAuthToken().then((value) => value.isRight()); + + if (!isLoggedIn) { + return const Left(ServerFailure("Пользователь не авторизован")); + } + + if (await connectionChecker.hasConnection) { + try { + final nfcCode = + await remoteDataSource.getNfcCode(code, studentId, deviceId); + await localDataSource.setNfcCodeToCache(nfcCode); + return const Right(null); + } on ServerException catch (e) { + if (e is NfcStaffnodeNotExistException) { + return const Left(NfcStaffnodeNotExistFailure()); + } + + await localDataSource.removeNfcCodeFromCache(); + + return Left(ServerFailure(e.cause)); + } + } else { + return const Left(ServerFailure("Отсутсвует соединение с интернетом")); + } + } + + @override + Future> sendNfcNotExistFeedback( + String fullName, String group, String personalNumber, String studentId) { + try { + remoteDataSource.sendNfcNotExistFeedback( + fullName, group, personalNumber, studentId); + return Future.value(const Right(null)); + } on ServerException catch (e) { + return Future.value(Left(ServerFailure(e.cause))); + } + } } diff --git a/lib/domain/entities/nfc_pass.dart b/lib/domain/entities/nfc_pass.dart new file mode 100644 index 00000000..c9b7a2e8 --- /dev/null +++ b/lib/domain/entities/nfc_pass.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; + +class NfcPass extends Equatable { + const NfcPass({ + required this.code, + required this.studentId, + required this.deviceName, + required this.deviceId, + required this.connected, + }); + + final String code; + final String studentId; + final String deviceName; + final String deviceId; + final bool connected; + + @override + List get props => [code, studentId, deviceName, deviceId, connected]; +} diff --git a/lib/domain/entities/user.dart b/lib/domain/entities/user.dart index 8d47b5af..4462051f 100644 --- a/lib/domain/entities/user.dart +++ b/lib/domain/entities/user.dart @@ -23,6 +23,8 @@ class User extends Equatable { final String department; final String prodDepartment; final String type; + final String code; + final String studentId; const User({ required this.id, @@ -47,6 +49,8 @@ class User extends Equatable { required this.department, required this.prodDepartment, required this.type, + required this.code, + required this.studentId, }); @override @@ -73,5 +77,7 @@ class User extends Equatable { department, prodDepartment, type, + code, + studentId, ]; } diff --git a/lib/domain/repositories/user_repository.dart b/lib/domain/repositories/user_repository.dart index 5fc38c72..486c55cf 100644 --- a/lib/domain/repositories/user_repository.dart +++ b/lib/domain/repositories/user_repository.dart @@ -3,18 +3,35 @@ import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/domain/entities/announce.dart'; import 'package:rtu_mirea_app/domain/entities/attendance.dart'; import 'package:rtu_mirea_app/domain/entities/employee.dart'; +import 'package:rtu_mirea_app/domain/entities/nfc_pass.dart'; import 'package:rtu_mirea_app/domain/entities/score.dart'; import 'package:rtu_mirea_app/domain/entities/user.dart'; abstract class UserRepository { - Future> logIn(String login, String password); + Future> logIn(); Future> logOut(); - Future> getUserData(String token); - Future>> getAnnounces(String token); - Future>> getEmployees( - String token, String name); - Future>>> getScores(String token); + Future> getUserData(); + Future>> getAnnounces(); + Future>> getEmployees(String name); + Future>>> getScores(); Future>> getAattendance( - String token, String dateStart, String dateEnd); + String dateStart, String dateEnd); Future> getAuthToken(); + + Future>> getNfcPasses( + String code, String studentId, String deviceId); + Future> connectNfcPass( + String code, String studentId, String deviceId, String deviceName); + + /// Fetch NFC code from server. If device is not connected, then clear all + /// local NFC codes from [FlutterSecureStorage]. If device is connected, then + /// update local data with new NFC code. + /// + /// Returns [Failure] if device is not connected or if there is no internet + /// connection. Returns [void] if device is connected. + Future> fetchNfcCode( + String code, String studentId, String deviceId, String deviceName); + + Future> sendNfcNotExistFeedback( + String fullName, String group, String personalNumber, String studentId); } diff --git a/lib/domain/usecases/connect_nfc_pass.dart b/lib/domain/usecases/connect_nfc_pass.dart new file mode 100644 index 00000000..c09f187c --- /dev/null +++ b/lib/domain/usecases/connect_nfc_pass.dart @@ -0,0 +1,30 @@ +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; +import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; + +class ConnectNfcPass extends UseCase { + final UserRepository userRepository; + + ConnectNfcPass(this.userRepository); + + @override + Future> call(ConnectNfcPassParams params) { + return userRepository.connectNfcPass( + params.code, params.studentId, params.deviceId, params.deviceName); + } +} + +class ConnectNfcPassParams extends Equatable { + const ConnectNfcPassParams( + this.code, this.studentId, this.deviceId, this.deviceName); + + final String code; + final String studentId; + final String deviceId; + final String deviceName; + + @override + List get props => [code, studentId, deviceId, deviceName]; +} diff --git a/lib/domain/usecases/fetch_nfc_code.dart b/lib/domain/usecases/fetch_nfc_code.dart new file mode 100644 index 00000000..8b70435c --- /dev/null +++ b/lib/domain/usecases/fetch_nfc_code.dart @@ -0,0 +1,34 @@ +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; +import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; + +/// Get NFC code from server if device is connected. If device is not connected, +/// then clear all local NFC codes. +/// +/// If device is connected, then update local data with new NFC code. +class FetchNfcCode extends UseCase { + final UserRepository userRepository; + + FetchNfcCode(this.userRepository); + + @override + Future> call(FetchNfcCodeParams params) { + return userRepository.fetchNfcCode( + params.code, params.studentId, params.deviceId, params.deviceName); + } +} + +class FetchNfcCodeParams extends Equatable { + const FetchNfcCodeParams( + this.code, this.studentId, this.deviceId, this.deviceName); + + final String code; + final String studentId; + final String deviceId; + final String deviceName; + + @override + List get props => [code, studentId, deviceId, deviceName]; +} diff --git a/lib/domain/usecases/get_announces.dart b/lib/domain/usecases/get_announces.dart index c8075473..6833a212 100644 --- a/lib/domain/usecases/get_announces.dart +++ b/lib/domain/usecases/get_announces.dart @@ -1,26 +1,16 @@ import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/domain/entities/announce.dart'; import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; -class GetAnnounces extends UseCase, GetAnnouncesParams> { +class GetAnnounces extends UseCase, void> { final UserRepository userRepository; GetAnnounces(this.userRepository); @override - Future>> call( - GetAnnouncesParams params) async { - return await userRepository.getAnnounces(params.token); + Future>> call([params]) async { + return await userRepository.getAnnounces(); } } - -class GetAnnouncesParams extends Equatable { - final String token; - const GetAnnouncesParams(this.token); - - @override - List get props => [token]; -} diff --git a/lib/domain/usecases/get_attendance.dart b/lib/domain/usecases/get_attendance.dart index 389b9c30..e5f464d6 100644 --- a/lib/domain/usecases/get_attendance.dart +++ b/lib/domain/usecases/get_attendance.dart @@ -14,16 +14,15 @@ class GetAttendance extends UseCase, GetAttendanceParams> { Future>> call( GetAttendanceParams params) async { return await userRepository.getAattendance( - params.token, params.startDate, params.endDate); + params.startDate, params.endDate); } } class GetAttendanceParams extends Equatable { - final String token; final String startDate; final String endDate; - const GetAttendanceParams(this.token, this.startDate, this.endDate); + const GetAttendanceParams(this.startDate, this.endDate); @override - List get props => [token, startDate, endDate]; + List get props => [startDate, endDate]; } diff --git a/lib/domain/usecases/get_employees.dart b/lib/domain/usecases/get_employees.dart index 4ec58a39..0c3f813c 100644 --- a/lib/domain/usecases/get_employees.dart +++ b/lib/domain/usecases/get_employees.dart @@ -12,15 +12,14 @@ class GetEmployees extends UseCase, GetEmployeesParams> { @override Future>> call(GetEmployeesParams params) { - return userRepository.getEmployees(params.token, params.name); + return userRepository.getEmployees(params.name); } } class GetEmployeesParams extends Equatable { - final String token; final String name; - const GetEmployeesParams(this.token, this.name); + const GetEmployeesParams(this.name); @override - List get props => [token, name]; + List get props => [name]; } diff --git a/lib/domain/usecases/get_nfc_passes.dart b/lib/domain/usecases/get_nfc_passes.dart new file mode 100644 index 00000000..a5a05794 --- /dev/null +++ b/lib/domain/usecases/get_nfc_passes.dart @@ -0,0 +1,29 @@ +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/domain/entities/nfc_pass.dart'; +import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; +import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; + +class GetNfcPasses extends UseCase, GetNfcPassesParams> { + final UserRepository userRepository; + + GetNfcPasses(this.userRepository); + + @override + Future>> call(GetNfcPassesParams params) { + return userRepository.getNfcPasses( + params.code, params.studentId, params.deviceId); + } +} + +class GetNfcPassesParams extends Equatable { + const GetNfcPassesParams(this.code, this.studentId, this.deviceId); + + final String code; + final String studentId; + final String deviceId; + + @override + List get props => [code, studentId, deviceId]; +} diff --git a/lib/domain/usecases/get_scores.dart b/lib/domain/usecases/get_scores.dart index b397f850..9030171d 100644 --- a/lib/domain/usecases/get_scores.dart +++ b/lib/domain/usecases/get_scores.dart @@ -1,26 +1,16 @@ import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/domain/entities/score.dart'; import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; -class GetScores extends UseCase>, GetScoresParams> { +class GetScores extends UseCase>, void> { final UserRepository userRepository; GetScores(this.userRepository); @override - Future>>> call( - GetScoresParams params) { - return userRepository.getScores(params.token); + Future>>> call([params]) async { + return userRepository.getScores(); } } - -class GetScoresParams extends Equatable { - final String token; - const GetScoresParams(this.token); - - @override - List get props => [token]; -} diff --git a/lib/domain/usecases/get_user_data.dart b/lib/domain/usecases/get_user_data.dart index 8b601b72..813111ef 100644 --- a/lib/domain/usecases/get_user_data.dart +++ b/lib/domain/usecases/get_user_data.dart @@ -1,25 +1,16 @@ import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/domain/entities/user.dart'; import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; -class GetUserData extends UseCase { +class GetUserData extends UseCase { final UserRepository userRepository; GetUserData(this.userRepository); @override - Future> call(GetUserDataParams params) { - return userRepository.getUserData(params.token); + Future> call([params]) async { + return userRepository.getUserData(); } } - -class GetUserDataParams extends Equatable { - final String token; - const GetUserDataParams(this.token); - - @override - List get props => [token]; -} diff --git a/lib/domain/usecases/log_in.dart b/lib/domain/usecases/log_in.dart index b340af0d..30640c94 100644 --- a/lib/domain/usecases/log_in.dart +++ b/lib/domain/usecases/log_in.dart @@ -1,26 +1,15 @@ import 'package:dartz/dartz.dart'; -import 'package:equatable/equatable.dart'; import 'package:rtu_mirea_app/common/errors/failures.dart'; import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; -class LogIn extends UseCase { +class LogIn extends UseCase { final UserRepository userRepository; LogIn(this.userRepository); @override - Future> call(LogInParams params) async { - return userRepository.logIn(params.login, params.password); + Future> call([params]) async { + return await userRepository.logIn(); } } - -class LogInParams extends Equatable { - final String login; - final String password; - - const LogInParams(this.login, this.password); - - @override - List get props => [login, password]; -} diff --git a/lib/domain/usecases/send_nfc_not_exist_feedback.dart b/lib/domain/usecases/send_nfc_not_exist_feedback.dart new file mode 100644 index 00000000..6ca7028c --- /dev/null +++ b/lib/domain/usecases/send_nfc_not_exist_feedback.dart @@ -0,0 +1,31 @@ +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; +import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; +import 'package:rtu_mirea_app/domain/usecases/usecase.dart'; + +class SendNfcNotExistFeedback + extends UseCase { + final UserRepository userRepository; + + SendNfcNotExistFeedback(this.userRepository); + + @override + Future> call(SendNfcNotExistFeedbackParams params) { + return userRepository.sendNfcNotExistFeedback( + params.fullName, params.group, params.personalNumber, params.studentId); + } +} + +class SendNfcNotExistFeedbackParams extends Equatable { + const SendNfcNotExistFeedbackParams( + this.fullName, this.group, this.personalNumber, this.studentId); + + final String fullName; + final String group; + final String personalNumber; + final String studentId; + + @override + List get props => [fullName, group, personalNumber, studentId]; +} diff --git a/lib/main.dart b/lib/main.dart index 7c7e4759..60715015 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:intl/intl.dart'; import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:rtu_mirea_app/common/widget_data_init.dart'; @@ -13,15 +14,18 @@ import 'package:rtu_mirea_app/presentation/bloc/about_app_bloc/about_app_bloc.da import 'package:rtu_mirea_app/presentation/bloc/announces_bloc/announces_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/app_cubit/app_cubit.dart'; import 'package:rtu_mirea_app/presentation/bloc/attendance_bloc/attendance_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; + import 'package:rtu_mirea_app/presentation/bloc/employee_bloc/employee_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/map_cubit/map_cubit.dart'; import 'package:rtu_mirea_app/presentation/bloc/news_bloc/news_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/profile_bloc/profile_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart'; + import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/stories_bloc/stories_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/update_info_bloc/update_info_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:intl/intl_standalone.dart'; @@ -60,6 +64,13 @@ Future main() async { await FirebaseAnalytics.instance.logAppOpen(); if (kDebugMode) { + // Force disable Crashlytics collection while doing every day development + await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false); + + // Clear Secure Storage + var secureStorage = getIt(); + await secureStorage.deleteAll(); + // Clear local dota var prefs = getIt(); await prefs.clear(); @@ -111,9 +122,11 @@ class App extends StatelessWidget { BlocProvider( create: (context) => getIt()..add(AboutAppGetMembers())), - BlocProvider( - create: (context) => getIt()..add(AuthLogInFromCache())), - BlocProvider(create: (context) => getIt()), + BlocProvider( + create: (context) => + getIt()..add(const UserEvent.started()), + lazy: false, + ), BlocProvider( create: (context) => getIt()), BlocProvider(create: (context) => getIt()), @@ -126,6 +139,16 @@ class App extends StatelessWidget { create: (_) => getIt(), lazy: false, // We need to init it as soon as possible ), + BlocProvider( + create: (_) => getIt() + ..add( + const NfcPassEvent.fetchNfcCode(), + ), + lazy: false, + ), + BlocProvider( + create: (_) => getIt(), + ), ], child: Consumer( builder: (BuildContext context, AppNotifier value, Widget? child) { diff --git a/lib/presentation/bloc/announces_bloc/announces_bloc.dart b/lib/presentation/bloc/announces_bloc/announces_bloc.dart index fcfaee28..f45d08fb 100644 --- a/lib/presentation/bloc/announces_bloc/announces_bloc.dart +++ b/lib/presentation/bloc/announces_bloc/announces_bloc.dart @@ -19,7 +19,7 @@ class AnnouncesBloc extends Bloc { ) async { emit(AnnouncesLoading()); - final announces = await getAnnounces(GetAnnouncesParams(event.token)); + final announces = await getAnnounces(); announces.fold((failure) => emit(AnnouncesLoadError()), (r) => emit(AnnouncesLoaded(announces: r))); diff --git a/lib/presentation/bloc/announces_bloc/announces_event.dart b/lib/presentation/bloc/announces_bloc/announces_event.dart index 0ae481c6..25697cff 100644 --- a/lib/presentation/bloc/announces_bloc/announces_event.dart +++ b/lib/presentation/bloc/announces_bloc/announces_event.dart @@ -8,12 +8,8 @@ abstract class AnnouncesEvent extends Equatable { } class LoadAnnounces extends AnnouncesEvent { - final String token; - - const LoadAnnounces({ - required this.token, - }); + const LoadAnnounces(); @override - List get props => [token]; + List get props => []; } diff --git a/lib/presentation/bloc/attendance_bloc/attendance_bloc.dart b/lib/presentation/bloc/attendance_bloc/attendance_bloc.dart index e3c0d080..d9fcaaed 100644 --- a/lib/presentation/bloc/attendance_bloc/attendance_bloc.dart +++ b/lib/presentation/bloc/attendance_bloc/attendance_bloc.dart @@ -40,7 +40,7 @@ class AttendanceBloc extends Bloc { emit(AttendanceLoading()); final announces = await getAttendance( - GetAttendanceParams(event.token, event.startDate, event.endDate)); + GetAttendanceParams(event.startDate, event.endDate)); announces.fold((failure) => emit(AttendanceLoadError()), (result) { emit(AttendanceLoaded( diff --git a/lib/presentation/bloc/attendance_bloc/attendance_event.dart b/lib/presentation/bloc/attendance_bloc/attendance_event.dart index fdbd1783..f9d39e9b 100644 --- a/lib/presentation/bloc/attendance_bloc/attendance_event.dart +++ b/lib/presentation/bloc/attendance_bloc/attendance_event.dart @@ -8,16 +8,14 @@ abstract class AttendanceEvent extends Equatable { } class LoadAttendance extends AttendanceEvent { - final String token; final String startDate; final String endDate; const LoadAttendance({ - required this.token, required this.startDate, required this.endDate, }); @override - List get props => [token, startDate, endDate]; + List get props => [startDate, endDate]; } diff --git a/lib/presentation/bloc/auth_bloc/auth_bloc.dart b/lib/presentation/bloc/auth_bloc/auth_bloc.dart deleted file mode 100644 index e620d90c..00000000 --- a/lib/presentation/bloc/auth_bloc/auth_bloc.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:rtu_mirea_app/domain/usecases/get_auth_token.dart'; -import 'package:rtu_mirea_app/domain/usecases/get_user_data.dart'; -import 'package:rtu_mirea_app/domain/usecases/log_in.dart'; -import 'package:rtu_mirea_app/domain/usecases/log_out.dart'; - -part 'auth_event.dart'; -part 'auth_state.dart'; - -class AuthBloc extends Bloc { - final LogIn logIn; - final LogOut logOut; - final GetUserData getUserData; - final GetAuthToken getAuthToken; - - AuthBloc({ - required this.getAuthToken, - required this.logIn, - required this.logOut, - required this.getUserData, - }) : super(AuthUnknown()) { - on(_onAuthLogIn); - on(_onAuthLogInFromCache); - on(_onAuthLogOut); - } - - void _onAuthLogInFromCache( - AuthLogInFromCache event, - Emitter emit, - ) async { - final res = await getAuthToken(); - res.fold((failure) => emit(const LogInError(cause: '')), - (token) => emit(LogInSuccess(token: token))); - } - - void _onAuthLogOut( - AuthLogOut event, - Emitter emit, - ) async { - final res = await logOut(); - res.fold((failure) => null, (r) => emit(AuthUnauthorized())); - } - - void _onAuthLogIn( - AuthLogInEvent event, - Emitter emit, - ) async { - if (event.login.length < 4 || event.password.length < 4) { - return emit(const LogInError(cause: "Введёт неверный логин или пароль")); - } - - final res = await logIn(LogInParams(event.login, event.password)); - res.fold( - (failure) => emit(LogInError( - cause: failure.cause ?? - "Ошибка при авторизации. Повторите попытку позже")), - (res) { - emit(LogInSuccess(token: res)); - FirebaseAnalytics.instance.logLogin(); - }, - ); - } -} diff --git a/lib/presentation/bloc/auth_bloc/auth_event.dart b/lib/presentation/bloc/auth_bloc/auth_event.dart deleted file mode 100644 index 026bd62d..00000000 --- a/lib/presentation/bloc/auth_bloc/auth_event.dart +++ /dev/null @@ -1,22 +0,0 @@ -part of 'auth_bloc.dart'; - -abstract class AuthEvent extends Equatable { - const AuthEvent(); - - @override - List get props => []; -} - -class AuthLogInEvent extends AuthEvent { - final String login; - final String password; - - const AuthLogInEvent({required this.login, required this.password}); - - @override - List get props => [login, password]; -} - -class AuthLogInFromCache extends AuthEvent {} - -class AuthLogOut extends AuthEvent {} diff --git a/lib/presentation/bloc/auth_bloc/auth_state.dart b/lib/presentation/bloc/auth_bloc/auth_state.dart deleted file mode 100644 index c921bcdf..00000000 --- a/lib/presentation/bloc/auth_bloc/auth_state.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'auth_bloc.dart'; - -abstract class AuthState extends Equatable { - const AuthState(); - - @override - List get props => []; -} - -class AuthUnknown extends AuthState {} - -class AuthUnauthorized extends AuthState {} - -class LogInError extends AuthState { - final String cause; - - const LogInError({required this.cause}); - - @override - List get props => [cause]; -} - -class LogInSuccess extends AuthState { - final String token; - - const LogInSuccess({required this.token}); - - @override - List get props => [token]; -} diff --git a/lib/presentation/bloc/employee_bloc/employee_bloc.dart b/lib/presentation/bloc/employee_bloc/employee_bloc.dart index b6a978ae..1fb5ca01 100644 --- a/lib/presentation/bloc/employee_bloc/employee_bloc.dart +++ b/lib/presentation/bloc/employee_bloc/employee_bloc.dart @@ -20,8 +20,7 @@ class EmployeeBloc extends Bloc { if (event.name.length > 2) { emit(EmployeeLoading()); - final employees = - await getEmployees(GetEmployeesParams(event.token, event.name)); + final employees = await getEmployees(GetEmployeesParams(event.name)); employees.fold((failure) => emit(EmployeeLoadError()), (result) { if (result.isEmpty) { diff --git a/lib/presentation/bloc/employee_bloc/employee_event.dart b/lib/presentation/bloc/employee_bloc/employee_event.dart index a58bb823..fb64a74e 100644 --- a/lib/presentation/bloc/employee_bloc/employee_event.dart +++ b/lib/presentation/bloc/employee_bloc/employee_event.dart @@ -9,10 +9,9 @@ abstract class EmployeeEvent extends Equatable { class LoadEmployees extends EmployeeEvent { final String name; - final String token; - const LoadEmployees({required this.token, required this.name}); + const LoadEmployees({required this.name}); @override - List get props => [name, token]; + List get props => [name]; } diff --git a/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.dart b/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.dart new file mode 100644 index 00000000..02fea498 --- /dev/null +++ b/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.dart @@ -0,0 +1,35 @@ +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rtu_mirea_app/domain/usecases/send_nfc_not_exist_feedback.dart'; + +part 'nfc_feedback_event.dart'; +part 'nfc_feedback_state.dart'; +part 'nfc_feedback_bloc.freezed.dart'; + +class NfcFeedbackBloc extends Bloc { + SendNfcNotExistFeedback sendNfcNotExistFeedback; + + NfcFeedbackBloc({required this.sendNfcNotExistFeedback}) + : super(const _Initial()) { + on((event, emit) { + event.map( + started: (e) => emit(const _Initial()), + sendFeedback: (e) async { + emit(const _Loading()); + final result = await sendNfcNotExistFeedback( + SendNfcNotExistFeedbackParams( + e.fullName, + e.group, + e.personalNumber, + e.studentId, + ), + ); + result.fold( + (failure) => emit(_Failure(failure.cause ?? 'Неизвестная ошибка')), + (success) => emit(const _Success()), + ); + }, + ); + }); + } +} diff --git a/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.freezed.dart b/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.freezed.dart new file mode 100644 index 00000000..45e0d059 --- /dev/null +++ b/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.freezed.dart @@ -0,0 +1,922 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'nfc_feedback_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$NfcFeedbackEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function(String fullName, String group, + String personalNumber, String studentId) + sendFeedback, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function(String fullName, String group, String personalNumber, + String studentId)? + sendFeedback, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function(String fullName, String group, String personalNumber, + String studentId)? + sendFeedback, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_SendFeedback value) sendFeedback, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_SendFeedback value)? sendFeedback, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_SendFeedback value)? sendFeedback, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NfcFeedbackEventCopyWith<$Res> { + factory $NfcFeedbackEventCopyWith( + NfcFeedbackEvent value, $Res Function(NfcFeedbackEvent) then) = + _$NfcFeedbackEventCopyWithImpl<$Res, NfcFeedbackEvent>; +} + +/// @nodoc +class _$NfcFeedbackEventCopyWithImpl<$Res, $Val extends NfcFeedbackEvent> + implements $NfcFeedbackEventCopyWith<$Res> { + _$NfcFeedbackEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_StartedCopyWith<$Res> { + factory _$$_StartedCopyWith( + _$_Started value, $Res Function(_$_Started) then) = + __$$_StartedCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_StartedCopyWithImpl<$Res> + extends _$NfcFeedbackEventCopyWithImpl<$Res, _$_Started> + implements _$$_StartedCopyWith<$Res> { + __$$_StartedCopyWithImpl(_$_Started _value, $Res Function(_$_Started) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Started implements _Started { + const _$_Started(); + + @override + String toString() { + return 'NfcFeedbackEvent.started()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Started); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function(String fullName, String group, + String personalNumber, String studentId) + sendFeedback, + }) { + return started(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function(String fullName, String group, String personalNumber, + String studentId)? + sendFeedback, + }) { + return started?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function(String fullName, String group, String personalNumber, + String studentId)? + sendFeedback, + required TResult orElse(), + }) { + if (started != null) { + return started(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_SendFeedback value) sendFeedback, + }) { + return started(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_SendFeedback value)? sendFeedback, + }) { + return started?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_SendFeedback value)? sendFeedback, + required TResult orElse(), + }) { + if (started != null) { + return started(this); + } + return orElse(); + } +} + +abstract class _Started implements NfcFeedbackEvent { + const factory _Started() = _$_Started; +} + +/// @nodoc +abstract class _$$_SendFeedbackCopyWith<$Res> { + factory _$$_SendFeedbackCopyWith( + _$_SendFeedback value, $Res Function(_$_SendFeedback) then) = + __$$_SendFeedbackCopyWithImpl<$Res>; + @useResult + $Res call( + {String fullName, String group, String personalNumber, String studentId}); +} + +/// @nodoc +class __$$_SendFeedbackCopyWithImpl<$Res> + extends _$NfcFeedbackEventCopyWithImpl<$Res, _$_SendFeedback> + implements _$$_SendFeedbackCopyWith<$Res> { + __$$_SendFeedbackCopyWithImpl( + _$_SendFeedback _value, $Res Function(_$_SendFeedback) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? fullName = null, + Object? group = null, + Object? personalNumber = null, + Object? studentId = null, + }) { + return _then(_$_SendFeedback( + fullName: null == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String, + group: null == group + ? _value.group + : group // ignore: cast_nullable_to_non_nullable + as String, + personalNumber: null == personalNumber + ? _value.personalNumber + : personalNumber // ignore: cast_nullable_to_non_nullable + as String, + studentId: null == studentId + ? _value.studentId + : studentId // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_SendFeedback implements _SendFeedback { + const _$_SendFeedback( + {required this.fullName, + required this.group, + required this.personalNumber, + required this.studentId}); + + @override + final String fullName; + @override + final String group; + @override + final String personalNumber; + @override + final String studentId; + + @override + String toString() { + return 'NfcFeedbackEvent.sendFeedback(fullName: $fullName, group: $group, personalNumber: $personalNumber, studentId: $studentId)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_SendFeedback && + (identical(other.fullName, fullName) || + other.fullName == fullName) && + (identical(other.group, group) || other.group == group) && + (identical(other.personalNumber, personalNumber) || + other.personalNumber == personalNumber) && + (identical(other.studentId, studentId) || + other.studentId == studentId)); + } + + @override + int get hashCode => + Object.hash(runtimeType, fullName, group, personalNumber, studentId); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_SendFeedbackCopyWith<_$_SendFeedback> get copyWith => + __$$_SendFeedbackCopyWithImpl<_$_SendFeedback>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function(String fullName, String group, + String personalNumber, String studentId) + sendFeedback, + }) { + return sendFeedback(fullName, group, personalNumber, studentId); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function(String fullName, String group, String personalNumber, + String studentId)? + sendFeedback, + }) { + return sendFeedback?.call(fullName, group, personalNumber, studentId); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function(String fullName, String group, String personalNumber, + String studentId)? + sendFeedback, + required TResult orElse(), + }) { + if (sendFeedback != null) { + return sendFeedback(fullName, group, personalNumber, studentId); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_SendFeedback value) sendFeedback, + }) { + return sendFeedback(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_SendFeedback value)? sendFeedback, + }) { + return sendFeedback?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_SendFeedback value)? sendFeedback, + required TResult orElse(), + }) { + if (sendFeedback != null) { + return sendFeedback(this); + } + return orElse(); + } +} + +abstract class _SendFeedback implements NfcFeedbackEvent { + const factory _SendFeedback( + {required final String fullName, + required final String group, + required final String personalNumber, + required final String studentId}) = _$_SendFeedback; + + String get fullName; + String get group; + String get personalNumber; + String get studentId; + @JsonKey(ignore: true) + _$$_SendFeedbackCopyWith<_$_SendFeedback> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$NfcFeedbackState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NfcFeedbackStateCopyWith<$Res> { + factory $NfcFeedbackStateCopyWith( + NfcFeedbackState value, $Res Function(NfcFeedbackState) then) = + _$NfcFeedbackStateCopyWithImpl<$Res, NfcFeedbackState>; +} + +/// @nodoc +class _$NfcFeedbackStateCopyWithImpl<$Res, $Val extends NfcFeedbackState> + implements $NfcFeedbackStateCopyWith<$Res> { + _$NfcFeedbackStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_InitialCopyWith<$Res> { + factory _$$_InitialCopyWith( + _$_Initial value, $Res Function(_$_Initial) then) = + __$$_InitialCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_InitialCopyWithImpl<$Res> + extends _$NfcFeedbackStateCopyWithImpl<$Res, _$_Initial> + implements _$$_InitialCopyWith<$Res> { + __$$_InitialCopyWithImpl(_$_Initial _value, $Res Function(_$_Initial) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Initial implements _Initial { + const _$_Initial(); + + @override + String toString() { + return 'NfcFeedbackState.initial()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Initial); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements NfcFeedbackState { + const factory _Initial() = _$_Initial; +} + +/// @nodoc +abstract class _$$_LoadingCopyWith<$Res> { + factory _$$_LoadingCopyWith( + _$_Loading value, $Res Function(_$_Loading) then) = + __$$_LoadingCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_LoadingCopyWithImpl<$Res> + extends _$NfcFeedbackStateCopyWithImpl<$Res, _$_Loading> + implements _$$_LoadingCopyWith<$Res> { + __$$_LoadingCopyWithImpl(_$_Loading _value, $Res Function(_$_Loading) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Loading implements _Loading { + const _$_Loading(); + + @override + String toString() { + return 'NfcFeedbackState.loading()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Loading); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements NfcFeedbackState { + const factory _Loading() = _$_Loading; +} + +/// @nodoc +abstract class _$$_SuccessCopyWith<$Res> { + factory _$$_SuccessCopyWith( + _$_Success value, $Res Function(_$_Success) then) = + __$$_SuccessCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_SuccessCopyWithImpl<$Res> + extends _$NfcFeedbackStateCopyWithImpl<$Res, _$_Success> + implements _$$_SuccessCopyWith<$Res> { + __$$_SuccessCopyWithImpl(_$_Success _value, $Res Function(_$_Success) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Success implements _Success { + const _$_Success(); + + @override + String toString() { + return 'NfcFeedbackState.success()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Success); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return success(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return success?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _Success implements NfcFeedbackState { + const factory _Success() = _$_Success; +} + +/// @nodoc +abstract class _$$_FailureCopyWith<$Res> { + factory _$$_FailureCopyWith( + _$_Failure value, $Res Function(_$_Failure) then) = + __$$_FailureCopyWithImpl<$Res>; + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$_FailureCopyWithImpl<$Res> + extends _$NfcFeedbackStateCopyWithImpl<$Res, _$_Failure> + implements _$$_FailureCopyWith<$Res> { + __$$_FailureCopyWithImpl(_$_Failure _value, $Res Function(_$_Failure) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + }) { + return _then(_$_Failure( + null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_Failure implements _Failure { + const _$_Failure(this.message); + + @override + final String message; + + @override + String toString() { + return 'NfcFeedbackState.failure(message: $message)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Failure && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_FailureCopyWith<_$_Failure> get copyWith => + __$$_FailureCopyWithImpl<_$_Failure>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function() success, + required TResult Function(String message) failure, + }) { + return failure(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function()? success, + TResult? Function(String message)? failure, + }) { + return failure?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function()? success, + TResult Function(String message)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Success value) success, + required TResult Function(_Failure value) failure, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Success value)? success, + TResult? Function(_Failure value)? failure, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Success value)? success, + TResult Function(_Failure value)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _Failure implements NfcFeedbackState { + const factory _Failure(final String message) = _$_Failure; + + String get message; + @JsonKey(ignore: true) + _$$_FailureCopyWith<_$_Failure> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_event.dart b/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_event.dart new file mode 100644 index 00000000..7c2d2536 --- /dev/null +++ b/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_event.dart @@ -0,0 +1,12 @@ +part of 'nfc_feedback_bloc.dart'; + +@freezed +class NfcFeedbackEvent with _$NfcFeedbackEvent { + const factory NfcFeedbackEvent.started() = _Started; + const factory NfcFeedbackEvent.sendFeedback({ + required String fullName, + required String group, + required String personalNumber, + required String studentId, + }) = _SendFeedback; +} diff --git a/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_state.dart b/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_state.dart new file mode 100644 index 00000000..afac69a4 --- /dev/null +++ b/lib/presentation/bloc/nfc_feedback_bloc/nfc_feedback_state.dart @@ -0,0 +1,9 @@ +part of 'nfc_feedback_bloc.dart'; + +@freezed +class NfcFeedbackState with _$NfcFeedbackState { + const factory NfcFeedbackState.initial() = _Initial; + const factory NfcFeedbackState.loading() = _Loading; + const factory NfcFeedbackState.success() = _Success; + const factory NfcFeedbackState.failure(String message) = _Failure; +} diff --git a/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart new file mode 100644 index 00000000..da0745c0 --- /dev/null +++ b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart @@ -0,0 +1,169 @@ +import 'package:bloc/bloc.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_nfc_kit/flutter_nfc_kit.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rtu_mirea_app/common/errors/failures.dart'; +import 'package:rtu_mirea_app/domain/entities/nfc_pass.dart'; +import 'package:rtu_mirea_app/domain/usecases/connect_nfc_pass.dart'; +import 'package:rtu_mirea_app/domain/usecases/fetch_nfc_code.dart'; +import 'package:rtu_mirea_app/domain/usecases/get_auth_token.dart'; +import 'package:rtu_mirea_app/domain/usecases/get_nfc_passes.dart'; +import 'package:rtu_mirea_app/domain/usecases/get_user_data.dart'; + +part 'nfc_pass_event.dart'; +part 'nfc_pass_state.dart'; +part 'nfc_pass_bloc.freezed.dart'; + +class NfcPassBloc extends Bloc { + /// This variable is used to prevent multiple fetches of NFC code. + static bool isNfcFetched = false; + + final GetNfcPasses getNfcPasses; + final ConnectNfcPass connectNfcPass; + final GetAuthToken getAuthToken; + final DeviceInfoPlugin deviceInfo; + final FetchNfcCode fetchNfcCode; + final GetUserData getUserData; + + NfcPassBloc({ + required this.getNfcPasses, + required this.connectNfcPass, + required this.deviceInfo, + required this.getAuthToken, + required this.getUserData, + required this.fetchNfcCode, + }) : super(const _Initial()) { + on<_Started>(_onStarted); + on<_GetNfcPasses>(_onGetNfcPasses); + on<_ConnectNfcPass>(_onConnectNfcPass); + on<_FetchNfcCode>(_onFetchNfcCode); + } + + void _onGetNfcPasses( + _GetNfcPasses event, + Emitter emit, + ) async { + final failureOrNfcPasses = await getNfcPasses( + GetNfcPassesParams( + event.code, + event.studentId, + event.deviceId, + ), + ); + failureOrNfcPasses.fold( + (failure) => emit(NfcPassState.error(failure.cause ?? "Ошибка")), + (nfcPasses) => emit(NfcPassState.loaded(nfcPasses)), + ); + } + + void _onConnectNfcPass( + _ConnectNfcPass event, + Emitter emit, + ) async { + emit(const NfcPassState.loading()); + + final failureOrNfcPasses = await connectNfcPass( + ConnectNfcPassParams( + event.code, + event.studentId, + event.deviceId, + event.deviceName, + ), + ); + + failureOrNfcPasses.fold( + (failure) => emit(NfcPassState.error(failure.cause ?? + "Не удалось подключить устройство. Попробуйте еще раз.")), + (nfcPasses) { + isNfcFetched = false; + add(_GetNfcPasses( + event.code, + event.studentId, + event.deviceId, + )); + }, + ); + } + + void _onStarted( + _Started event, + Emitter emit, + ) async { + emit(const NfcPassState.loading()); + if (kDebugMode) { + emit(const NfcPassState.initial()); + return; + } + final availability = await FlutterNfcKit.nfcAvailability; + + if (availability == NFCAvailability.not_supported) { + emit(const NfcPassState.nfcNotSupported()); + } else if (availability == NFCAvailability.disabled) { + emit(const NfcPassState.nfcDisabled()); + } else { + emit(const NfcPassState.initial()); + } + } + + void _onFetchNfcCode( + _FetchNfcCode event, + Emitter emit, + ) async { + // Token is not empty if user is logged in + bool isAunthenticated = false; + final tokenRes = await getAuthToken(); + tokenRes.fold( + (failure) => isAunthenticated = false, + (token) => isAunthenticated = true, + ); + + if (!isAunthenticated) { + return; + } + + final userDataRes = await getUserData(); + String? studentId; + String? code; + userDataRes.fold( + (failure) => studentId = null, + (userData) { + studentId = userData.studentId; + code = userData.code; + }, + ); + + if (studentId == null || code == null) { + return; + } + + final deviceInfoRes = await deviceInfo.androidInfo; + final deviceId = deviceInfoRes.id; + final deviceName = deviceInfoRes.model; + + final failureOrNfcCode = await fetchNfcCode( + FetchNfcCodeParams( + code!, + studentId!, + deviceId, + deviceName, + ), + ); + + failureOrNfcCode.fold((failure) { + if (failure is NfcStaffnodeNotExistFailure) { + emit(const NfcPassState.nfcNotExist()); + } else { + emit(NfcPassState.error(failure.cause ?? "Неизвестная ошибка")); + } + }, (nfcCode) { + add(_GetNfcPasses( + code!, + studentId!, + deviceId, + )); + + isNfcFetched = true; + }); + } +} diff --git a/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.freezed.dart b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.freezed.dart new file mode 100644 index 00000000..67c9b94b --- /dev/null +++ b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.freezed.dart @@ -0,0 +1,1865 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'nfc_pass_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$NfcPassEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function( + String code, String studentId, String deviceId, String deviceName) + connectNfcPass, + required TResult Function(String code, String studentId, String deviceId) + getNfcPasses, + required TResult Function() fetchNfcCode, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult? Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult? Function()? fetchNfcCode, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult Function()? fetchNfcCode, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_ConnectNfcPass value) connectNfcPass, + required TResult Function(_GetNfcPasses value) getNfcPasses, + required TResult Function(_FetchNfcCode value) fetchNfcCode, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_ConnectNfcPass value)? connectNfcPass, + TResult? Function(_GetNfcPasses value)? getNfcPasses, + TResult? Function(_FetchNfcCode value)? fetchNfcCode, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_ConnectNfcPass value)? connectNfcPass, + TResult Function(_GetNfcPasses value)? getNfcPasses, + TResult Function(_FetchNfcCode value)? fetchNfcCode, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NfcPassEventCopyWith<$Res> { + factory $NfcPassEventCopyWith( + NfcPassEvent value, $Res Function(NfcPassEvent) then) = + _$NfcPassEventCopyWithImpl<$Res, NfcPassEvent>; +} + +/// @nodoc +class _$NfcPassEventCopyWithImpl<$Res, $Val extends NfcPassEvent> + implements $NfcPassEventCopyWith<$Res> { + _$NfcPassEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_StartedCopyWith<$Res> { + factory _$$_StartedCopyWith( + _$_Started value, $Res Function(_$_Started) then) = + __$$_StartedCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_StartedCopyWithImpl<$Res> + extends _$NfcPassEventCopyWithImpl<$Res, _$_Started> + implements _$$_StartedCopyWith<$Res> { + __$$_StartedCopyWithImpl(_$_Started _value, $Res Function(_$_Started) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Started with DiagnosticableTreeMixin implements _Started { + const _$_Started(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassEvent.started()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('type', 'NfcPassEvent.started')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Started); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function( + String code, String studentId, String deviceId, String deviceName) + connectNfcPass, + required TResult Function(String code, String studentId, String deviceId) + getNfcPasses, + required TResult Function() fetchNfcCode, + }) { + return started(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult? Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult? Function()? fetchNfcCode, + }) { + return started?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult Function()? fetchNfcCode, + required TResult orElse(), + }) { + if (started != null) { + return started(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_ConnectNfcPass value) connectNfcPass, + required TResult Function(_GetNfcPasses value) getNfcPasses, + required TResult Function(_FetchNfcCode value) fetchNfcCode, + }) { + return started(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_ConnectNfcPass value)? connectNfcPass, + TResult? Function(_GetNfcPasses value)? getNfcPasses, + TResult? Function(_FetchNfcCode value)? fetchNfcCode, + }) { + return started?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_ConnectNfcPass value)? connectNfcPass, + TResult Function(_GetNfcPasses value)? getNfcPasses, + TResult Function(_FetchNfcCode value)? fetchNfcCode, + required TResult orElse(), + }) { + if (started != null) { + return started(this); + } + return orElse(); + } +} + +abstract class _Started implements NfcPassEvent { + const factory _Started() = _$_Started; +} + +/// @nodoc +abstract class _$$_ConnectNfcPassCopyWith<$Res> { + factory _$$_ConnectNfcPassCopyWith( + _$_ConnectNfcPass value, $Res Function(_$_ConnectNfcPass) then) = + __$$_ConnectNfcPassCopyWithImpl<$Res>; + @useResult + $Res call( + {String code, String studentId, String deviceId, String deviceName}); +} + +/// @nodoc +class __$$_ConnectNfcPassCopyWithImpl<$Res> + extends _$NfcPassEventCopyWithImpl<$Res, _$_ConnectNfcPass> + implements _$$_ConnectNfcPassCopyWith<$Res> { + __$$_ConnectNfcPassCopyWithImpl( + _$_ConnectNfcPass _value, $Res Function(_$_ConnectNfcPass) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? code = null, + Object? studentId = null, + Object? deviceId = null, + Object? deviceName = null, + }) { + return _then(_$_ConnectNfcPass( + null == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as String, + null == studentId + ? _value.studentId + : studentId // ignore: cast_nullable_to_non_nullable + as String, + null == deviceId + ? _value.deviceId + : deviceId // ignore: cast_nullable_to_non_nullable + as String, + null == deviceName + ? _value.deviceName + : deviceName // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_ConnectNfcPass + with DiagnosticableTreeMixin + implements _ConnectNfcPass { + const _$_ConnectNfcPass( + this.code, this.studentId, this.deviceId, this.deviceName); + + @override + final String code; + @override + final String studentId; + @override + final String deviceId; + @override + final String deviceName; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassEvent.connectNfcPass(code: $code, studentId: $studentId, deviceId: $deviceId, deviceName: $deviceName)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'NfcPassEvent.connectNfcPass')) + ..add(DiagnosticsProperty('code', code)) + ..add(DiagnosticsProperty('studentId', studentId)) + ..add(DiagnosticsProperty('deviceId', deviceId)) + ..add(DiagnosticsProperty('deviceName', deviceName)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_ConnectNfcPass && + (identical(other.code, code) || other.code == code) && + (identical(other.studentId, studentId) || + other.studentId == studentId) && + (identical(other.deviceId, deviceId) || + other.deviceId == deviceId) && + (identical(other.deviceName, deviceName) || + other.deviceName == deviceName)); + } + + @override + int get hashCode => + Object.hash(runtimeType, code, studentId, deviceId, deviceName); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ConnectNfcPassCopyWith<_$_ConnectNfcPass> get copyWith => + __$$_ConnectNfcPassCopyWithImpl<_$_ConnectNfcPass>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function( + String code, String studentId, String deviceId, String deviceName) + connectNfcPass, + required TResult Function(String code, String studentId, String deviceId) + getNfcPasses, + required TResult Function() fetchNfcCode, + }) { + return connectNfcPass(code, studentId, deviceId, deviceName); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult? Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult? Function()? fetchNfcCode, + }) { + return connectNfcPass?.call(code, studentId, deviceId, deviceName); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult Function()? fetchNfcCode, + required TResult orElse(), + }) { + if (connectNfcPass != null) { + return connectNfcPass(code, studentId, deviceId, deviceName); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_ConnectNfcPass value) connectNfcPass, + required TResult Function(_GetNfcPasses value) getNfcPasses, + required TResult Function(_FetchNfcCode value) fetchNfcCode, + }) { + return connectNfcPass(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_ConnectNfcPass value)? connectNfcPass, + TResult? Function(_GetNfcPasses value)? getNfcPasses, + TResult? Function(_FetchNfcCode value)? fetchNfcCode, + }) { + return connectNfcPass?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_ConnectNfcPass value)? connectNfcPass, + TResult Function(_GetNfcPasses value)? getNfcPasses, + TResult Function(_FetchNfcCode value)? fetchNfcCode, + required TResult orElse(), + }) { + if (connectNfcPass != null) { + return connectNfcPass(this); + } + return orElse(); + } +} + +abstract class _ConnectNfcPass implements NfcPassEvent { + const factory _ConnectNfcPass(final String code, final String studentId, + final String deviceId, final String deviceName) = _$_ConnectNfcPass; + + String get code; + String get studentId; + String get deviceId; + String get deviceName; + @JsonKey(ignore: true) + _$$_ConnectNfcPassCopyWith<_$_ConnectNfcPass> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_GetNfcPassesCopyWith<$Res> { + factory _$$_GetNfcPassesCopyWith( + _$_GetNfcPasses value, $Res Function(_$_GetNfcPasses) then) = + __$$_GetNfcPassesCopyWithImpl<$Res>; + @useResult + $Res call({String code, String studentId, String deviceId}); +} + +/// @nodoc +class __$$_GetNfcPassesCopyWithImpl<$Res> + extends _$NfcPassEventCopyWithImpl<$Res, _$_GetNfcPasses> + implements _$$_GetNfcPassesCopyWith<$Res> { + __$$_GetNfcPassesCopyWithImpl( + _$_GetNfcPasses _value, $Res Function(_$_GetNfcPasses) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? code = null, + Object? studentId = null, + Object? deviceId = null, + }) { + return _then(_$_GetNfcPasses( + null == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as String, + null == studentId + ? _value.studentId + : studentId // ignore: cast_nullable_to_non_nullable + as String, + null == deviceId + ? _value.deviceId + : deviceId // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_GetNfcPasses with DiagnosticableTreeMixin implements _GetNfcPasses { + const _$_GetNfcPasses(this.code, this.studentId, this.deviceId); + + @override + final String code; + @override + final String studentId; + @override + final String deviceId; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassEvent.getNfcPasses(code: $code, studentId: $studentId, deviceId: $deviceId)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'NfcPassEvent.getNfcPasses')) + ..add(DiagnosticsProperty('code', code)) + ..add(DiagnosticsProperty('studentId', studentId)) + ..add(DiagnosticsProperty('deviceId', deviceId)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_GetNfcPasses && + (identical(other.code, code) || other.code == code) && + (identical(other.studentId, studentId) || + other.studentId == studentId) && + (identical(other.deviceId, deviceId) || + other.deviceId == deviceId)); + } + + @override + int get hashCode => Object.hash(runtimeType, code, studentId, deviceId); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_GetNfcPassesCopyWith<_$_GetNfcPasses> get copyWith => + __$$_GetNfcPassesCopyWithImpl<_$_GetNfcPasses>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function( + String code, String studentId, String deviceId, String deviceName) + connectNfcPass, + required TResult Function(String code, String studentId, String deviceId) + getNfcPasses, + required TResult Function() fetchNfcCode, + }) { + return getNfcPasses(code, studentId, deviceId); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult? Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult? Function()? fetchNfcCode, + }) { + return getNfcPasses?.call(code, studentId, deviceId); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult Function()? fetchNfcCode, + required TResult orElse(), + }) { + if (getNfcPasses != null) { + return getNfcPasses(code, studentId, deviceId); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_ConnectNfcPass value) connectNfcPass, + required TResult Function(_GetNfcPasses value) getNfcPasses, + required TResult Function(_FetchNfcCode value) fetchNfcCode, + }) { + return getNfcPasses(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_ConnectNfcPass value)? connectNfcPass, + TResult? Function(_GetNfcPasses value)? getNfcPasses, + TResult? Function(_FetchNfcCode value)? fetchNfcCode, + }) { + return getNfcPasses?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_ConnectNfcPass value)? connectNfcPass, + TResult Function(_GetNfcPasses value)? getNfcPasses, + TResult Function(_FetchNfcCode value)? fetchNfcCode, + required TResult orElse(), + }) { + if (getNfcPasses != null) { + return getNfcPasses(this); + } + return orElse(); + } +} + +abstract class _GetNfcPasses implements NfcPassEvent { + const factory _GetNfcPasses( + final String code, final String studentId, final String deviceId) = + _$_GetNfcPasses; + + String get code; + String get studentId; + String get deviceId; + @JsonKey(ignore: true) + _$$_GetNfcPassesCopyWith<_$_GetNfcPasses> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_FetchNfcCodeCopyWith<$Res> { + factory _$$_FetchNfcCodeCopyWith( + _$_FetchNfcCode value, $Res Function(_$_FetchNfcCode) then) = + __$$_FetchNfcCodeCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_FetchNfcCodeCopyWithImpl<$Res> + extends _$NfcPassEventCopyWithImpl<$Res, _$_FetchNfcCode> + implements _$$_FetchNfcCodeCopyWith<$Res> { + __$$_FetchNfcCodeCopyWithImpl( + _$_FetchNfcCode _value, $Res Function(_$_FetchNfcCode) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_FetchNfcCode with DiagnosticableTreeMixin implements _FetchNfcCode { + const _$_FetchNfcCode(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassEvent.fetchNfcCode()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('type', 'NfcPassEvent.fetchNfcCode')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_FetchNfcCode); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function( + String code, String studentId, String deviceId, String deviceName) + connectNfcPass, + required TResult Function(String code, String studentId, String deviceId) + getNfcPasses, + required TResult Function() fetchNfcCode, + }) { + return fetchNfcCode(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult? Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult? Function()? fetchNfcCode, + }) { + return fetchNfcCode?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function( + String code, String studentId, String deviceId, String deviceName)? + connectNfcPass, + TResult Function(String code, String studentId, String deviceId)? + getNfcPasses, + TResult Function()? fetchNfcCode, + required TResult orElse(), + }) { + if (fetchNfcCode != null) { + return fetchNfcCode(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_ConnectNfcPass value) connectNfcPass, + required TResult Function(_GetNfcPasses value) getNfcPasses, + required TResult Function(_FetchNfcCode value) fetchNfcCode, + }) { + return fetchNfcCode(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_ConnectNfcPass value)? connectNfcPass, + TResult? Function(_GetNfcPasses value)? getNfcPasses, + TResult? Function(_FetchNfcCode value)? fetchNfcCode, + }) { + return fetchNfcCode?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_ConnectNfcPass value)? connectNfcPass, + TResult Function(_GetNfcPasses value)? getNfcPasses, + TResult Function(_FetchNfcCode value)? fetchNfcCode, + required TResult orElse(), + }) { + if (fetchNfcCode != null) { + return fetchNfcCode(this); + } + return orElse(); + } +} + +abstract class _FetchNfcCode implements NfcPassEvent { + const factory _FetchNfcCode() = _$_FetchNfcCode; +} + +/// @nodoc +mixin _$NfcPassState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List nfcPasses) loaded, + required TResult Function() nfcNotExist, + required TResult Function(String cause) error, + required TResult Function() nfcDisabled, + required TResult Function() nfcNotSupported, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List nfcPasses)? loaded, + TResult? Function()? nfcNotExist, + TResult? Function(String cause)? error, + TResult? Function()? nfcDisabled, + TResult? Function()? nfcNotSupported, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List nfcPasses)? loaded, + TResult Function()? nfcNotExist, + TResult Function(String cause)? error, + TResult Function()? nfcDisabled, + TResult Function()? nfcNotSupported, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_NfcNotExist value) nfcNotExist, + required TResult Function(_Error value) error, + required TResult Function(_NfcDisabled value) nfcDisabled, + required TResult Function(_NfcNotSupported value) nfcNotSupported, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_NfcNotExist value)? nfcNotExist, + TResult? Function(_Error value)? error, + TResult? Function(_NfcDisabled value)? nfcDisabled, + TResult? Function(_NfcNotSupported value)? nfcNotSupported, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_NfcNotExist value)? nfcNotExist, + TResult Function(_Error value)? error, + TResult Function(_NfcDisabled value)? nfcDisabled, + TResult Function(_NfcNotSupported value)? nfcNotSupported, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NfcPassStateCopyWith<$Res> { + factory $NfcPassStateCopyWith( + NfcPassState value, $Res Function(NfcPassState) then) = + _$NfcPassStateCopyWithImpl<$Res, NfcPassState>; +} + +/// @nodoc +class _$NfcPassStateCopyWithImpl<$Res, $Val extends NfcPassState> + implements $NfcPassStateCopyWith<$Res> { + _$NfcPassStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_InitialCopyWith<$Res> { + factory _$$_InitialCopyWith( + _$_Initial value, $Res Function(_$_Initial) then) = + __$$_InitialCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_InitialCopyWithImpl<$Res> + extends _$NfcPassStateCopyWithImpl<$Res, _$_Initial> + implements _$$_InitialCopyWith<$Res> { + __$$_InitialCopyWithImpl(_$_Initial _value, $Res Function(_$_Initial) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Initial with DiagnosticableTreeMixin implements _Initial { + const _$_Initial(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassState.initial()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('type', 'NfcPassState.initial')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Initial); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List nfcPasses) loaded, + required TResult Function() nfcNotExist, + required TResult Function(String cause) error, + required TResult Function() nfcDisabled, + required TResult Function() nfcNotSupported, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List nfcPasses)? loaded, + TResult? Function()? nfcNotExist, + TResult? Function(String cause)? error, + TResult? Function()? nfcDisabled, + TResult? Function()? nfcNotSupported, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List nfcPasses)? loaded, + TResult Function()? nfcNotExist, + TResult Function(String cause)? error, + TResult Function()? nfcDisabled, + TResult Function()? nfcNotSupported, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_NfcNotExist value) nfcNotExist, + required TResult Function(_Error value) error, + required TResult Function(_NfcDisabled value) nfcDisabled, + required TResult Function(_NfcNotSupported value) nfcNotSupported, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_NfcNotExist value)? nfcNotExist, + TResult? Function(_Error value)? error, + TResult? Function(_NfcDisabled value)? nfcDisabled, + TResult? Function(_NfcNotSupported value)? nfcNotSupported, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_NfcNotExist value)? nfcNotExist, + TResult Function(_Error value)? error, + TResult Function(_NfcDisabled value)? nfcDisabled, + TResult Function(_NfcNotSupported value)? nfcNotSupported, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements NfcPassState { + const factory _Initial() = _$_Initial; +} + +/// @nodoc +abstract class _$$_LoadingCopyWith<$Res> { + factory _$$_LoadingCopyWith( + _$_Loading value, $Res Function(_$_Loading) then) = + __$$_LoadingCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_LoadingCopyWithImpl<$Res> + extends _$NfcPassStateCopyWithImpl<$Res, _$_Loading> + implements _$$_LoadingCopyWith<$Res> { + __$$_LoadingCopyWithImpl(_$_Loading _value, $Res Function(_$_Loading) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Loading with DiagnosticableTreeMixin implements _Loading { + const _$_Loading(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassState.loading()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('type', 'NfcPassState.loading')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Loading); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List nfcPasses) loaded, + required TResult Function() nfcNotExist, + required TResult Function(String cause) error, + required TResult Function() nfcDisabled, + required TResult Function() nfcNotSupported, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List nfcPasses)? loaded, + TResult? Function()? nfcNotExist, + TResult? Function(String cause)? error, + TResult? Function()? nfcDisabled, + TResult? Function()? nfcNotSupported, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List nfcPasses)? loaded, + TResult Function()? nfcNotExist, + TResult Function(String cause)? error, + TResult Function()? nfcDisabled, + TResult Function()? nfcNotSupported, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_NfcNotExist value) nfcNotExist, + required TResult Function(_Error value) error, + required TResult Function(_NfcDisabled value) nfcDisabled, + required TResult Function(_NfcNotSupported value) nfcNotSupported, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_NfcNotExist value)? nfcNotExist, + TResult? Function(_Error value)? error, + TResult? Function(_NfcDisabled value)? nfcDisabled, + TResult? Function(_NfcNotSupported value)? nfcNotSupported, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_NfcNotExist value)? nfcNotExist, + TResult Function(_Error value)? error, + TResult Function(_NfcDisabled value)? nfcDisabled, + TResult Function(_NfcNotSupported value)? nfcNotSupported, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements NfcPassState { + const factory _Loading() = _$_Loading; +} + +/// @nodoc +abstract class _$$_LoadedCopyWith<$Res> { + factory _$$_LoadedCopyWith(_$_Loaded value, $Res Function(_$_Loaded) then) = + __$$_LoadedCopyWithImpl<$Res>; + @useResult + $Res call({List nfcPasses}); +} + +/// @nodoc +class __$$_LoadedCopyWithImpl<$Res> + extends _$NfcPassStateCopyWithImpl<$Res, _$_Loaded> + implements _$$_LoadedCopyWith<$Res> { + __$$_LoadedCopyWithImpl(_$_Loaded _value, $Res Function(_$_Loaded) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? nfcPasses = null, + }) { + return _then(_$_Loaded( + null == nfcPasses + ? _value._nfcPasses + : nfcPasses // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$_Loaded with DiagnosticableTreeMixin implements _Loaded { + const _$_Loaded(final List nfcPasses) : _nfcPasses = nfcPasses; + + final List _nfcPasses; + @override + List get nfcPasses { + if (_nfcPasses is EqualUnmodifiableListView) return _nfcPasses; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_nfcPasses); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassState.loaded(nfcPasses: $nfcPasses)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'NfcPassState.loaded')) + ..add(DiagnosticsProperty('nfcPasses', nfcPasses)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Loaded && + const DeepCollectionEquality() + .equals(other._nfcPasses, _nfcPasses)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_nfcPasses)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_LoadedCopyWith<_$_Loaded> get copyWith => + __$$_LoadedCopyWithImpl<_$_Loaded>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List nfcPasses) loaded, + required TResult Function() nfcNotExist, + required TResult Function(String cause) error, + required TResult Function() nfcDisabled, + required TResult Function() nfcNotSupported, + }) { + return loaded(nfcPasses); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List nfcPasses)? loaded, + TResult? Function()? nfcNotExist, + TResult? Function(String cause)? error, + TResult? Function()? nfcDisabled, + TResult? Function()? nfcNotSupported, + }) { + return loaded?.call(nfcPasses); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List nfcPasses)? loaded, + TResult Function()? nfcNotExist, + TResult Function(String cause)? error, + TResult Function()? nfcDisabled, + TResult Function()? nfcNotSupported, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(nfcPasses); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_NfcNotExist value) nfcNotExist, + required TResult Function(_Error value) error, + required TResult Function(_NfcDisabled value) nfcDisabled, + required TResult Function(_NfcNotSupported value) nfcNotSupported, + }) { + return loaded(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_NfcNotExist value)? nfcNotExist, + TResult? Function(_Error value)? error, + TResult? Function(_NfcDisabled value)? nfcDisabled, + TResult? Function(_NfcNotSupported value)? nfcNotSupported, + }) { + return loaded?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_NfcNotExist value)? nfcNotExist, + TResult Function(_Error value)? error, + TResult Function(_NfcDisabled value)? nfcDisabled, + TResult Function(_NfcNotSupported value)? nfcNotSupported, + required TResult orElse(), + }) { + if (loaded != null) { + return loaded(this); + } + return orElse(); + } +} + +abstract class _Loaded implements NfcPassState { + const factory _Loaded(final List nfcPasses) = _$_Loaded; + + List get nfcPasses; + @JsonKey(ignore: true) + _$$_LoadedCopyWith<_$_Loaded> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_NfcNotExistCopyWith<$Res> { + factory _$$_NfcNotExistCopyWith( + _$_NfcNotExist value, $Res Function(_$_NfcNotExist) then) = + __$$_NfcNotExistCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_NfcNotExistCopyWithImpl<$Res> + extends _$NfcPassStateCopyWithImpl<$Res, _$_NfcNotExist> + implements _$$_NfcNotExistCopyWith<$Res> { + __$$_NfcNotExistCopyWithImpl( + _$_NfcNotExist _value, $Res Function(_$_NfcNotExist) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_NfcNotExist with DiagnosticableTreeMixin implements _NfcNotExist { + const _$_NfcNotExist(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassState.nfcNotExist()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('type', 'NfcPassState.nfcNotExist')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_NfcNotExist); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List nfcPasses) loaded, + required TResult Function() nfcNotExist, + required TResult Function(String cause) error, + required TResult Function() nfcDisabled, + required TResult Function() nfcNotSupported, + }) { + return nfcNotExist(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List nfcPasses)? loaded, + TResult? Function()? nfcNotExist, + TResult? Function(String cause)? error, + TResult? Function()? nfcDisabled, + TResult? Function()? nfcNotSupported, + }) { + return nfcNotExist?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List nfcPasses)? loaded, + TResult Function()? nfcNotExist, + TResult Function(String cause)? error, + TResult Function()? nfcDisabled, + TResult Function()? nfcNotSupported, + required TResult orElse(), + }) { + if (nfcNotExist != null) { + return nfcNotExist(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_NfcNotExist value) nfcNotExist, + required TResult Function(_Error value) error, + required TResult Function(_NfcDisabled value) nfcDisabled, + required TResult Function(_NfcNotSupported value) nfcNotSupported, + }) { + return nfcNotExist(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_NfcNotExist value)? nfcNotExist, + TResult? Function(_Error value)? error, + TResult? Function(_NfcDisabled value)? nfcDisabled, + TResult? Function(_NfcNotSupported value)? nfcNotSupported, + }) { + return nfcNotExist?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_NfcNotExist value)? nfcNotExist, + TResult Function(_Error value)? error, + TResult Function(_NfcDisabled value)? nfcDisabled, + TResult Function(_NfcNotSupported value)? nfcNotSupported, + required TResult orElse(), + }) { + if (nfcNotExist != null) { + return nfcNotExist(this); + } + return orElse(); + } +} + +abstract class _NfcNotExist implements NfcPassState { + const factory _NfcNotExist() = _$_NfcNotExist; +} + +/// @nodoc +abstract class _$$_ErrorCopyWith<$Res> { + factory _$$_ErrorCopyWith(_$_Error value, $Res Function(_$_Error) then) = + __$$_ErrorCopyWithImpl<$Res>; + @useResult + $Res call({String cause}); +} + +/// @nodoc +class __$$_ErrorCopyWithImpl<$Res> + extends _$NfcPassStateCopyWithImpl<$Res, _$_Error> + implements _$$_ErrorCopyWith<$Res> { + __$$_ErrorCopyWithImpl(_$_Error _value, $Res Function(_$_Error) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? cause = null, + }) { + return _then(_$_Error( + null == cause + ? _value.cause + : cause // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_Error with DiagnosticableTreeMixin implements _Error { + const _$_Error(this.cause); + + @override + final String cause; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassState.error(cause: $cause)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'NfcPassState.error')) + ..add(DiagnosticsProperty('cause', cause)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_Error && + (identical(other.cause, cause) || other.cause == cause)); + } + + @override + int get hashCode => Object.hash(runtimeType, cause); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_ErrorCopyWith<_$_Error> get copyWith => + __$$_ErrorCopyWithImpl<_$_Error>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List nfcPasses) loaded, + required TResult Function() nfcNotExist, + required TResult Function(String cause) error, + required TResult Function() nfcDisabled, + required TResult Function() nfcNotSupported, + }) { + return error(cause); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List nfcPasses)? loaded, + TResult? Function()? nfcNotExist, + TResult? Function(String cause)? error, + TResult? Function()? nfcDisabled, + TResult? Function()? nfcNotSupported, + }) { + return error?.call(cause); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List nfcPasses)? loaded, + TResult Function()? nfcNotExist, + TResult Function(String cause)? error, + TResult Function()? nfcDisabled, + TResult Function()? nfcNotSupported, + required TResult orElse(), + }) { + if (error != null) { + return error(cause); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_NfcNotExist value) nfcNotExist, + required TResult Function(_Error value) error, + required TResult Function(_NfcDisabled value) nfcDisabled, + required TResult Function(_NfcNotSupported value) nfcNotSupported, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_NfcNotExist value)? nfcNotExist, + TResult? Function(_Error value)? error, + TResult? Function(_NfcDisabled value)? nfcDisabled, + TResult? Function(_NfcNotSupported value)? nfcNotSupported, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_NfcNotExist value)? nfcNotExist, + TResult Function(_Error value)? error, + TResult Function(_NfcDisabled value)? nfcDisabled, + TResult Function(_NfcNotSupported value)? nfcNotSupported, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class _Error implements NfcPassState { + const factory _Error(final String cause) = _$_Error; + + String get cause; + @JsonKey(ignore: true) + _$$_ErrorCopyWith<_$_Error> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_NfcDisabledCopyWith<$Res> { + factory _$$_NfcDisabledCopyWith( + _$_NfcDisabled value, $Res Function(_$_NfcDisabled) then) = + __$$_NfcDisabledCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_NfcDisabledCopyWithImpl<$Res> + extends _$NfcPassStateCopyWithImpl<$Res, _$_NfcDisabled> + implements _$$_NfcDisabledCopyWith<$Res> { + __$$_NfcDisabledCopyWithImpl( + _$_NfcDisabled _value, $Res Function(_$_NfcDisabled) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_NfcDisabled with DiagnosticableTreeMixin implements _NfcDisabled { + const _$_NfcDisabled(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassState.nfcDisabled()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('type', 'NfcPassState.nfcDisabled')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_NfcDisabled); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List nfcPasses) loaded, + required TResult Function() nfcNotExist, + required TResult Function(String cause) error, + required TResult Function() nfcDisabled, + required TResult Function() nfcNotSupported, + }) { + return nfcDisabled(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List nfcPasses)? loaded, + TResult? Function()? nfcNotExist, + TResult? Function(String cause)? error, + TResult? Function()? nfcDisabled, + TResult? Function()? nfcNotSupported, + }) { + return nfcDisabled?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List nfcPasses)? loaded, + TResult Function()? nfcNotExist, + TResult Function(String cause)? error, + TResult Function()? nfcDisabled, + TResult Function()? nfcNotSupported, + required TResult orElse(), + }) { + if (nfcDisabled != null) { + return nfcDisabled(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_NfcNotExist value) nfcNotExist, + required TResult Function(_Error value) error, + required TResult Function(_NfcDisabled value) nfcDisabled, + required TResult Function(_NfcNotSupported value) nfcNotSupported, + }) { + return nfcDisabled(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_NfcNotExist value)? nfcNotExist, + TResult? Function(_Error value)? error, + TResult? Function(_NfcDisabled value)? nfcDisabled, + TResult? Function(_NfcNotSupported value)? nfcNotSupported, + }) { + return nfcDisabled?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_NfcNotExist value)? nfcNotExist, + TResult Function(_Error value)? error, + TResult Function(_NfcDisabled value)? nfcDisabled, + TResult Function(_NfcNotSupported value)? nfcNotSupported, + required TResult orElse(), + }) { + if (nfcDisabled != null) { + return nfcDisabled(this); + } + return orElse(); + } +} + +abstract class _NfcDisabled implements NfcPassState { + const factory _NfcDisabled() = _$_NfcDisabled; +} + +/// @nodoc +abstract class _$$_NfcNotSupportedCopyWith<$Res> { + factory _$$_NfcNotSupportedCopyWith( + _$_NfcNotSupported value, $Res Function(_$_NfcNotSupported) then) = + __$$_NfcNotSupportedCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_NfcNotSupportedCopyWithImpl<$Res> + extends _$NfcPassStateCopyWithImpl<$Res, _$_NfcNotSupported> + implements _$$_NfcNotSupportedCopyWith<$Res> { + __$$_NfcNotSupportedCopyWithImpl( + _$_NfcNotSupported _value, $Res Function(_$_NfcNotSupported) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_NfcNotSupported + with DiagnosticableTreeMixin + implements _NfcNotSupported { + const _$_NfcNotSupported(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'NfcPassState.nfcNotSupported()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('type', 'NfcPassState.nfcNotSupported')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_NfcNotSupported); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(List nfcPasses) loaded, + required TResult Function() nfcNotExist, + required TResult Function(String cause) error, + required TResult Function() nfcDisabled, + required TResult Function() nfcNotSupported, + }) { + return nfcNotSupported(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(List nfcPasses)? loaded, + TResult? Function()? nfcNotExist, + TResult? Function(String cause)? error, + TResult? Function()? nfcDisabled, + TResult? Function()? nfcNotSupported, + }) { + return nfcNotSupported?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(List nfcPasses)? loaded, + TResult Function()? nfcNotExist, + TResult Function(String cause)? error, + TResult Function()? nfcDisabled, + TResult Function()? nfcNotSupported, + required TResult orElse(), + }) { + if (nfcNotSupported != null) { + return nfcNotSupported(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Initial value) initial, + required TResult Function(_Loading value) loading, + required TResult Function(_Loaded value) loaded, + required TResult Function(_NfcNotExist value) nfcNotExist, + required TResult Function(_Error value) error, + required TResult Function(_NfcDisabled value) nfcDisabled, + required TResult Function(_NfcNotSupported value) nfcNotSupported, + }) { + return nfcNotSupported(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Initial value)? initial, + TResult? Function(_Loading value)? loading, + TResult? Function(_Loaded value)? loaded, + TResult? Function(_NfcNotExist value)? nfcNotExist, + TResult? Function(_Error value)? error, + TResult? Function(_NfcDisabled value)? nfcDisabled, + TResult? Function(_NfcNotSupported value)? nfcNotSupported, + }) { + return nfcNotSupported?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Initial value)? initial, + TResult Function(_Loading value)? loading, + TResult Function(_Loaded value)? loaded, + TResult Function(_NfcNotExist value)? nfcNotExist, + TResult Function(_Error value)? error, + TResult Function(_NfcDisabled value)? nfcDisabled, + TResult Function(_NfcNotSupported value)? nfcNotSupported, + required TResult orElse(), + }) { + if (nfcNotSupported != null) { + return nfcNotSupported(this); + } + return orElse(); + } +} + +abstract class _NfcNotSupported implements NfcPassState { + const factory _NfcNotSupported() = _$_NfcNotSupported; +} diff --git a/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_event.dart b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_event.dart new file mode 100644 index 00000000..c359b5ce --- /dev/null +++ b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_event.dart @@ -0,0 +1,19 @@ +part of 'nfc_pass_bloc.dart'; + +@freezed +class NfcPassEvent with _$NfcPassEvent { + const factory NfcPassEvent.started() = _Started; + const factory NfcPassEvent.connectNfcPass( + String code, String studentId, String deviceId, String deviceName) = + _ConnectNfcPass; + const factory NfcPassEvent.getNfcPasses( + String code, String studentId, String deviceId) = _GetNfcPasses; + + /// Event for fetching NFC code from server. If device is connected to the + /// server, it will return NFC code. If not, it will clear NFC code from + /// the device (Secure Storage) + /// + /// This event must be called after application is started, user is logged in + /// and device is connected to the server. + const factory NfcPassEvent.fetchNfcCode() = _FetchNfcCode; +} diff --git a/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_state.dart b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_state.dart new file mode 100644 index 00000000..5f8e945f --- /dev/null +++ b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_state.dart @@ -0,0 +1,15 @@ +part of 'nfc_pass_bloc.dart'; + +@freezed +class NfcPassState with _$NfcPassState { + const factory NfcPassState.initial() = _Initial; + + /// Loading state when the app is tring to get Device NFC availability status + /// and get NFC passes from the server + const factory NfcPassState.loading() = _Loading; + const factory NfcPassState.loaded(List nfcPasses) = _Loaded; + const factory NfcPassState.nfcNotExist() = _NfcNotExist; + const factory NfcPassState.error(String cause) = _Error; + const factory NfcPassState.nfcDisabled() = _NfcDisabled; + const factory NfcPassState.nfcNotSupported() = _NfcNotSupported; +} diff --git a/lib/presentation/bloc/profile_bloc/profile_bloc.dart b/lib/presentation/bloc/profile_bloc/profile_bloc.dart deleted file mode 100644 index 9e7088b7..00000000 --- a/lib/presentation/bloc/profile_bloc/profile_bloc.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:rtu_mirea_app/domain/entities/user.dart'; -import 'package:rtu_mirea_app/domain/usecases/get_user_data.dart'; - -part 'profile_event.dart'; -part 'profile_state.dart'; - -class ProfileBloc extends Bloc { - final GetUserData getUserData; - - ProfileBloc({required this.getUserData}) : super(ProfileInitial()) { - on(_onProfileGetUserData); - } - - void _onProfileGetUserData( - ProfileGetUserData event, - Emitter emit, - ) async { - // To get profile data only once - if (state.runtimeType != ProfileLoaded) { - emit(ProfileLoading()); - final user = await getUserData(GetUserDataParams(event.token)); - user.fold((failure) => emit(ProfileInitial()), - (r) => emit(ProfileLoaded(user: r))); - } - } -} diff --git a/lib/presentation/bloc/profile_bloc/profile_event.dart b/lib/presentation/bloc/profile_bloc/profile_event.dart deleted file mode 100644 index 7a9b6abc..00000000 --- a/lib/presentation/bloc/profile_bloc/profile_event.dart +++ /dev/null @@ -1,14 +0,0 @@ -part of 'profile_bloc.dart'; - -abstract class ProfileEvent extends Equatable { - const ProfileEvent(); - - @override - List get props => []; -} - -class ProfileGetUserData extends ProfileEvent { - final String token; - - const ProfileGetUserData(this.token); -} diff --git a/lib/presentation/bloc/profile_bloc/profile_state.dart b/lib/presentation/bloc/profile_bloc/profile_state.dart deleted file mode 100644 index 20de792f..00000000 --- a/lib/presentation/bloc/profile_bloc/profile_state.dart +++ /dev/null @@ -1,21 +0,0 @@ -part of 'profile_bloc.dart'; - -abstract class ProfileState extends Equatable { - const ProfileState(); - - @override - List get props => []; -} - -class ProfileInitial extends ProfileState {} - -class ProfileLoading extends ProfileState {} - -class ProfileLoaded extends ProfileState { - final User user; - - const ProfileLoaded({required this.user}); - - @override - List get props => [user]; -} diff --git a/lib/presentation/bloc/scores_bloc/scores_bloc.dart b/lib/presentation/bloc/scores_bloc/scores_bloc.dart index bd3034e5..28116073 100644 --- a/lib/presentation/bloc/scores_bloc/scores_bloc.dart +++ b/lib/presentation/bloc/scores_bloc/scores_bloc.dart @@ -45,7 +45,7 @@ class ScoresBloc extends Bloc { if (state.runtimeType != ScoresLoaded) { emit(ScoresLoading()); - final scores = await getScores(GetScoresParams(event.token)); + final scores = await getScores(); scores.fold((failure) => emit(ScoresLoadError()), (result) { emit(ScoresLoaded( diff --git a/lib/presentation/bloc/scores_bloc/scores_event.dart b/lib/presentation/bloc/scores_bloc/scores_event.dart index cabed0e2..8a4ae1d8 100644 --- a/lib/presentation/bloc/scores_bloc/scores_event.dart +++ b/lib/presentation/bloc/scores_bloc/scores_event.dart @@ -8,14 +8,10 @@ abstract class ScoresEvent extends Equatable { } class LoadScores extends ScoresEvent { - final String token; - - const LoadScores({ - required this.token, - }); + const LoadScores(); @override - List get props => [token]; + List get props => []; } class ChangeSelectedScoresSemester extends ScoresEvent { diff --git a/lib/presentation/bloc/user_bloc/user_bloc.dart b/lib/presentation/bloc/user_bloc/user_bloc.dart new file mode 100644 index 00000000..b1d9065f --- /dev/null +++ b/lib/presentation/bloc/user_bloc/user_bloc.dart @@ -0,0 +1,100 @@ +import 'package:bloc/bloc.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:rtu_mirea_app/domain/entities/user.dart'; +import 'package:rtu_mirea_app/domain/usecases/get_auth_token.dart'; +import 'package:rtu_mirea_app/domain/usecases/get_user_data.dart'; +import 'package:rtu_mirea_app/domain/usecases/log_in.dart'; +import 'package:rtu_mirea_app/domain/usecases/log_out.dart'; + +part 'user_event.dart'; +part 'user_state.dart'; +part 'user_bloc.freezed.dart'; + +class UserBloc extends Bloc { + final LogIn logIn; + final LogOut logOut; + final GetUserData getUserData; + final GetAuthToken getAuthToken; + + UserBloc({ + required this.logIn, + required this.logOut, + required this.getUserData, + required this.getAuthToken, + }) : super(const _Unauthorized()) { + on<_LogIn>(_onLogInEvent); + on<_LogOut>(_onLogOutEvent); + on<_Started>(_onGetUserDataEvent); + on<_GetUserData>(_onGetUserDataEvent); + } + + void _onLogInEvent( + UserEvent event, + Emitter emit, + ) async { + if (state is _Loading) return; + + emit(const _Loading()); + // We use oauth2 to get token. So we don't need to pass login and password + // to the server. We just need to pass them to the oauth2 server. + + bool loggedIn = false; + + (await logIn()).fold( + (failure) => emit(_LogInError( + failure.cause ?? "Ошибка при авторизации. Повторите попытку")), + (res) { + loggedIn = true; + }, + ); + + if (loggedIn) { + final user = await getUserData(); + + user.fold( + (failure) => emit(const _Unauthorized()), + (user) { + FirebaseAnalytics.instance.logLogin(); + emit(_LogInSuccess(user)); + }, + ); + } + } + + void _onLogOutEvent( + UserEvent event, + Emitter emit, + ) async { + final res = await logOut(); + res.fold((failure) => null, (r) => emit(const _Unauthorized())); + } + + void _onGetUserDataEvent( + UserEvent event, + Emitter emit, + ) async { + // To get profile data only once (If state is not loading) + if (state is _Loading) return; + + final token = await getAuthToken(); + + bool loggedIn = false; + + // If token in the storage, user is authorized at least once and we can + // try to get user data + token.fold((failure) => emit(const _Unauthorized()), (r) { + loggedIn = true; + }); + + if (loggedIn) { + emit(const _Loading()); + final user = await getUserData(); + + user.fold( + (failure) => emit(const _Unauthorized()), + (r) => emit(_LogInSuccess(r)), + ); + } + } +} diff --git a/lib/presentation/bloc/user_bloc/user_bloc.freezed.dart b/lib/presentation/bloc/user_bloc/user_bloc.freezed.dart new file mode 100644 index 00000000..fc3448bd --- /dev/null +++ b/lib/presentation/bloc/user_bloc/user_bloc.freezed.dart @@ -0,0 +1,1125 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'user_bloc.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$UserEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function() logIn, + required TResult Function() logOut, + required TResult Function() getUserData, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function()? logIn, + TResult? Function()? logOut, + TResult? Function()? getUserData, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function()? logIn, + TResult Function()? logOut, + TResult Function()? getUserData, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_LogIn value) logIn, + required TResult Function(_LogOut value) logOut, + required TResult Function(_GetUserData value) getUserData, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_LogIn value)? logIn, + TResult? Function(_LogOut value)? logOut, + TResult? Function(_GetUserData value)? getUserData, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_LogIn value)? logIn, + TResult Function(_LogOut value)? logOut, + TResult Function(_GetUserData value)? getUserData, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserEventCopyWith<$Res> { + factory $UserEventCopyWith(UserEvent value, $Res Function(UserEvent) then) = + _$UserEventCopyWithImpl<$Res, UserEvent>; +} + +/// @nodoc +class _$UserEventCopyWithImpl<$Res, $Val extends UserEvent> + implements $UserEventCopyWith<$Res> { + _$UserEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_StartedCopyWith<$Res> { + factory _$$_StartedCopyWith( + _$_Started value, $Res Function(_$_Started) then) = + __$$_StartedCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_StartedCopyWithImpl<$Res> + extends _$UserEventCopyWithImpl<$Res, _$_Started> + implements _$$_StartedCopyWith<$Res> { + __$$_StartedCopyWithImpl(_$_Started _value, $Res Function(_$_Started) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Started implements _Started { + const _$_Started(); + + @override + String toString() { + return 'UserEvent.started()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Started); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function() logIn, + required TResult Function() logOut, + required TResult Function() getUserData, + }) { + return started(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function()? logIn, + TResult? Function()? logOut, + TResult? Function()? getUserData, + }) { + return started?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function()? logIn, + TResult Function()? logOut, + TResult Function()? getUserData, + required TResult orElse(), + }) { + if (started != null) { + return started(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_LogIn value) logIn, + required TResult Function(_LogOut value) logOut, + required TResult Function(_GetUserData value) getUserData, + }) { + return started(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_LogIn value)? logIn, + TResult? Function(_LogOut value)? logOut, + TResult? Function(_GetUserData value)? getUserData, + }) { + return started?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_LogIn value)? logIn, + TResult Function(_LogOut value)? logOut, + TResult Function(_GetUserData value)? getUserData, + required TResult orElse(), + }) { + if (started != null) { + return started(this); + } + return orElse(); + } +} + +abstract class _Started implements UserEvent { + const factory _Started() = _$_Started; +} + +/// @nodoc +abstract class _$$_LogInCopyWith<$Res> { + factory _$$_LogInCopyWith(_$_LogIn value, $Res Function(_$_LogIn) then) = + __$$_LogInCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_LogInCopyWithImpl<$Res> + extends _$UserEventCopyWithImpl<$Res, _$_LogIn> + implements _$$_LogInCopyWith<$Res> { + __$$_LogInCopyWithImpl(_$_LogIn _value, $Res Function(_$_LogIn) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_LogIn implements _LogIn { + const _$_LogIn(); + + @override + String toString() { + return 'UserEvent.logIn()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_LogIn); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function() logIn, + required TResult Function() logOut, + required TResult Function() getUserData, + }) { + return logIn(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function()? logIn, + TResult? Function()? logOut, + TResult? Function()? getUserData, + }) { + return logIn?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function()? logIn, + TResult Function()? logOut, + TResult Function()? getUserData, + required TResult orElse(), + }) { + if (logIn != null) { + return logIn(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_LogIn value) logIn, + required TResult Function(_LogOut value) logOut, + required TResult Function(_GetUserData value) getUserData, + }) { + return logIn(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_LogIn value)? logIn, + TResult? Function(_LogOut value)? logOut, + TResult? Function(_GetUserData value)? getUserData, + }) { + return logIn?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_LogIn value)? logIn, + TResult Function(_LogOut value)? logOut, + TResult Function(_GetUserData value)? getUserData, + required TResult orElse(), + }) { + if (logIn != null) { + return logIn(this); + } + return orElse(); + } +} + +abstract class _LogIn implements UserEvent { + const factory _LogIn() = _$_LogIn; +} + +/// @nodoc +abstract class _$$_LogOutCopyWith<$Res> { + factory _$$_LogOutCopyWith(_$_LogOut value, $Res Function(_$_LogOut) then) = + __$$_LogOutCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_LogOutCopyWithImpl<$Res> + extends _$UserEventCopyWithImpl<$Res, _$_LogOut> + implements _$$_LogOutCopyWith<$Res> { + __$$_LogOutCopyWithImpl(_$_LogOut _value, $Res Function(_$_LogOut) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_LogOut implements _LogOut { + const _$_LogOut(); + + @override + String toString() { + return 'UserEvent.logOut()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_LogOut); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function() logIn, + required TResult Function() logOut, + required TResult Function() getUserData, + }) { + return logOut(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function()? logIn, + TResult? Function()? logOut, + TResult? Function()? getUserData, + }) { + return logOut?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function()? logIn, + TResult Function()? logOut, + TResult Function()? getUserData, + required TResult orElse(), + }) { + if (logOut != null) { + return logOut(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_LogIn value) logIn, + required TResult Function(_LogOut value) logOut, + required TResult Function(_GetUserData value) getUserData, + }) { + return logOut(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_LogIn value)? logIn, + TResult? Function(_LogOut value)? logOut, + TResult? Function(_GetUserData value)? getUserData, + }) { + return logOut?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_LogIn value)? logIn, + TResult Function(_LogOut value)? logOut, + TResult Function(_GetUserData value)? getUserData, + required TResult orElse(), + }) { + if (logOut != null) { + return logOut(this); + } + return orElse(); + } +} + +abstract class _LogOut implements UserEvent { + const factory _LogOut() = _$_LogOut; +} + +/// @nodoc +abstract class _$$_GetUserDataCopyWith<$Res> { + factory _$$_GetUserDataCopyWith( + _$_GetUserData value, $Res Function(_$_GetUserData) then) = + __$$_GetUserDataCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_GetUserDataCopyWithImpl<$Res> + extends _$UserEventCopyWithImpl<$Res, _$_GetUserData> + implements _$$_GetUserDataCopyWith<$Res> { + __$$_GetUserDataCopyWithImpl( + _$_GetUserData _value, $Res Function(_$_GetUserData) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_GetUserData implements _GetUserData { + const _$_GetUserData(); + + @override + String toString() { + return 'UserEvent.getUserData()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_GetUserData); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() started, + required TResult Function() logIn, + required TResult Function() logOut, + required TResult Function() getUserData, + }) { + return getUserData(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? started, + TResult? Function()? logIn, + TResult? Function()? logOut, + TResult? Function()? getUserData, + }) { + return getUserData?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? started, + TResult Function()? logIn, + TResult Function()? logOut, + TResult Function()? getUserData, + required TResult orElse(), + }) { + if (getUserData != null) { + return getUserData(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Started value) started, + required TResult Function(_LogIn value) logIn, + required TResult Function(_LogOut value) logOut, + required TResult Function(_GetUserData value) getUserData, + }) { + return getUserData(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Started value)? started, + TResult? Function(_LogIn value)? logIn, + TResult? Function(_LogOut value)? logOut, + TResult? Function(_GetUserData value)? getUserData, + }) { + return getUserData?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Started value)? started, + TResult Function(_LogIn value)? logIn, + TResult Function(_LogOut value)? logOut, + TResult Function(_GetUserData value)? getUserData, + required TResult orElse(), + }) { + if (getUserData != null) { + return getUserData(this); + } + return orElse(); + } +} + +abstract class _GetUserData implements UserEvent { + const factory _GetUserData() = _$_GetUserData; +} + +/// @nodoc +mixin _$UserState { + @optionalTypeArgs + TResult when({ + required TResult Function() unauthorized, + required TResult Function() loading, + required TResult Function(String cause) logInError, + required TResult Function(User user) logInSuccess, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unauthorized, + TResult? Function()? loading, + TResult? Function(String cause)? logInError, + TResult? Function(User user)? logInSuccess, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unauthorized, + TResult Function()? loading, + TResult Function(String cause)? logInError, + TResult Function(User user)? logInSuccess, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Unauthorized value) unauthorized, + required TResult Function(_Loading value) loading, + required TResult Function(_LogInError value) logInError, + required TResult Function(_LogInSuccess value) logInSuccess, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Unauthorized value)? unauthorized, + TResult? Function(_Loading value)? loading, + TResult? Function(_LogInError value)? logInError, + TResult? Function(_LogInSuccess value)? logInSuccess, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Unauthorized value)? unauthorized, + TResult Function(_Loading value)? loading, + TResult Function(_LogInError value)? logInError, + TResult Function(_LogInSuccess value)? logInSuccess, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserStateCopyWith<$Res> { + factory $UserStateCopyWith(UserState value, $Res Function(UserState) then) = + _$UserStateCopyWithImpl<$Res, UserState>; +} + +/// @nodoc +class _$UserStateCopyWithImpl<$Res, $Val extends UserState> + implements $UserStateCopyWith<$Res> { + _$UserStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$_UnauthorizedCopyWith<$Res> { + factory _$$_UnauthorizedCopyWith( + _$_Unauthorized value, $Res Function(_$_Unauthorized) then) = + __$$_UnauthorizedCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_UnauthorizedCopyWithImpl<$Res> + extends _$UserStateCopyWithImpl<$Res, _$_Unauthorized> + implements _$$_UnauthorizedCopyWith<$Res> { + __$$_UnauthorizedCopyWithImpl( + _$_Unauthorized _value, $Res Function(_$_Unauthorized) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Unauthorized implements _Unauthorized { + const _$_Unauthorized(); + + @override + String toString() { + return 'UserState.unauthorized()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Unauthorized); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() unauthorized, + required TResult Function() loading, + required TResult Function(String cause) logInError, + required TResult Function(User user) logInSuccess, + }) { + return unauthorized(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unauthorized, + TResult? Function()? loading, + TResult? Function(String cause)? logInError, + TResult? Function(User user)? logInSuccess, + }) { + return unauthorized?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unauthorized, + TResult Function()? loading, + TResult Function(String cause)? logInError, + TResult Function(User user)? logInSuccess, + required TResult orElse(), + }) { + if (unauthorized != null) { + return unauthorized(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Unauthorized value) unauthorized, + required TResult Function(_Loading value) loading, + required TResult Function(_LogInError value) logInError, + required TResult Function(_LogInSuccess value) logInSuccess, + }) { + return unauthorized(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Unauthorized value)? unauthorized, + TResult? Function(_Loading value)? loading, + TResult? Function(_LogInError value)? logInError, + TResult? Function(_LogInSuccess value)? logInSuccess, + }) { + return unauthorized?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Unauthorized value)? unauthorized, + TResult Function(_Loading value)? loading, + TResult Function(_LogInError value)? logInError, + TResult Function(_LogInSuccess value)? logInSuccess, + required TResult orElse(), + }) { + if (unauthorized != null) { + return unauthorized(this); + } + return orElse(); + } +} + +abstract class _Unauthorized implements UserState { + const factory _Unauthorized() = _$_Unauthorized; +} + +/// @nodoc +abstract class _$$_LoadingCopyWith<$Res> { + factory _$$_LoadingCopyWith( + _$_Loading value, $Res Function(_$_Loading) then) = + __$$_LoadingCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$_LoadingCopyWithImpl<$Res> + extends _$UserStateCopyWithImpl<$Res, _$_Loading> + implements _$$_LoadingCopyWith<$Res> { + __$$_LoadingCopyWithImpl(_$_Loading _value, $Res Function(_$_Loading) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$_Loading implements _Loading { + const _$_Loading(); + + @override + String toString() { + return 'UserState.loading()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$_Loading); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() unauthorized, + required TResult Function() loading, + required TResult Function(String cause) logInError, + required TResult Function(User user) logInSuccess, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unauthorized, + TResult? Function()? loading, + TResult? Function(String cause)? logInError, + TResult? Function(User user)? logInSuccess, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unauthorized, + TResult Function()? loading, + TResult Function(String cause)? logInError, + TResult Function(User user)? logInSuccess, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Unauthorized value) unauthorized, + required TResult Function(_Loading value) loading, + required TResult Function(_LogInError value) logInError, + required TResult Function(_LogInSuccess value) logInSuccess, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Unauthorized value)? unauthorized, + TResult? Function(_Loading value)? loading, + TResult? Function(_LogInError value)? logInError, + TResult? Function(_LogInSuccess value)? logInSuccess, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Unauthorized value)? unauthorized, + TResult Function(_Loading value)? loading, + TResult Function(_LogInError value)? logInError, + TResult Function(_LogInSuccess value)? logInSuccess, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _Loading implements UserState { + const factory _Loading() = _$_Loading; +} + +/// @nodoc +abstract class _$$_LogInErrorCopyWith<$Res> { + factory _$$_LogInErrorCopyWith( + _$_LogInError value, $Res Function(_$_LogInError) then) = + __$$_LogInErrorCopyWithImpl<$Res>; + @useResult + $Res call({String cause}); +} + +/// @nodoc +class __$$_LogInErrorCopyWithImpl<$Res> + extends _$UserStateCopyWithImpl<$Res, _$_LogInError> + implements _$$_LogInErrorCopyWith<$Res> { + __$$_LogInErrorCopyWithImpl( + _$_LogInError _value, $Res Function(_$_LogInError) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? cause = null, + }) { + return _then(_$_LogInError( + null == cause + ? _value.cause + : cause // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$_LogInError implements _LogInError { + const _$_LogInError(this.cause); + + @override + final String cause; + + @override + String toString() { + return 'UserState.logInError(cause: $cause)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_LogInError && + (identical(other.cause, cause) || other.cause == cause)); + } + + @override + int get hashCode => Object.hash(runtimeType, cause); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_LogInErrorCopyWith<_$_LogInError> get copyWith => + __$$_LogInErrorCopyWithImpl<_$_LogInError>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() unauthorized, + required TResult Function() loading, + required TResult Function(String cause) logInError, + required TResult Function(User user) logInSuccess, + }) { + return logInError(cause); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unauthorized, + TResult? Function()? loading, + TResult? Function(String cause)? logInError, + TResult? Function(User user)? logInSuccess, + }) { + return logInError?.call(cause); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unauthorized, + TResult Function()? loading, + TResult Function(String cause)? logInError, + TResult Function(User user)? logInSuccess, + required TResult orElse(), + }) { + if (logInError != null) { + return logInError(cause); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Unauthorized value) unauthorized, + required TResult Function(_Loading value) loading, + required TResult Function(_LogInError value) logInError, + required TResult Function(_LogInSuccess value) logInSuccess, + }) { + return logInError(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Unauthorized value)? unauthorized, + TResult? Function(_Loading value)? loading, + TResult? Function(_LogInError value)? logInError, + TResult? Function(_LogInSuccess value)? logInSuccess, + }) { + return logInError?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Unauthorized value)? unauthorized, + TResult Function(_Loading value)? loading, + TResult Function(_LogInError value)? logInError, + TResult Function(_LogInSuccess value)? logInSuccess, + required TResult orElse(), + }) { + if (logInError != null) { + return logInError(this); + } + return orElse(); + } +} + +abstract class _LogInError implements UserState { + const factory _LogInError(final String cause) = _$_LogInError; + + String get cause; + @JsonKey(ignore: true) + _$$_LogInErrorCopyWith<_$_LogInError> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$_LogInSuccessCopyWith<$Res> { + factory _$$_LogInSuccessCopyWith( + _$_LogInSuccess value, $Res Function(_$_LogInSuccess) then) = + __$$_LogInSuccessCopyWithImpl<$Res>; + @useResult + $Res call({User user}); +} + +/// @nodoc +class __$$_LogInSuccessCopyWithImpl<$Res> + extends _$UserStateCopyWithImpl<$Res, _$_LogInSuccess> + implements _$$_LogInSuccessCopyWith<$Res> { + __$$_LogInSuccessCopyWithImpl( + _$_LogInSuccess _value, $Res Function(_$_LogInSuccess) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + }) { + return _then(_$_LogInSuccess( + null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as User, + )); + } +} + +/// @nodoc + +class _$_LogInSuccess implements _LogInSuccess { + const _$_LogInSuccess(this.user); + + @override + final User user; + + @override + String toString() { + return 'UserState.logInSuccess(user: $user)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_LogInSuccess && + (identical(other.user, user) || other.user == user)); + } + + @override + int get hashCode => Object.hash(runtimeType, user); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_LogInSuccessCopyWith<_$_LogInSuccess> get copyWith => + __$$_LogInSuccessCopyWithImpl<_$_LogInSuccess>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() unauthorized, + required TResult Function() loading, + required TResult Function(String cause) logInError, + required TResult Function(User user) logInSuccess, + }) { + return logInSuccess(user); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unauthorized, + TResult? Function()? loading, + TResult? Function(String cause)? logInError, + TResult? Function(User user)? logInSuccess, + }) { + return logInSuccess?.call(user); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unauthorized, + TResult Function()? loading, + TResult Function(String cause)? logInError, + TResult Function(User user)? logInSuccess, + required TResult orElse(), + }) { + if (logInSuccess != null) { + return logInSuccess(user); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Unauthorized value) unauthorized, + required TResult Function(_Loading value) loading, + required TResult Function(_LogInError value) logInError, + required TResult Function(_LogInSuccess value) logInSuccess, + }) { + return logInSuccess(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Unauthorized value)? unauthorized, + TResult? Function(_Loading value)? loading, + TResult? Function(_LogInError value)? logInError, + TResult? Function(_LogInSuccess value)? logInSuccess, + }) { + return logInSuccess?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Unauthorized value)? unauthorized, + TResult Function(_Loading value)? loading, + TResult Function(_LogInError value)? logInError, + TResult Function(_LogInSuccess value)? logInSuccess, + required TResult orElse(), + }) { + if (logInSuccess != null) { + return logInSuccess(this); + } + return orElse(); + } +} + +abstract class _LogInSuccess implements UserState { + const factory _LogInSuccess(final User user) = _$_LogInSuccess; + + User get user; + @JsonKey(ignore: true) + _$$_LogInSuccessCopyWith<_$_LogInSuccess> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/presentation/bloc/user_bloc/user_event.dart b/lib/presentation/bloc/user_bloc/user_event.dart new file mode 100644 index 00000000..834e1120 --- /dev/null +++ b/lib/presentation/bloc/user_bloc/user_event.dart @@ -0,0 +1,9 @@ +part of 'user_bloc.dart'; + +@freezed +class UserEvent with _$UserEvent { + const factory UserEvent.started() = _Started; + const factory UserEvent.logIn() = _LogIn; + const factory UserEvent.logOut() = _LogOut; + const factory UserEvent.getUserData() = _GetUserData; +} diff --git a/lib/presentation/bloc/user_bloc/user_state.dart b/lib/presentation/bloc/user_bloc/user_state.dart new file mode 100644 index 00000000..50da8ea5 --- /dev/null +++ b/lib/presentation/bloc/user_bloc/user_state.dart @@ -0,0 +1,9 @@ +part of 'user_bloc.dart'; + +@freezed +class UserState with _$UserState { + const factory UserState.unauthorized() = _Unauthorized; + const factory UserState.loading() = _Loading; + const factory UserState.logInError(String cause) = _LogInError; + const factory UserState.logInSuccess(User user) = _LogInSuccess; +} diff --git a/lib/presentation/core/routes/routes.dart b/lib/presentation/core/routes/routes.dart index 00c2f073..7858af3b 100644 --- a/lib/presentation/core/routes/routes.dart +++ b/lib/presentation/core/routes/routes.dart @@ -14,6 +14,7 @@ import 'package:rtu_mirea_app/presentation/pages/profile/profile_announces_page. import 'package:rtu_mirea_app/presentation/pages/profile/profile_attendance_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_detail_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_lectors_page.dart'; +import 'package:rtu_mirea_app/presentation/pages/profile/profile_nfc_pass_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_scores_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_page.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/profile_settings_page.dart'; @@ -103,6 +104,10 @@ import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart'; path: 'settings', page: ProfileSettingsPage, ), + AutoRoute( + path: 'nfc-pass', + page: ProfileNfcPassPage, + ) ], ), ], diff --git a/lib/presentation/core/routes/routes.gr.dart b/lib/presentation/core/routes/routes.gr.dart index 67bbdf1a..669b8db2 100644 --- a/lib/presentation/core/routes/routes.gr.dart +++ b/lib/presentation/core/routes/routes.gr.dart @@ -11,13 +11,13 @@ // ignore_for_file: type=lint // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i19; +import 'package:auto_route/auto_route.dart' as _i20; import 'package:auto_route/empty_router_widgets.dart' as _i4; -import 'package:flutter/material.dart' as _i20; -import 'package:rtu_mirea_app/domain/entities/news_item.dart' as _i23; -import 'package:rtu_mirea_app/domain/entities/story.dart' as _i22; -import 'package:rtu_mirea_app/domain/entities/user.dart' as _i24; -import 'package:rtu_mirea_app/presentation/core/routes/routes.dart' as _i21; +import 'package:flutter/material.dart' as _i21; +import 'package:rtu_mirea_app/domain/entities/news_item.dart' as _i24; +import 'package:rtu_mirea_app/domain/entities/story.dart' as _i23; +import 'package:rtu_mirea_app/domain/entities/user.dart' as _i25; +import 'package:rtu_mirea_app/presentation/core/routes/routes.dart' as _i22; import 'package:rtu_mirea_app/presentation/pages/home_page.dart' as _i1; import 'package:rtu_mirea_app/presentation/pages/login/login_page.dart' as _i11; import 'package:rtu_mirea_app/presentation/pages/map/map_page.dart' as _i5; @@ -38,6 +38,8 @@ import 'package:rtu_mirea_app/presentation/pages/profile/profile_detail_page.dar as _i15; import 'package:rtu_mirea_app/presentation/pages/profile/profile_lectors_page.dart' as _i16; +import 'package:rtu_mirea_app/presentation/pages/profile/profile_nfc_pass_page.dart' + as _i19; import 'package:rtu_mirea_app/presentation/pages/profile/profile_page.dart' as _i10; import 'package:rtu_mirea_app/presentation/pages/profile/profile_scores_page.dart' @@ -49,83 +51,83 @@ import 'package:rtu_mirea_app/presentation/pages/schedule/groups_select_page.dar import 'package:rtu_mirea_app/presentation/pages/schedule/schedule_page.dart' as _i6; -class AppRouter extends _i19.RootStackRouter { - AppRouter([_i20.GlobalKey<_i20.NavigatorState>? navigatorKey]) +class AppRouter extends _i20.RootStackRouter { + AppRouter([_i21.GlobalKey<_i21.NavigatorState>? navigatorKey]) : super(navigatorKey); @override - final Map pagesMap = { + final Map pagesMap = { HomeRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i1.HomePage(), ); }, OnBoardingRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i2.OnBoardingPage(), ); }, StoriesWrapperRoute.name: (routeData) { final args = routeData.argsAs(); - return _i19.CustomPage( + return _i20.CustomPage( routeData: routeData, child: _i3.StoriesWrapper( key: args.key, stories: args.stories, storyIndex: args.storyIndex, ), - customRouteBuilder: _i21.transparentRoute, + customRouteBuilder: _i22.transparentRoute, opaque: false, barrierDismissible: false, ); }, ScheduleRouter.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i4.EmptyRouterPage(), ); }, NewsRouter.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i4.EmptyRouterPage(), ); }, MapRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i5.MapPage(), ); }, ProfileRouter.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i4.EmptyRouterPage(), ); }, ScheduleRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i6.SchedulePage(), ); }, GroupsSelectRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i7.GroupsSelectPage(), ); }, NewsRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i8.NewsPage(), ); }, NewsDetailsRoute.name: (routeData) { final args = routeData.argsAs(); - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: _i9.NewsDetailsPage( key: args.key, @@ -134,38 +136,38 @@ class AppRouter extends _i19.RootStackRouter { ); }, ProfileRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i10.ProfilePage(), ); }, LoginRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i11.LoginPage(), ); }, AboutAppRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i12.AboutAppPage(), ); }, ProfileAnnouncesRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i13.ProfileAnnouncesPage(), ); }, ProfileAttendanceRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i14.ProfileAttendancePage(), ); }, ProfileDetailRoute.name: (routeData) { final args = routeData.argsAs(); - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: _i15.ProfileDetailPage( key: args.key, @@ -174,140 +176,151 @@ class AppRouter extends _i19.RootStackRouter { ); }, ProfileLectrosRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i16.ProfileLectrosPage(), ); }, ProfileScoresRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i17.ProfileScoresPage(), ); }, ProfileSettingsRoute.name: (routeData) { - return _i19.AdaptivePage( + return _i20.AdaptivePage( routeData: routeData, child: const _i18.ProfileSettingsPage(), ); }, + ProfileNfcPassRoute.name: (routeData) { + return _i20.AdaptivePage( + routeData: routeData, + child: const _i19.ProfileNfcPassPage(), + ); + }, }; @override - List<_i19.RouteConfig> get routes => [ - _i19.RouteConfig( + List<_i20.RouteConfig> get routes => [ + _i20.RouteConfig( HomeRoute.name, path: '/', children: [ - _i19.RouteConfig( + _i20.RouteConfig( '#redirect', path: '', parent: HomeRoute.name, redirectTo: 'schedule', fullMatch: true, ), - _i19.RouteConfig( + _i20.RouteConfig( ScheduleRouter.name, path: 'schedule', parent: HomeRoute.name, children: [ - _i19.RouteConfig( + _i20.RouteConfig( ScheduleRoute.name, path: '', parent: ScheduleRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( GroupsSelectRoute.name, path: 'select-group', parent: ScheduleRouter.name, ), ], ), - _i19.RouteConfig( + _i20.RouteConfig( NewsRouter.name, path: 'news', parent: HomeRoute.name, children: [ - _i19.RouteConfig( + _i20.RouteConfig( NewsRoute.name, path: '', parent: NewsRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( NewsDetailsRoute.name, path: 'details', parent: NewsRouter.name, ), ], ), - _i19.RouteConfig( + _i20.RouteConfig( MapRoute.name, path: 'map', parent: HomeRoute.name, ), - _i19.RouteConfig( + _i20.RouteConfig( ProfileRouter.name, path: 'profile', parent: HomeRoute.name, children: [ - _i19.RouteConfig( + _i20.RouteConfig( ProfileRoute.name, path: '', parent: ProfileRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( LoginRoute.name, path: 'login', parent: ProfileRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( AboutAppRoute.name, path: 'about', parent: ProfileRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( ProfileAnnouncesRoute.name, path: 'announces', parent: ProfileRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( ProfileAttendanceRoute.name, path: 'attendance', parent: ProfileRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( ProfileDetailRoute.name, path: 'details', parent: ProfileRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( ProfileLectrosRoute.name, path: 'lectors', parent: ProfileRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( ProfileScoresRoute.name, path: 'scores', parent: ProfileRouter.name, ), - _i19.RouteConfig( + _i20.RouteConfig( ProfileSettingsRoute.name, path: 'settings', parent: ProfileRouter.name, ), + _i20.RouteConfig( + ProfileNfcPassRoute.name, + path: 'nfc-pass', + parent: ProfileRouter.name, + ), ], ), ], ), - _i19.RouteConfig( + _i20.RouteConfig( OnBoardingRoute.name, path: '/onboarding', ), - _i19.RouteConfig( + _i20.RouteConfig( StoriesWrapperRoute.name, path: '/story', ), - _i19.RouteConfig( + _i20.RouteConfig( '*#redirect', path: '*', redirectTo: '/', @@ -318,8 +331,8 @@ class AppRouter extends _i19.RootStackRouter { /// generated route for /// [_i1.HomePage] -class HomeRoute extends _i19.PageRouteInfo { - const HomeRoute({List<_i19.PageRouteInfo>? children}) +class HomeRoute extends _i20.PageRouteInfo { + const HomeRoute({List<_i20.PageRouteInfo>? children}) : super( HomeRoute.name, path: '/', @@ -331,7 +344,7 @@ class HomeRoute extends _i19.PageRouteInfo { /// generated route for /// [_i2.OnBoardingPage] -class OnBoardingRoute extends _i19.PageRouteInfo { +class OnBoardingRoute extends _i20.PageRouteInfo { const OnBoardingRoute() : super( OnBoardingRoute.name, @@ -343,10 +356,10 @@ class OnBoardingRoute extends _i19.PageRouteInfo { /// generated route for /// [_i3.StoriesWrapper] -class StoriesWrapperRoute extends _i19.PageRouteInfo { +class StoriesWrapperRoute extends _i20.PageRouteInfo { StoriesWrapperRoute({ - _i20.Key? key, - required List<_i22.Story> stories, + _i21.Key? key, + required List<_i23.Story> stories, required int storyIndex, }) : super( StoriesWrapperRoute.name, @@ -368,9 +381,9 @@ class StoriesWrapperRouteArgs { required this.storyIndex, }); - final _i20.Key? key; + final _i21.Key? key; - final List<_i22.Story> stories; + final List<_i23.Story> stories; final int storyIndex; @@ -382,8 +395,8 @@ class StoriesWrapperRouteArgs { /// generated route for /// [_i4.EmptyRouterPage] -class ScheduleRouter extends _i19.PageRouteInfo { - const ScheduleRouter({List<_i19.PageRouteInfo>? children}) +class ScheduleRouter extends _i20.PageRouteInfo { + const ScheduleRouter({List<_i20.PageRouteInfo>? children}) : super( ScheduleRouter.name, path: 'schedule', @@ -395,8 +408,8 @@ class ScheduleRouter extends _i19.PageRouteInfo { /// generated route for /// [_i4.EmptyRouterPage] -class NewsRouter extends _i19.PageRouteInfo { - const NewsRouter({List<_i19.PageRouteInfo>? children}) +class NewsRouter extends _i20.PageRouteInfo { + const NewsRouter({List<_i20.PageRouteInfo>? children}) : super( NewsRouter.name, path: 'news', @@ -408,7 +421,7 @@ class NewsRouter extends _i19.PageRouteInfo { /// generated route for /// [_i5.MapPage] -class MapRoute extends _i19.PageRouteInfo { +class MapRoute extends _i20.PageRouteInfo { const MapRoute() : super( MapRoute.name, @@ -420,8 +433,8 @@ class MapRoute extends _i19.PageRouteInfo { /// generated route for /// [_i4.EmptyRouterPage] -class ProfileRouter extends _i19.PageRouteInfo { - const ProfileRouter({List<_i19.PageRouteInfo>? children}) +class ProfileRouter extends _i20.PageRouteInfo { + const ProfileRouter({List<_i20.PageRouteInfo>? children}) : super( ProfileRouter.name, path: 'profile', @@ -433,7 +446,7 @@ class ProfileRouter extends _i19.PageRouteInfo { /// generated route for /// [_i6.SchedulePage] -class ScheduleRoute extends _i19.PageRouteInfo { +class ScheduleRoute extends _i20.PageRouteInfo { const ScheduleRoute() : super( ScheduleRoute.name, @@ -445,7 +458,7 @@ class ScheduleRoute extends _i19.PageRouteInfo { /// generated route for /// [_i7.GroupsSelectPage] -class GroupsSelectRoute extends _i19.PageRouteInfo { +class GroupsSelectRoute extends _i20.PageRouteInfo { const GroupsSelectRoute() : super( GroupsSelectRoute.name, @@ -457,7 +470,7 @@ class GroupsSelectRoute extends _i19.PageRouteInfo { /// generated route for /// [_i8.NewsPage] -class NewsRoute extends _i19.PageRouteInfo { +class NewsRoute extends _i20.PageRouteInfo { const NewsRoute() : super( NewsRoute.name, @@ -469,10 +482,10 @@ class NewsRoute extends _i19.PageRouteInfo { /// generated route for /// [_i9.NewsDetailsPage] -class NewsDetailsRoute extends _i19.PageRouteInfo { +class NewsDetailsRoute extends _i20.PageRouteInfo { NewsDetailsRoute({ - _i20.Key? key, - required _i23.NewsItem newsItem, + _i21.Key? key, + required _i24.NewsItem newsItem, }) : super( NewsDetailsRoute.name, path: 'details', @@ -491,9 +504,9 @@ class NewsDetailsRouteArgs { required this.newsItem, }); - final _i20.Key? key; + final _i21.Key? key; - final _i23.NewsItem newsItem; + final _i24.NewsItem newsItem; @override String toString() { @@ -503,7 +516,7 @@ class NewsDetailsRouteArgs { /// generated route for /// [_i10.ProfilePage] -class ProfileRoute extends _i19.PageRouteInfo { +class ProfileRoute extends _i20.PageRouteInfo { const ProfileRoute() : super( ProfileRoute.name, @@ -515,7 +528,7 @@ class ProfileRoute extends _i19.PageRouteInfo { /// generated route for /// [_i11.LoginPage] -class LoginRoute extends _i19.PageRouteInfo { +class LoginRoute extends _i20.PageRouteInfo { const LoginRoute() : super( LoginRoute.name, @@ -527,7 +540,7 @@ class LoginRoute extends _i19.PageRouteInfo { /// generated route for /// [_i12.AboutAppPage] -class AboutAppRoute extends _i19.PageRouteInfo { +class AboutAppRoute extends _i20.PageRouteInfo { const AboutAppRoute() : super( AboutAppRoute.name, @@ -539,7 +552,7 @@ class AboutAppRoute extends _i19.PageRouteInfo { /// generated route for /// [_i13.ProfileAnnouncesPage] -class ProfileAnnouncesRoute extends _i19.PageRouteInfo { +class ProfileAnnouncesRoute extends _i20.PageRouteInfo { const ProfileAnnouncesRoute() : super( ProfileAnnouncesRoute.name, @@ -551,7 +564,7 @@ class ProfileAnnouncesRoute extends _i19.PageRouteInfo { /// generated route for /// [_i14.ProfileAttendancePage] -class ProfileAttendanceRoute extends _i19.PageRouteInfo { +class ProfileAttendanceRoute extends _i20.PageRouteInfo { const ProfileAttendanceRoute() : super( ProfileAttendanceRoute.name, @@ -563,10 +576,10 @@ class ProfileAttendanceRoute extends _i19.PageRouteInfo { /// generated route for /// [_i15.ProfileDetailPage] -class ProfileDetailRoute extends _i19.PageRouteInfo { +class ProfileDetailRoute extends _i20.PageRouteInfo { ProfileDetailRoute({ - _i20.Key? key, - required _i24.User user, + _i21.Key? key, + required _i25.User user, }) : super( ProfileDetailRoute.name, path: 'details', @@ -585,9 +598,9 @@ class ProfileDetailRouteArgs { required this.user, }); - final _i20.Key? key; + final _i21.Key? key; - final _i24.User user; + final _i25.User user; @override String toString() { @@ -597,7 +610,7 @@ class ProfileDetailRouteArgs { /// generated route for /// [_i16.ProfileLectrosPage] -class ProfileLectrosRoute extends _i19.PageRouteInfo { +class ProfileLectrosRoute extends _i20.PageRouteInfo { const ProfileLectrosRoute() : super( ProfileLectrosRoute.name, @@ -609,7 +622,7 @@ class ProfileLectrosRoute extends _i19.PageRouteInfo { /// generated route for /// [_i17.ProfileScoresPage] -class ProfileScoresRoute extends _i19.PageRouteInfo { +class ProfileScoresRoute extends _i20.PageRouteInfo { const ProfileScoresRoute() : super( ProfileScoresRoute.name, @@ -621,7 +634,7 @@ class ProfileScoresRoute extends _i19.PageRouteInfo { /// generated route for /// [_i18.ProfileSettingsPage] -class ProfileSettingsRoute extends _i19.PageRouteInfo { +class ProfileSettingsRoute extends _i20.PageRouteInfo { const ProfileSettingsRoute() : super( ProfileSettingsRoute.name, @@ -630,3 +643,15 @@ class ProfileSettingsRoute extends _i19.PageRouteInfo { static const String name = 'ProfileSettingsRoute'; } + +/// generated route for +/// [_i19.ProfileNfcPassPage] +class ProfileNfcPassRoute extends _i20.PageRouteInfo { + const ProfileNfcPassRoute() + : super( + ProfileNfcPassRoute.name, + path: 'nfc-pass', + ); + + static const String name = 'ProfileNfcPassRoute'; +} diff --git a/lib/presentation/pages/login/login_page.dart b/lib/presentation/pages/login/login_page.dart index fb9a40fc..c1fc21b4 100644 --- a/lib/presentation/pages/login/login_page.dart +++ b/lib/presentation/pages/login/login_page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/forms/labelled_input.dart'; @@ -23,13 +23,15 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: BlocListener( + body: BlocConsumer( listener: (context, state) { - if (state is LogInSuccess) { - context.router.pop(); - } + state.whenOrNull( + logInSuccess: (st) { + context.router.pop(); + }, + ); }, - child: SafeArea( + builder: (context, state) => SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), @@ -75,26 +77,22 @@ class _LoginPageState extends State { ], ), ), - BlocBuilder(builder: (context, state) { - if (state is LogInError && state.cause != '') { - return Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - state.cause, - style: AppTextStyle.bodyRegular - .copyWith(color: AppTheme.colors.colorful07), - ), - ); - } - return Container(); - }), + state.maybeMap( + logInError: (st) => Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + st.cause, + style: AppTextStyle.bodyRegular + .copyWith(color: AppTheme.colors.colorful07), + ), + ), + orElse: () => Container(), + ), const SizedBox(height: 40), PrimaryButton( text: 'Войти', onClick: () { - context.read().add(AuthLogInEvent( - login: _emailController.text, - password: _passController.text)); + context.read().add(const UserEvent.logIn()); }, ), ], diff --git a/lib/presentation/pages/map/map_page.dart b/lib/presentation/pages/map/map_page.dart index 837321b4..76c2f9a9 100644 --- a/lib/presentation/pages/map/map_page.dart +++ b/lib/presentation/pages/map/map_page.dart @@ -77,7 +77,6 @@ class _MapPageState extends State { // TODO: implement search bar without using [ImplicitlyAnimatedList]. // Package implicitly_animated_reorderable_list is DISCONTINUED and // project compilation fails because of it. - // Widget _buildSearchBar() { // return FloatingSearchBar( // accentColor: AppTheme.colors.primary, diff --git a/lib/presentation/pages/profile/profile_attendance_page.dart b/lib/presentation/pages/profile/profile_attendance_page.dart index 44dd8745..4cdf05aa 100644 --- a/lib/presentation/pages/profile/profile_attendance_page.dart +++ b/lib/presentation/pages/profile/profile_attendance_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/attendance_bloc/attendance_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/widgets/attendance_card.dart'; import 'package:intl/intl.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/select_range_date_button.dart'; @@ -42,14 +42,14 @@ class _ProfileAttendancePageState extends State { ), body: SafeArea( bottom: false, - child: BlocBuilder( - builder: (context, authState) { - if (authState is LogInSuccess) { - return BlocBuilder( + child: BlocBuilder( + builder: (context, userState) { + return userState.maybeMap( + logInSuccess: (value) => + BlocBuilder( builder: (context, state) { if (state is AttendanceInitial) { context.read().add(LoadAttendance( - token: authState.token, startDate: _getTextDates(_getFirstAndLastWeekDaysText())[0], endDate: @@ -79,7 +79,6 @@ class _ProfileAttendancePageState extends State { onDateSelected: (date) { context.read().add( LoadAttendance( - token: authState.token, startDate: _getTextDates(date)[0], endDate: _getTextDates(date)[1]), ); @@ -135,9 +134,9 @@ class _ProfileAttendancePageState extends State { ], ); }, - ); - } - return Container(); + ), + orElse: () => const Center(child: Text("Ошибка")), + ); }, ), ), diff --git a/lib/presentation/pages/profile/profile_lectors_page.dart b/lib/presentation/pages/profile/profile_lectors_page.dart index d1f4fa0c..7df99b87 100644 --- a/lib/presentation/pages/profile/profile_lectors_page.dart +++ b/lib/presentation/pages/profile/profile_lectors_page.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:material_floating_search_bar/material_floating_search_bar.dart'; -import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/employee_bloc/employee_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/widgets/lector_search_card.dart'; import 'package:rtu_mirea_app/presentation/typography.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; @@ -44,10 +44,10 @@ class _ProfileLectrosPageState extends State { backgroundColor: AppTheme.colors.background01, body: SafeArea( bottom: false, - child: BlocBuilder( - builder: (context, authState) { - if (authState is LogInSuccess) { - return BlocBuilder( + child: BlocBuilder( + builder: (context, userState) { + return userState.maybeMap( + logInSuccess: (value) => BlocBuilder( builder: (context, state) { return FloatingSearchBar( key: _searchBarKey, @@ -84,8 +84,9 @@ class _ProfileLectrosPageState extends State { progress: state is EmployeeLoading, onQueryChanged: (query) { if (query.length > 2) { - context.read().add( - LoadEmployees(token: authState.token, name: query)); + context + .read() + .add(LoadEmployees(name: query)); } }, transition: CircularFloatingSearchBarTransition(), @@ -126,9 +127,9 @@ class _ProfileLectrosPageState extends State { }, ); }, - ); - } - return Container(); + ), + orElse: () => Container(), + ); }, ), ), diff --git a/lib/presentation/pages/profile/profile_nfc_pass_page.dart b/lib/presentation/pages/profile/profile_nfc_pass_page.dart new file mode 100644 index 00000000..779598d1 --- /dev/null +++ b/lib/presentation/pages/profile/profile_nfc_pass_page.dart @@ -0,0 +1,695 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get/get.dart'; +import 'package:rtu_mirea_app/domain/entities/nfc_pass.dart'; +import 'package:rtu_mirea_app/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.dart'; + +import 'package:rtu_mirea_app/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; +import 'package:rtu_mirea_app/presentation/widgets/buttons/colorful_button.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_button.dart'; + +import '../../widgets/buttons/text_outlined_button.dart'; +import 'package:rtu_mirea_app/presentation/typography.dart'; +import 'package:rtu_mirea_app/presentation/theme.dart'; +import 'package:app_settings/app_settings.dart'; + +import 'package:flutter/cupertino.dart'; + +class ProfileNfcPassPage extends StatefulWidget { + const ProfileNfcPassPage({Key? key}) : super(key: key); + + @override + State createState() => _ProfileNfcPageState(); +} + +class _ProfileNfcPageState extends State { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + + void _showSnackBarLoadInfo() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Загружаем пропуск в устройство"), + ), + ); + } + + bool _showMyDevices = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("NFC-пропуск"), + ), + body: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: BlocBuilder( + builder: (context, state) { + state.whenOrNull( + logInSuccess: (user) => context + .read() + .add(const NfcPassEvent.started())); + + return state.maybeMap( + logInSuccess: (state) { + final student = state.user; + return ListView( + children: [ + const SizedBox(height: 24), + FutureBuilder( + future: deviceInfo.androidInfo, + builder: (context, snapshot) { + if (snapshot.hasData) { + if (snapshot.data == null) { + return const _ErrorAndroidDataFetch(); + } + return BlocConsumer( + listener: (context, state) { + state.whenOrNull( + loaded: (nfcPasses) { + if (!NfcPassBloc.isNfcFetched) { + // If current device is not connected to + // any nfc pass + if (!nfcPasses.any((element) => + element.connected && + element.deviceId == + snapshot.data!.id)) { + return; + } + + _showSnackBarLoadInfo(); + context.read().add( + const NfcPassEvent.fetchNfcCode()); + } + }, + ); + }, + builder: (context, state) => state.map( + initial: (_) { + context.read().add( + NfcPassEvent.getNfcPasses( + student.code, + student.studentId, + snapshot.data!.id, + ), + ); + return const Center( + child: CircularProgressIndicator(), + ); + }, + loading: (_) => const Center( + child: CircularProgressIndicator(), + ), + loaded: (state) { + final connectedDevice = state.nfcPasses + .firstWhereOrNull((element) => + element.connected && + element.deviceId == + snapshot.data!.id); + + if (state.nfcPasses.isEmpty) { + return _NfcNotConnected( + onPressed: () => + context.read().add( + NfcPassEvent.connectNfcPass( + student.code, + student.studentId, + snapshot.data!.id, + snapshot.data!.model, + ), + ), + ); + } + if (!_showMyDevices && + connectedDevice != null) { + return _NfcPassCard( + deviceId: connectedDevice.deviceId, + deviceName: connectedDevice.deviceName, + onClick: () { + setState(() { + _showMyDevices = true; + }); + }, + ); + } else { + return Column( + children: [ + Text( + "Подключенные устройства", + style: AppTextStyle.title, + ), + const SizedBox(height: 16), + // Сначала подключенные устройства + for (final nfcPass in state.nfcPasses) + if (nfcPass.connected) + _NfcPassDeviceCard( + nfcPass: nfcPass, + ), + // Потом остальные + for (final nfcPass in state.nfcPasses) + if (!nfcPass.connected) + _NfcPassDeviceCard( + nfcPass: nfcPass, + ), + const SizedBox(height: 16), + // Если нет ни одного подключенного или подключенное устройство не текущее + if (state.nfcPasses.every((element) => + !element.connected) || + state.nfcPasses.any((element) => + element.connected && + element.deviceId != + snapshot.data!.id)) + ColorfulButton( + text: + "Привязать пропуск к этому устройству", + backgroundColor: + AppTheme.colors.primary, + onClick: () { + context.read().add( + NfcPassEvent.connectNfcPass( + student.code, + student.studentId, + snapshot.data!.id, + snapshot.data!.model, + ), + ); + }, + ), + ], + ); + } + }, + nfcDisabled: (_) => + const _NfcNotAviable(disabled: true), + nfcNotSupported: (_) => + const _NfcNotAviable(disabled: false), + error: (st) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Произошла ошибка:", + style: AppTextStyle.titleM, + ), + const SizedBox(height: 16), + Text( + st.cause, + style: AppTextStyle.body, + ), + ], + ), + ), + nfcNotExist: (_) => BlocBuilder( + builder: (context, state) => state.map( + initial: (_) { + final fullName = + "${student.name} ${student.secondName.replaceAll(" ", "").isNotEmpty ? "${student.secondName} " : ""}${student.lastName}"; + + return _NfcPassNotExistOnAccount( + onClick: () => context + .read() + .add( + NfcFeedbackEvent.sendFeedback( + fullName: fullName, + group: student.academicGroup, + personalNumber: + student.personalNumber, + studentId: + student.id.toString(), + ), + ), + fullName: fullName, + personalNumber: student.personalNumber, + ); + }, + loading: (_) => const Center( + child: CircularProgressIndicator(), + ), + success: (state) => + // Сообщение о том что заявка на привязку пропуска отправлена + Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Icon( + Icons.check_circle, + color: AppTheme.colors.colorful04, + size: 48, + ), + const SizedBox(height: 16), + Text( + "Заявка на привязку пропуска отправлена", + style: AppTextStyle.title, + ), + const SizedBox(height: 16), + Text( + "Подождите, пока администратор подтвердит вашу заявку. " + "Время ожидания может занять до 7 рабочих дней", + style: AppTextStyle.body, + ), + ], + ), + failure: (st) => Center( + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Text( + "Произошла ошибка:", + style: AppTextStyle.titleM, + ), + const SizedBox(height: 16), + Text( + st.message, + style: AppTextStyle.body, + ), + ], + ), + ), + ), + ), + ), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ) + ], + ); + }, + orElse: () => const Center( + child: CircularProgressIndicator(), + ), + ); + }, + ), + ), + ), + ); + } +} + +class _NfcPassCard extends StatelessWidget { + const _NfcPassCard( + {Key? key, + required this.deviceId, + required this.deviceName, + required this.onClick}) + : super(key: key); + + final String deviceId; + final String deviceName; + final VoidCallback onClick; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 16), + Text( + "Приложите телефон\nк турникету", + style: AppTextStyle.titleM, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Container( + width: 200, + height: 300, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: AppTheme.colors.active, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.25), + offset: const Offset(0, 4), + blurRadius: 4, + ), + ], + ), + child: Stack( + children: [ + Positioned( + top: 16, + left: 16, + child: Image.asset( + "assets/icons/gerb.ico", + width: 32, + height: 32, + ), + ), + Positioned( + top: 64, + left: 0, + right: 0, + child: Align( + alignment: Alignment.center, + child: Transform.rotate( + angle: pi / 2, + child: Icon( + CupertinoIcons.radiowaves_left, + color: AppTheme.colors.background02, + size: 80, + ), + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + right: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "ID", + style: AppTextStyle.titleM.copyWith( + color: AppTheme.colors.background01, + fontWeight: FontWeight.w700), + ), + const SizedBox(height: 4), + Text( + deviceId, + style: AppTextStyle.titleM.copyWith( + color: AppTheme.colors.background01, + fontSize: 14, + fontWeight: FontWeight.w400), + ), + const SizedBox(height: 16), + // device + Text( + "Устройство", + style: AppTextStyle.titleM.copyWith( + color: AppTheme.colors.background01, + fontWeight: FontWeight.w700), + ), + const SizedBox(height: 4), + Text( + deviceName, + style: AppTextStyle.titleM.copyWith( + color: AppTheme.colors.background01, + fontSize: 14, + fontWeight: FontWeight.w400), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 48), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(top: 4), + width: 24, + height: 24, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: AppTheme.colors.colorful04, + ), + child: Icon( + CupertinoIcons.checkmark_alt, + size: 16, + color: AppTheme.colors.background01, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Это устройство зарегистрировано как основное", + style: AppTextStyle.titleM), + const SizedBox(height: 4), + Text( + "Пропуск работает только на одном устройстве. " + "При входе на другом устройстве, пропуск на этом будет " + "отключен!", + style: AppTextStyle.body, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 48), + PrimaryButton( + onClick: onClick, + text: "Мои устройства", + ), + ], + ); + } +} + +class _NfcPassNotExistOnAccount extends StatelessWidget { + const _NfcPassNotExistOnAccount({ + Key? key, + required this.onClick, + required this.fullName, + required this.personalNumber, + }) : super(key: key); + + final VoidCallback onClick; + final String fullName; + final String personalNumber; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text( + "Пропуск не привязан к вашей учетной записи", + style: AppTextStyle.title, + ), + const SizedBox(height: 16), + Text( + "Ваш NFC-пропуск не привязан к вашей учетной записи и не может быть использован в данный момент.\n\n" + "Чтобы привязать пропуск к вашей учетной записи, необходимо оставить заявку.\n\n" + "Время обработки - до 7 рабочих дней, после чего ваш NFC-пропуск будет автоматически активирован, " + " а эта ошибка больше не будет отображаться.", + style: AppTextStyle.body, + ), + const SizedBox(height: 36), + Text( + "Информация о заявке", + style: AppTextStyle.titleM, + ), + const SizedBox(height: 8), + Text( + "Имя: $fullName", + style: AppTextStyle.bodyL, + ), + const SizedBox(height: 4), + Text( + "Персональный номер: $personalNumber", + style: AppTextStyle.bodyL, + ), + const SizedBox(height: 16), + ColorfulButton( + text: "Оставить заявку", + backgroundColor: AppTheme.colors.primary, + onClick: onClick, + ), + ], + ); + } +} + +// Виджет с иноформацией о том, что пропуск ещё не подключен ни к одному устройству +class _NfcNotConnected extends StatelessWidget { + const _NfcNotConnected({ + Key? key, + required this.onPressed, + }) : super(key: key); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 16), + Text( + "Пропуск не подключен", + style: AppTextStyle.titleM, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + "Ваш NFC-пропуск ещё не подключен ни к одному устройству и не может быть использован. Чтобы подключить пропуск, нажмите на кнопку ниже.", + style: AppTextStyle.body, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ColorfulButton( + text: "Подключить пропуск", + onClick: onPressed, + backgroundColor: AppTheme.colors.primary, + ), + ], + ); + } +} + +class _ErrorAndroidDataFetch extends StatelessWidget { + const _ErrorAndroidDataFetch({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 16), + Text( + "Не удалось получить данные об устройстве", + style: AppTextStyle.titleM, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + "Приложение не может получить данные об устройстве. Попробуйте перезагрузить приложение или предоставить приложению нужные разрешения.", + style: AppTextStyle.bodyL, + textAlign: TextAlign.center, + ), + ], + ); + } +} + +class _NfcPassDeviceCard extends StatelessWidget { + const _NfcPassDeviceCard({ + Key? key, + required this.nfcPass, + }) : super(key: key); + + final NfcPass nfcPass; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Card( + color: AppTheme.colors.background02, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + margin: const EdgeInsets.all(0), + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.nfc), + const SizedBox(width: 8), + Text( + "NFC-пропуск", + style: AppTextStyle.titleS, + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + "ID: ", + style: AppTextStyle.body, + ), + Text( + nfcPass.deviceId, + style: AppTextStyle.body, + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + "Модель: ", + style: AppTextStyle.body, + ), + Text( + nfcPass.deviceName, + style: AppTextStyle.body, + ), + ], + ), + const SizedBox(height: 8), + if (nfcPass.connected) + Row( + children: [ + Icon( + Icons.check_circle, + color: AppTheme.colors.colorful04, + ), + const SizedBox(width: 8), + Text( + "Это устройство подключено к пропуску", + style: AppTextStyle.body, + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +class _NfcNotAviable extends StatelessWidget { + const _NfcNotAviable({Key? key, required this.disabled}) : super(key: key); + + final bool disabled; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Icon( + Icons.error, + color: AppTheme.colors.colorful07, + size: 64, + ), + const SizedBox(height: 16), + Text( + disabled ? "NFC отключено" : "Ваше устройство не поддерживает NFC", + style: AppTextStyle.titleM, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + disabled + ? "Включите NFC в настройках вашего устройства" + : "Ваше устройство не поддерживает NFC. Пропуск по NFC недоступен", + style: AppTextStyle.bodyL, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + if (disabled) + TextOutlinedButton( + content: "Включить NFC", + onPressed: () { + AppSettings.openNFCSettings(); + }, + ), + ], + ); + } +} diff --git a/lib/presentation/pages/profile/profile_page.dart b/lib/presentation/pages/profile/profile_page.dart index 1a3a6963..0c50a2a3 100644 --- a/lib/presentation/pages/profile/profile_page.dart +++ b/lib/presentation/pages/profile/profile_page.dart @@ -1,7 +1,10 @@ +import 'dart:io'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; +import 'package:rtu_mirea_app/domain/entities/user.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/colorful_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/icon_button.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/settings_button.dart'; @@ -9,7 +12,6 @@ import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../bloc/announces_bloc/announces_bloc.dart'; -import '../../bloc/profile_bloc/profile_bloc.dart'; import '../../widgets/buttons/text_outlined_button.dart'; import '../../widgets/container_label.dart'; import 'package:rtu_mirea_app/presentation/typography.dart'; @@ -37,161 +39,21 @@ class _ProfilePageState extends State { return SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - // return const _InitialProfileStatePage(); - - if (state is LogInSuccess) { - context - .read() - .add(ProfileGetUserData(state.token)); - return BlocBuilder( - builder: (context, profileState) { - if (profileState is ProfileLoaded) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CircleAvatar( - radius: 68, - backgroundImage: Image.network( - 'https://lk.mirea.ru${profileState.user.photoUrl}') - .image, - ), - Padding( - padding: - const EdgeInsets.only(top: 13, bottom: 4), - child: Text( - '${profileState.user.name} ${profileState.user.lastName}', - style: AppTextStyle.h5, - ), - ), - ShaderMask( - shaderCallback: (bounds) => - AppTheme.colors.gradient07.createShader( - Rect.fromLTWH( - 0, 0, bounds.width, bounds.height), - ), - child: Text( - profileState.user.login, - style: AppTextStyle.titleS, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextOutlinedButton( - width: 160, - content: "Профиль", - onPressed: () => context.router.push( - ProfileDetailRoute( - user: profileState.user), - ), - ), - const SizedBox(width: 12), - SizedBox( - width: 146, - height: 45, - child: SocialIconButton( - assetImage: const AssetImage( - 'assets/icons/gerb.ico'), - onClick: () { - launchUrlString( - profileState.user.authShortlink != - null - ? "https://lk.mirea.ru/auth/link/?url=${profileState.user.authShortlink!}" - : "https://lk.mirea.ru/auth", - mode: LaunchMode - .externalApplication); - }, - text: "Вход в ЛКС", - ), - ), - ]), - - const SizedBox(height: 40), - const ContainerLabel(label: "Информация"), - const SizedBox(height: 20), - SettingsButton( - text: 'Объявления', - icon: Icons.message_rounded, - onClick: () { - context - .read() - .add(LoadAnnounces(token: state.token)); - context.router.push( - const ProfileAnnouncesRoute(), - ); - }), - // const SizedBox(height: 8), - // SettingsButton( - // text: 'Адреса', - // icon: Icons.map_rounded, - // onClick: () {}), - const SizedBox(height: 8), - SettingsButton( - text: 'Преподаватели', - icon: Icons.people_alt_rounded, - onClick: () => context.router - .push(const ProfileLectrosRoute()), - ), - const SizedBox(height: 8), - SettingsButton( - text: 'Посещения', - icon: Icons.access_time_rounded, - onClick: () => context.router - .push(const ProfileAttendanceRoute()), - ), - const SizedBox(height: 8), - SettingsButton( - text: 'Зачетная книжка', - icon: Icons.menu_book_rounded, - onClick: () => context.router - .push(const ProfileScoresRoute())), - const SizedBox(height: 8), - SettingsButton( - text: 'О приложении', - icon: Icons.apps_rounded, - onClick: () => - context.router.push(const AboutAppRoute()), - ), - const SizedBox(height: 8), - SettingsButton( - text: 'Настройки', - icon: Icons.settings_rounded, - onClick: () => { - context.router - .push(const ProfileSettingsRoute()), - }), - const SizedBox(height: 8), - ColorfulButton( - text: 'Выйти', - onClick: () => context - .read() - .add(AuthLogOut()), - backgroundColor: AppTheme.colors.colorful07), - ], - ); - } else if (profileState is ProfileLoading) { - return SizedBox( - height: MediaQuery.of(context).size.height, - child: const Center( - child: CircularProgressIndicator())); - } - return Container(); - }); - } else if (state is LogInError || - state is AuthUnauthorized) { - return const _InitialProfileStatePage(); - } else if (state is AuthUnknown) { - return ConstrainedBox( + return state.map( + unauthorized: (_) => const _InitialProfileStatePage(), + loading: (_) => ConstrainedBox( constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight, ), - child: const Center(child: CircularProgressIndicator()), - ); - } - return Container(); + child: const Center( + child: CircularProgressIndicator(), + ), + ), + logInError: (st) => const _InitialProfileStatePage(), + logInSuccess: (st) => _UserLoggedInView(user: st.user), + ); }, ), ), @@ -203,6 +65,134 @@ class _ProfilePageState extends State { } } +class _UserLoggedInView extends StatelessWidget { + const _UserLoggedInView({Key? key, required this.user}) : super(key: key); + + final User user; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircleAvatar( + radius: 68, + backgroundImage: + Image.network('https://lk.mirea.ru${user.photoUrl}').image, + ), + Padding( + padding: const EdgeInsets.only(top: 13, bottom: 4), + child: Text( + '${user.name} ${user.lastName}', + style: AppTextStyle.h5, + ), + ), + ShaderMask( + shaderCallback: (bounds) => AppTheme.colors.gradient07.createShader( + Rect.fromLTWH(0, 0, bounds.width, bounds.height), + ), + child: Text( + user.login, + style: AppTextStyle.titleS, + ), + ), + const SizedBox(height: 12), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + TextOutlinedButton( + width: 160, + content: "Профиль", + onPressed: () => context.router.push( + ProfileDetailRoute(user: user), + ), + ), + const SizedBox(width: 12), + SizedBox( + width: 146, + height: 45, + child: SocialIconButton( + assetImage: const AssetImage('assets/icons/gerb.ico'), + onClick: () { + launchUrlString( + user.authShortlink != null + ? "https://lk.mirea.ru/auth/link/?url=${user.authShortlink!}" + : "https://lk.mirea.ru/auth", + mode: LaunchMode.externalApplication); + }, + text: "Вход в ЛКС", + ), + ), + ]), + + const SizedBox(height: 40), + const ContainerLabel(label: "Информация"), + const SizedBox(height: 20), + SettingsButton( + text: 'Объявления', + icon: Icons.message_rounded, + onClick: () { + context.read().add(const LoadAnnounces()); + context.router.push( + const ProfileAnnouncesRoute(), + ); + }), + // const SizedBox(height: 8), + // SettingsButton( + // text: 'Адреса', + // icon: Icons.map_rounded, + // onClick: () {}), + const SizedBox(height: 8), + SettingsButton( + text: 'Преподаватели', + icon: Icons.people_alt_rounded, + onClick: () => context.router.push(const ProfileLectrosRoute()), + ), + const SizedBox(height: 8), + SettingsButton( + text: 'Посещения', + icon: Icons.access_time_rounded, + onClick: () => context.router.push(const ProfileAttendanceRoute()), + ), + const SizedBox(height: 8), + SettingsButton( + text: 'Зачетная книжка', + icon: Icons.menu_book_rounded, + onClick: () => context.router.push(const ProfileScoresRoute())), + const SizedBox(height: 8), + SettingsButton( + text: 'О приложении', + icon: Icons.apps_rounded, + onClick: () => context.router.push(const AboutAppRoute()), + ), + + // Display only for android devices because of + // NFC support only for android + if (Platform.isAndroid) ...[ + const SizedBox(height: 8), + SettingsButton( + text: 'NFC пропуск', + icon: Icons.nfc_rounded, + onClick: () => context.router.push(const ProfileNfcPassRoute()), + ), + ], + + const SizedBox(height: 8), + SettingsButton( + text: 'Настройки', + icon: Icons.settings_rounded, + onClick: () => { + context.router.push(const ProfileSettingsRoute()), + }), + const SizedBox(height: 8), + ColorfulButton( + text: 'Выйти', + onClick: () => + context.read().add(const UserEvent.logOut()), + backgroundColor: AppTheme.colors.colorful07), + ], + ); + } +} + class _InitialProfileStatePage extends StatelessWidget { const _InitialProfileStatePage({Key? key}) : super(key: key); @@ -217,8 +207,7 @@ class _InitialProfileStatePage extends StatelessWidget { // вместо того, чтобы открывать страницу с логином и паролем, // мы просто вызываем событие авторизации, которое откроет // страницу авторизации в браузере. - context.read().add( - const AuthLogInEvent(login: 'login', password: 'password')); + context.read().add(const UserEvent.logIn()); // Страница с вводом логина и пароля: // context.router.push(const LoginRoute()); diff --git a/lib/presentation/pages/profile/profile_scores_page.dart b/lib/presentation/pages/profile/profile_scores_page.dart index bd4bc252..d2908c27 100644 --- a/lib/presentation/pages/profile/profile_scores_page.dart +++ b/lib/presentation/pages/profile/profile_scores_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:rtu_mirea_app/domain/entities/score.dart'; -import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:rtu_mirea_app/presentation/pages/profile/widgets/scores_chart_modal.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:rtu_mirea_app/presentation/widgets/buttons/primary_tab_button.dart'; @@ -53,15 +53,13 @@ class _ProfileScoresPageState extends State { backgroundColor: AppTheme.colors.background01, body: SafeArea( bottom: false, - child: BlocBuilder( - builder: (context, authState) { - if (authState is LogInSuccess) { - return BlocBuilder( + child: BlocBuilder( + builder: (context, userState) { + return userState.maybeMap( + logInSuccess: (_) => BlocBuilder( builder: (context, state) { if (state is ScoresInitial) { - context - .read() - .add(LoadScores(token: authState.token)); + context.read().add(const LoadScores()); } else if (state is ScoresLoaded) { _tabValueNotifier.value = state.scores.keys .toList() @@ -153,9 +151,9 @@ class _ProfileScoresPageState extends State { } return Container(); }, - ); - } - return Container(); + ), + orElse: () => Container(), + ); }, ), ), diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 5e076bca..ea80cae4 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -1,3 +1,4 @@ +import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:get_it/get_it.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -27,7 +28,9 @@ import 'package:rtu_mirea_app/domain/repositories/github_repository.dart'; import 'package:rtu_mirea_app/domain/repositories/schedule_repository.dart'; import 'package:rtu_mirea_app/domain/repositories/strapi_repository.dart'; import 'package:rtu_mirea_app/domain/repositories/user_repository.dart'; +import 'package:rtu_mirea_app/domain/usecases/connect_nfc_pass.dart'; import 'package:rtu_mirea_app/domain/usecases/delete_schedule.dart'; +import 'package:rtu_mirea_app/domain/usecases/fetch_nfc_code.dart'; import 'package:rtu_mirea_app/domain/usecases/get_active_group.dart'; import 'package:rtu_mirea_app/domain/usecases/get_announces.dart'; import 'package:rtu_mirea_app/domain/usecases/get_app_settings.dart'; @@ -39,6 +42,7 @@ import 'package:rtu_mirea_app/domain/usecases/get_employees.dart'; import 'package:rtu_mirea_app/domain/usecases/get_groups.dart'; import 'package:rtu_mirea_app/domain/usecases/get_news.dart'; import 'package:rtu_mirea_app/domain/usecases/get_news_tags.dart'; +import 'package:rtu_mirea_app/domain/usecases/get_nfc_passes.dart'; import 'package:rtu_mirea_app/domain/usecases/get_patrons.dart'; import 'package:rtu_mirea_app/domain/usecases/get_schedule.dart'; import 'package:rtu_mirea_app/domain/usecases/get_schedule_settings.dart'; @@ -48,6 +52,7 @@ import 'package:rtu_mirea_app/domain/usecases/get_update_info.dart'; import 'package:rtu_mirea_app/domain/usecases/get_user_data.dart'; import 'package:rtu_mirea_app/domain/usecases/log_in.dart'; import 'package:rtu_mirea_app/domain/usecases/log_out.dart'; +import 'package:rtu_mirea_app/domain/usecases/send_nfc_not_exist_feedback.dart'; import 'package:rtu_mirea_app/domain/usecases/set_active_group.dart'; import 'package:rtu_mirea_app/domain/usecases/set_app_settings.dart'; import 'package:rtu_mirea_app/domain/usecases/set_schedule_settings.dart'; @@ -56,16 +61,19 @@ import 'package:rtu_mirea_app/presentation/bloc/about_app_bloc/about_app_bloc.da import 'package:rtu_mirea_app/presentation/bloc/announces_bloc/announces_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/app_cubit/app_cubit.dart'; import 'package:rtu_mirea_app/presentation/bloc/attendance_bloc/attendance_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/auth_bloc/auth_bloc.dart'; + import 'package:rtu_mirea_app/presentation/bloc/employee_bloc/employee_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/map_cubit/map_cubit.dart'; import 'package:rtu_mirea_app/presentation/bloc/news_bloc/news_bloc.dart'; -import 'package:rtu_mirea_app/presentation/bloc/profile_bloc/profile_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/nfc_feedback_bloc/nfc_feedback_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/scores_bloc/scores_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/stories_bloc/stories_bloc.dart'; import 'package:rtu_mirea_app/presentation/bloc/update_info_bloc/update_info_bloc.dart'; +import 'package:rtu_mirea_app/presentation/bloc/user_bloc/user_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'data/repositories/schedule_repository_impl.dart'; @@ -97,14 +105,11 @@ Future setup() async { getIt.registerFactory( () => AboutAppBloc(getContributors: getIt(), getForumPatrons: getIt())); getIt.registerFactory(() => MapCubit()); - getIt.registerFactory( - () => AuthBloc( - logOut: getIt(), - getAuthToken: getIt(), - logIn: getIt(), - getUserData: getIt()), - ); - getIt.registerFactory(() => ProfileBloc(getUserData: getIt())); + getIt.registerFactory(() => UserBloc( + logIn: getIt(), + logOut: getIt(), + getUserData: getIt(), + getAuthToken: getIt())); getIt.registerFactory(() => AnnouncesBloc(getAnnounces: getIt())); getIt.registerFactory(() => EmployeeBloc(getEmployees: getIt())); getIt.registerFactory(() => ScoresBloc(getScores: getIt())); @@ -123,6 +128,16 @@ Future setup() async { setAppSettings: getIt(), ), ); + getIt.registerFactory(() => NfcPassBloc( + getNfcPasses: getIt(), + connectNfcPass: getIt(), + deviceInfo: getIt(), + fetchNfcCode: getIt(), + getAuthToken: getIt(), + getUserData: getIt(), + )); + getIt + .registerFactory(() => NfcFeedbackBloc(sendNfcNotExistFeedback: getIt())); // Usecases getIt.registerLazySingleton(() => GetStories(getIt())); @@ -149,6 +164,10 @@ Future setup() async { getIt.registerLazySingleton(() => SetScheduleSettings(getIt())); getIt.registerLazySingleton(() => SetAppSettings(getIt())); getIt.registerLazySingleton(() => GetAppSettings(getIt())); + getIt.registerLazySingleton(() => GetNfcPasses(getIt())); + getIt.registerLazySingleton(() => ConnectNfcPass(getIt())); + getIt.registerLazySingleton(() => FetchNfcCode(getIt())); + getIt.registerLazySingleton(() => SendNfcNotExistFeedback(getIt())); // Repositories getIt.registerLazySingleton( @@ -193,8 +212,8 @@ Future setup() async { localDataSource: getIt(), )); - getIt.registerLazySingleton( - () => UserLocalDataImpl(sharedPreferences: getIt())); + getIt.registerLazySingleton(() => + UserLocalDataImpl(sharedPreferences: getIt(), secureStorage: getIt())); getIt.registerLazySingleton( () => UserRemoteDataImpl(httpClient: getIt(), lksOauth2: getIt())); getIt.registerLazySingleton( @@ -223,8 +242,17 @@ Future setup() async { getIt.registerLazySingleton(() => Dio(BaseOptions(receiveTimeout: 20000))); final sharedPreferences = await SharedPreferences.getInstance(); getIt.registerLazySingleton(() => sharedPreferences); + const secureStorage = FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + )); + getIt.registerLazySingleton(() => secureStorage); getIt.registerLazySingleton(() => InternetConnectionCheckerPlus()); final PackageInfo packageInfo = await PackageInfo.fromPlatform(); getIt.registerLazySingleton(() => packageInfo); getIt.registerLazySingleton(() => LksOauth2()); + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + getIt.registerLazySingleton(() => deviceInfo); + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + getIt.registerLazySingleton(() => androidInfo); } diff --git a/pubspec.yaml b/pubspec.yaml index 9fa8dbc2..ea053662 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ publish_to: 'none' # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.3+9 +version: 1.3.0+12 environment: sdk: ">=2.19.0 <3.0.0" @@ -43,11 +43,11 @@ dependencies: # State management library. # See https://pub.dev/packages/bloc - bloc: ^8.0.2 + bloc: ^8.1.1 # Widgets that make it easy to implement the BLoC. # See https://pub.dev/packages/flutter_bloc - flutter_bloc: ^8.0.1 + flutter_bloc: ^8.1.2 # SVG rendering widget for Flutter. # See https://pub.dev/packages/flutter_svg @@ -173,12 +173,26 @@ dependencies: firebase_analytics: ^10.1.0 firebase_crashlytics: ^3.0.11 - oauth2_client: ^3.0.0 + oauth2_client: ^3.1.0 # Flutter Secure Storage provides API to store data in secure storage. # See https://pub.dev/packages/flutter_secure_storage - flutter_secure_storage: ^6.0.0 + flutter_secure_storage: ^7.0.1 + + # Flutter plugin providing detailed information about the device (make, model, etc.), + # and Android or iOS version the app is running on. + # See https://pub.dev/packages/device_info_plus + device_info_plus: ^8.0.0 + + # Provide NFC functionality on Android, iOS & Web, including reading metadata, read & write + # NDEF records, and transceive layer 3 & 4 data with NFC tags / cards + # See https://pub.dev/packages/flutter_nfc_kit + flutter_nfc_kit: ^3.3.1 + # A Flutter plugin for opening iOS and Android phone settings from an app. + # See https://pub.dev/packages/app_settings + app_settings: ^4.2.0 + dev_dependencies: # The "flutter_lints" package below contains a set of recommended lints to From 396d8ddfbecfc9a8d4bfd2f4fee0711aa194584f Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Thu, 16 Feb 2023 14:01:38 +0300 Subject: [PATCH 32/38] refactor: Change deprecated `backgroundColor` to `colorScheme` (#281) --- lib/presentation/theme.dart | 59 +++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/lib/presentation/theme.dart b/lib/presentation/theme.dart index 459551a2..8035b859 100644 --- a/lib/presentation/theme.dart +++ b/lib/presentation/theme.dart @@ -28,7 +28,6 @@ class AppTheme { displayColor: darkThemeColors.active, ), scaffoldBackgroundColor: darkThemeColors.background01, - backgroundColor: darkThemeColors.background01, appBarTheme: AppBarTheme( titleSpacing: 24, backgroundColor: darkThemeColors.background01, @@ -50,6 +49,19 @@ class AppTheme { TargetPlatform.iOS: NoShadowCupertinoPageTransitionsBuilder(), TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), }), + colorScheme: ColorScheme( + background: darkThemeColors.background01, + brightness: Brightness.dark, + primary: darkThemeColors.primary, + secondary: darkThemeColors.background02, + surface: darkThemeColors.background01, + onBackground: darkThemeColors.active, + onSurface: darkThemeColors.active, + onError: darkThemeColors.active, + onPrimary: darkThemeColors.active, + onSecondary: darkThemeColors.active, + error: darkThemeColors.colorful07, + ), ); static final lightTheme = ThemeData.light().copyWith( @@ -58,7 +70,6 @@ class AppTheme { displayColor: lightThemeColors.active, ), scaffoldBackgroundColor: lightThemeColors.background01, - backgroundColor: lightThemeColors.background01, appBarTheme: AppBarTheme( titleSpacing: 24, backgroundColor: lightThemeColors.background01, @@ -80,35 +91,19 @@ class AppTheme { TargetPlatform.iOS: NoShadowCupertinoPageTransitionsBuilder(), TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), }), - ); - - static final blackTheme = ThemeData.dark().copyWith( - textTheme: ThemeData.dark().textTheme.apply( - bodyColor: blackThemeColors.active, - displayColor: blackThemeColors.active, - ), - scaffoldBackgroundColor: blackThemeColors.background01, - backgroundColor: blackThemeColors.background01, - appBarTheme: AppBarTheme( - titleSpacing: 24, - backgroundColor: blackThemeColors.background01, - shadowColor: Colors.transparent, - titleTextStyle: AppTextStyle.title, - iconTheme: IconThemeData(color: blackThemeColors.active), + colorScheme: ColorScheme( + background: lightThemeColors.background01, + brightness: Brightness.light, + primary: lightThemeColors.primary, + secondary: lightThemeColors.background02, + surface: lightThemeColors.background01, + onBackground: lightThemeColors.active, + onSurface: lightThemeColors.active, + onError: lightThemeColors.active, + onPrimary: lightThemeColors.active, + onSecondary: lightThemeColors.active, + error: lightThemeColors.colorful07, ), - bottomNavigationBarTheme: - ThemeData.dark().bottomNavigationBarTheme.copyWith( - type: BottomNavigationBarType.shifting, - backgroundColor: blackThemeColors.background03, - selectedItemColor: blackThemeColors.active, - unselectedItemColor: blackThemeColors.deactive, - selectedLabelStyle: AppTextStyle.captionL, - unselectedLabelStyle: AppTextStyle.captionS, - ), - pageTransitionsTheme: const PageTransitionsTheme(builders: { - TargetPlatform.iOS: NoShadowCupertinoPageTransitionsBuilder(), - TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), - }), ); static ThemeData getDataByThemeType({AppThemeType? themeType}) { @@ -117,8 +112,8 @@ class AppTheme { switch (themeType) { case AppThemeType.light: return lightTheme; - case AppThemeType.black: - return blackTheme; + case AppThemeType.dark: + return darkTheme; default: return darkTheme; } From c80ba8b22289ed89afe8a07daaa0d1d555bc7dad Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Thu, 16 Feb 2023 14:13:28 +0300 Subject: [PATCH 33/38] ui: Remove Schedule bottom modal window (#282) --- lib/main.dart | 7 +-- .../pages/schedule/schedule_page.dart | 48 +++++++++++-------- lib/presentation/theme.dart | 4 +- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 60715015..dba1754b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,3 @@ - import 'package:auto_route/auto_route.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/foundation.dart'; @@ -7,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:intl/intl.dart'; + import 'package:rtu_mirea_app/common/oauth.dart'; import 'package:rtu_mirea_app/common/widget_data_init.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -32,7 +32,6 @@ import 'package:intl/intl_standalone.dart'; import 'package:rtu_mirea_app/service_locator.dart' as dependency_injection; import 'package:url_strategy/url_strategy.dart'; import 'presentation/app_notifier.dart'; -import 'package:intl/intl_browser.dart' as intl_browser; import 'service_locator.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; @@ -85,9 +84,7 @@ Future main() async { Intl.defaultLocale = 'ru_RU'; if (kIsWeb) { - await intl_browser - .findSystemLocale() - .then((value) => Intl.systemLocale = value); + Intl.systemLocale = Intl.defaultLocale!; } else { Intl.systemLocale = await findSystemLocale(); } diff --git a/lib/presentation/pages/schedule/schedule_page.dart b/lib/presentation/pages/schedule/schedule_page.dart index c1b3f3d4..9f8e3d7d 100644 --- a/lib/presentation/pages/schedule/schedule_page.dart +++ b/lib/presentation/pages/schedule/schedule_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rtu_mirea_app/domain/entities/schedule.dart'; import 'package:rtu_mirea_app/presentation/bloc/schedule_bloc/schedule_bloc.dart'; +import 'package:rtu_mirea_app/presentation/core/routes/routes.gr.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_drawer.dart'; import 'package:rtu_mirea_app/presentation/pages/schedule/widgets/schedule_settings_modal.dart'; import 'package:rtu_mirea_app/presentation/theme.dart'; @@ -30,10 +31,10 @@ class _SchedulePageState extends State { @override void dispose() { super.dispose(); - // dispose mounted modal - if (_modalShown) { - Navigator.of(context).pop(); - } + // // dispose mounted modal + // if (_modalShown) { + // Navigator.of(context).pop(); + // } } Widget _buildGroupButton( @@ -257,7 +258,9 @@ class _SchedulePageState extends State { ), ], ), - onTap: () => _showModal(isFirstRun: false), + // onTap: () => _showModal(isFirstRun: false), + onTap: () => + context.router.push(const GroupsSelectRoute()), ), ), if (state is ScheduleLoaded) @@ -342,15 +345,15 @@ class _SchedulePageState extends State { child: SafeArea( child: BlocConsumer( listener: (context, state) { - if (state is ScheduleActiveGroupEmpty) { - if (!_modalShown) { - // show after 300 ms - Future.delayed( - const Duration(milliseconds: 300), - () => _showModal(), - ); - } - } + // if (state is ScheduleActiveGroupEmpty) { + // if (!_modalShown) { + // // show after 300 ms + // Future.delayed( + // const Duration(milliseconds: 300), + // () => _showModal(), + // ); + // } + // } }, buildWhen: (prevState, currentState) { if (prevState is ScheduleLoaded && @@ -362,12 +365,12 @@ class _SchedulePageState extends State { builder: (context, state) { if (state is ScheduleLoading) { // Add post frame callback to hide modal after build - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_modalShown) { - _modalShown = false; - context.router.root.pop(); - } - }); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // if (_modalShown) { + // _modalShown = false; + // context.router.root.pop(); + // } + // }); return Center( child: CircularProgressIndicator( @@ -394,7 +397,10 @@ class _SchedulePageState extends State { ], ); } else { - return _NoActiveGroupFoundMessage(onTap: () => _showModal()); + // return _NoActiveGroupFoundMessage(onTap: () => _showModal()); + return _NoActiveGroupFoundMessage( + onTap: () => + context.router.push(const GroupsSelectRoute())); } }, ), diff --git a/lib/presentation/theme.dart b/lib/presentation/theme.dart index 8035b859..a5ec74c0 100644 --- a/lib/presentation/theme.dart +++ b/lib/presentation/theme.dart @@ -58,7 +58,7 @@ class AppTheme { onBackground: darkThemeColors.active, onSurface: darkThemeColors.active, onError: darkThemeColors.active, - onPrimary: darkThemeColors.active, + onPrimary: lightThemeColors.white, onSecondary: darkThemeColors.active, error: darkThemeColors.colorful07, ), @@ -100,7 +100,7 @@ class AppTheme { onBackground: lightThemeColors.active, onSurface: lightThemeColors.active, onError: lightThemeColors.active, - onPrimary: lightThemeColors.active, + onPrimary: lightThemeColors.white, onSecondary: lightThemeColors.active, error: lightThemeColors.colorful07, ), From 453ac94e942048b594dedf52566fe962a0586ed3 Mon Sep 17 00:00:00 2001 From: witelokk Date: Thu, 16 Feb 2023 16:21:12 +0300 Subject: [PATCH 34/38] fix: android widget --- .../main/kotlin/ninja/mirea/mireaapp/HomeWidgerProvider.kt | 4 ++-- .../ninja/mirea/mireaapp/widget_channel/HomeWidgetIntent.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/kotlin/ninja/mirea/mireaapp/HomeWidgerProvider.kt b/android/app/src/main/kotlin/ninja/mirea/mireaapp/HomeWidgerProvider.kt index b7053f9a..2136df92 100644 --- a/android/app/src/main/kotlin/ninja/mirea/mireaapp/HomeWidgerProvider.kt +++ b/android/app/src/main/kotlin/ninja/mirea/mireaapp/HomeWidgerProvider.kt @@ -29,7 +29,7 @@ class HomeWidgetProvider : AbstractHomeWidgetProvider() { // Reload widget every midnight val intent = Intent(context, HomeWidgetProvider::class.java) intent.action = ACTION_AUTO_UPDATE_WIDGET - val pIntent = PendingIntent.getBroadcast(context, 0, intent, 0) + val pIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) val alarmManager = context .getSystemService(Context.ALARM_SERVICE) as AlarmManager val c = Calendar.getInstance() @@ -124,7 +124,7 @@ class HomeWidgetProvider : AbstractHomeWidgetProvider() { super.onDisabled(context) val intent = Intent(ACTION_AUTO_UPDATE_WIDGET) val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - alarmMgr.cancel(PendingIntent.getBroadcast(context, 0, intent, 0)) + alarmMgr.cancel(PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)) } override fun onReceive(context: Context?, intent: Intent) { diff --git a/android/app/src/main/kotlin/ninja/mirea/mireaapp/widget_channel/HomeWidgetIntent.kt b/android/app/src/main/kotlin/ninja/mirea/mireaapp/widget_channel/HomeWidgetIntent.kt index 4fc9e912..fff145c9 100644 --- a/android/app/src/main/kotlin/ninja/mirea/mireaapp/widget_channel/HomeWidgetIntent.kt +++ b/android/app/src/main/kotlin/ninja/mirea/mireaapp/widget_channel/HomeWidgetIntent.kt @@ -19,7 +19,7 @@ object HomeWidgetLaunchIntent { intent.data = uri intent.action = HOME_WIDGET_LAUNCH_ACTION - return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } } @@ -32,6 +32,6 @@ object HomeWidgetBackgroundIntent { intent.data = uri intent.action = HOME_WIDGET_BACKGROUND_ACTION - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } } \ No newline at end of file From ad6685b84931071cf10538f4916ddca3dd8acc6c Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Thu, 16 Feb 2023 21:08:09 +0300 Subject: [PATCH 35/38] fix: Add check for android-specific code (#284) --- .../bloc/nfc_pass_bloc/nfc_pass_bloc.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart index da0745c0..8e90f071 100644 --- a/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart +++ b/lib/presentation/bloc/nfc_pass_bloc/nfc_pass_bloc.dart @@ -1,3 +1,4 @@ +import 'dart:io' show Platform; import 'package:bloc/bloc.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; @@ -34,10 +35,12 @@ class NfcPassBloc extends Bloc { required this.getUserData, required this.fetchNfcCode, }) : super(const _Initial()) { - on<_Started>(_onStarted); - on<_GetNfcPasses>(_onGetNfcPasses); - on<_ConnectNfcPass>(_onConnectNfcPass); - on<_FetchNfcCode>(_onFetchNfcCode); + if (Platform.isAndroid) { + on<_Started>(_onStarted); + on<_GetNfcPasses>(_onGetNfcPasses); + on<_ConnectNfcPass>(_onConnectNfcPass); + on<_FetchNfcCode>(_onFetchNfcCode); + } } void _onGetNfcPasses( From 926f6788b194f1c57c96cef947a42d8de8bd6bbb Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Thu, 16 Feb 2023 21:25:55 +0300 Subject: [PATCH 36/38] ui: Add popup with build number (#285) --- .../pages/profile/about_app_page.dart | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/lib/presentation/pages/profile/about_app_page.dart b/lib/presentation/pages/profile/about_app_page.dart index 447cb649..07c462ff 100644 --- a/lib/presentation/pages/profile/about_app_page.dart +++ b/lib/presentation/pages/profile/about_app_page.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -33,17 +34,58 @@ class AboutAppPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('Open Source', style: AppTextStyle.h4), - Container( - padding: const EdgeInsets.only( - left: 8, right: 8, top: 4, bottom: 4), - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(4)), - color: AppTheme.colors.primary, - ), - child: Text( - getIt().version, - style: AppTextStyle.buttonS, + PopupMenuButton( + color: AppTheme.colors.background03, + onSelected: (value) {}, + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Версия приложения:', + style: AppTextStyle.body, + ), + const SizedBox(height: 4), + Text( + getIt().version, + style: AppTextStyle.bodyRegular, + ), + ], + ), + ), + const PopupMenuDivider(), + PopupMenuItem( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Номер сборки:', + style: AppTextStyle.body, + ), + const SizedBox(height: 4), + Text( + getIt().buildNumber, + style: AppTextStyle.bodyRegular, + ), + ], + ), + ), + ]; + }, + child: Container( + padding: const EdgeInsets.only( + left: 8, right: 8, top: 4, bottom: 4), + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(4)), + color: AppTheme.colors.primary, + ), + child: Text( + getIt().version, + style: AppTextStyle.buttonS, + ), ), ), ]), From a6a1b668312d4bfca4e735bf18cbcdf6998f4941 Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Thu, 16 Feb 2023 21:45:52 +0300 Subject: [PATCH 37/38] refactor: Remove unused code --- lib/service_locator.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/service_locator.dart b/lib/service_locator.dart index ea80cae4..d05ebb64 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -253,6 +253,4 @@ Future setup() async { getIt.registerLazySingleton(() => LksOauth2()); final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); getIt.registerLazySingleton(() => deviceInfo); - final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - getIt.registerLazySingleton(() => androidInfo); } From 6ad9b6ece9953b3cc2d576b42c1e3d7be1d02b2c Mon Sep 17 00:00:00 2001 From: Sergey Dmitriev <51058739+0niel@users.noreply.github.com> Date: Sat, 18 Feb 2023 13:36:58 +0300 Subject: [PATCH 38/38] fix: White Screen Loading on iOS (#286) --- .../repositories/user_repository_impl.dart | 2 +- lib/main.dart | 17 +++++----- .../bloc/user_bloc/user_bloc.dart | 33 ++++++++----------- pubspec.yaml | 2 +- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/lib/data/repositories/user_repository_impl.dart b/lib/data/repositories/user_repository_impl.dart index 04be8294..c0311676 100644 --- a/lib/data/repositories/user_repository_impl.dart +++ b/lib/data/repositories/user_repository_impl.dart @@ -27,7 +27,7 @@ class UserRepositoryImpl implements UserRepository { if (await connectionChecker.hasConnection) { try { final authToken = await remoteDataSource.auth(); - localDataSource.setTokenToCache(authToken); + await localDataSource.setTokenToCache(authToken); return Right(authToken); } catch (e) { if (e is ServerException) { diff --git a/lib/main.dart b/lib/main.dart index dba1754b..ea1d7c20 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:io' show Platform; import 'package:auto_route/auto_route.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/foundation.dart'; @@ -122,7 +123,6 @@ class App extends StatelessWidget { BlocProvider( create: (context) => getIt()..add(const UserEvent.started()), - lazy: false, ), BlocProvider( create: (context) => getIt()), @@ -136,13 +136,14 @@ class App extends StatelessWidget { create: (_) => getIt(), lazy: false, // We need to init it as soon as possible ), - BlocProvider( - create: (_) => getIt() - ..add( - const NfcPassEvent.fetchNfcCode(), - ), - lazy: false, - ), + if (Platform.isAndroid) + BlocProvider( + create: (_) => getIt() + ..add( + const NfcPassEvent.fetchNfcCode(), + ), + lazy: false, + ), BlocProvider( create: (_) => getIt(), ), diff --git a/lib/presentation/bloc/user_bloc/user_bloc.dart b/lib/presentation/bloc/user_bloc/user_bloc.dart index b1d9065f..a9ae575d 100644 --- a/lib/presentation/bloc/user_bloc/user_bloc.dart +++ b/lib/presentation/bloc/user_bloc/user_bloc.dart @@ -33,15 +33,14 @@ class UserBloc extends Bloc { UserEvent event, Emitter emit, ) async { - if (state is _Loading) return; - - emit(const _Loading()); // We use oauth2 to get token. So we don't need to pass login and password // to the server. We just need to pass them to the oauth2 server. bool loggedIn = false; - (await logIn()).fold( + final logInRes = await logIn(); + + logInRes.fold( (failure) => emit(_LogInError( failure.cause ?? "Ошибка при авторизации. Повторите попытку")), (res) { @@ -50,6 +49,7 @@ class UserBloc extends Bloc { ); if (loggedIn) { + emit(const _Loading()); final user = await getUserData(); user.fold( @@ -74,27 +74,20 @@ class UserBloc extends Bloc { UserEvent event, Emitter emit, ) async { - // To get profile data only once (If state is not loading) - if (state is _Loading) return; - final token = await getAuthToken(); - bool loggedIn = false; + bool loggedIn = token.isRight(); + + if (!loggedIn) return; // If token in the storage, user is authorized at least once and we can // try to get user data - token.fold((failure) => emit(const _Unauthorized()), (r) { - loggedIn = true; - }); - - if (loggedIn) { - emit(const _Loading()); - final user = await getUserData(); + emit(const _Loading()); + final user = await getUserData(); - user.fold( - (failure) => emit(const _Unauthorized()), - (r) => emit(_LogInSuccess(r)), - ); - } + user.fold( + (failure) => emit(const _Unauthorized()), + (r) => emit(_LogInSuccess(r)), + ); } } diff --git a/pubspec.yaml b/pubspec.yaml index ea053662..895631f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ publish_to: 'none' # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.3.0+12 +version: 1.3.0+15 environment: sdk: ">=2.19.0 <3.0.0"