diff --git a/ios/Podfile b/ios/Podfile
index 3e32eec2..c4f7a36b 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -37,6 +37,64 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
+ target.build_configurations.each do |build_configuration|
+ # GoogleSignIn does not support arm64 simulators.
+ # https://github.com/flutter/flutter/issues/85713
+ build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386'
+
+ # You can enable the permissions needed here. For example to enable camera
+ # permission, just remove the `#` character in front so it looks like this:
+ #
+ # ## dart: PermissionGroup.camera
+ # 'PERMISSION_CAMERA=1'
+ #
+ # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h
+ build_configuration.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
+ '$(inherited)',
+
+ ## dart: PermissionGroup.calendar
+ # 'PERMISSION_EVENTS=1',
+
+ ## dart: PermissionGroup.reminders
+ # 'PERMISSION_REMINDERS=1',
+
+ ## dart: PermissionGroup.contacts
+ # 'PERMISSION_CONTACTS=1',
+
+ ## dart: PermissionGroup.camera
+ # 'PERMISSION_CAMERA=1',
+
+ ## dart: PermissionGroup.microphone
+ # 'PERMISSION_MICROPHONE=1',
+
+ ## dart: PermissionGroup.speech
+ # 'PERMISSION_SPEECH_RECOGNIZER=1',
+
+ ## dart: PermissionGroup.photos
+ # 'PERMISSION_PHOTOS=1',
+
+ ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
+ # 'PERMISSION_LOCATION=1',
+
+ ## dart: PermissionGroup.notification
+ 'PERMISSION_NOTIFICATIONS=1',
+
+ ## dart: PermissionGroup.mediaLibrary
+ # 'PERMISSION_MEDIA_LIBRARY=1',
+
+ ## dart: PermissionGroup.sensors
+ # 'PERMISSION_SENSORS=1',
+
+ ## dart: PermissionGroup.bluetooth
+ # 'PERMISSION_BLUETOOTH=1',
+
+ ## dart: PermissionGroup.appTrackingTransparency
+ # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1',
+
+ ## dart: PermissionGroup.criticalAlerts
+ # 'PERMISSION_CRITICAL_ALERTS=1'
+ ]
+ end
end
end
$FirebaseAnalyticsWithoutAdIdSupport = true
diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist
new file mode 100644
index 00000000..a1232f0d
--- /dev/null
+++ b/ios/Runner/GoogleService-Info.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ CLIENT_ID
+ 510978291920-31sgk97k4bifhc0ebpamk9m46om2e50r.apps.googleusercontent.com
+ REVERSED_CLIENT_ID
+ com.googleusercontent.apps.510978291920-31sgk97k4bifhc0ebpamk9m46om2e50r
+ API_KEY
+ AIzaSyAYZ5JlWF94jBGrcds7fSi5uMN1zmuieec
+ GCM_SENDER_ID
+ 510978291920
+ PLIST_VERSION
+ 1
+ BUNDLE_ID
+ mirea.ninja.mireaapp
+ PROJECT_ID
+ rtu-mirea-app
+ STORAGE_BUCKET
+ rtu-mirea-app.appspot.com
+ IS_ADS_ENABLED
+
+ IS_ANALYTICS_ENABLED
+
+ IS_APPINVITE_ENABLED
+
+ IS_GCM_ENABLED
+
+ IS_SIGNIN_ENABLED
+
+ GOOGLE_APP_ID
+ 1:510978291920:ios:dd9496a1680c72828c46d5
+
+
\ No newline at end of file
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 69abe59a..24a33c71 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -2,6 +2,8 @@
+ FLTEnableImpeller
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
@@ -61,5 +63,9 @@
CADisableMinimumFrameDurationOnPhone
+ UIApplicationSupportsIndirectInputEvents
+
+ PermissionGroupNotification
+ Приложение запрашивает разрешение на отправку уведомлений
diff --git a/lib/common/utils/schedule_utils.dart b/lib/common/utils/schedule_utils.dart
index eff2a8b4..690c8e93 100644
--- a/lib/common/utils/schedule_utils.dart
+++ b/lib/common/utils/schedule_utils.dart
@@ -21,9 +21,9 @@ class ScheduleUtils {
"14:20": 4,
"16:20": 5,
"18:00": 6,
- "19:40": 7,
"18:30": 7,
- "20:10": 8,
+ "19:40": 8,
+ "20:10": 9,
};
static Map get universityTimesEnd => const {
@@ -33,9 +33,9 @@ class ScheduleUtils {
"15:50": 4,
"17:50": 5,
"19:30": 6,
- "21:00": 7,
"20:00": 7,
- "21:40": 8,
+ "21:00": 8,
+ "21:40": 9,
};
static bool isCollegeGroup(String group) {
diff --git a/lib/main.dart b/lib/main.dart
index edaaa3a8..b58323e2 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,5 +1,4 @@
import 'dart:io' show Platform;
-import 'package:auto_route/auto_route.dart';
import 'package:dio/dio.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/foundation.dart';
@@ -7,6 +6,7 @@ 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:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:rtu_mirea_app/common/oauth.dart';
@@ -22,13 +22,13 @@ 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/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/notification_preferences/notification_preferences_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';
import 'package:rtu_mirea_app/service_locator.dart' as dependency_injection;
@@ -60,14 +60,14 @@ class GlobalBlocObserver extends BlocObserver {
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
- await dependency_injection.setup();
-
- WidgetDataProvider.initData();
-
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
+ await dependency_injection.setup();
+
+ WidgetDataProvider.initData();
+
if (Platform.isAndroid || Platform.isIOS) {
await FirebaseAnalytics.instance.logAppOpen();
}
@@ -137,10 +137,10 @@ Future main() async {
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
- static final appRouter = AppRouter();
-
@override
Widget build(BuildContext context) {
+ final router = getIt();
+
// blocking the orientation of the application to
// vertical only
SystemChrome.setPreferredOrientations([
@@ -182,6 +182,9 @@ class App extends StatelessWidget {
BlocProvider(
create: (_) => getIt(),
),
+ BlocProvider(
+ create: (_) => getIt(),
+ ),
],
child: Consumer(
builder: (BuildContext context, AppNotifier value, Widget? child) {
@@ -198,19 +201,7 @@ class App extends StatelessWidget {
locale: const Locale('ru'),
debugShowCheckedModeBanner: false,
title: 'Приложение РТУ МИРЭА',
- routerDelegate: appRouter.delegate(
- navigatorObservers: () => [
- FirebaseAnalyticsObserver(
- analytics: FirebaseAnalytics.instance,
- ),
- AutoRouteObserver(),
- SentryNavigatorObserver(
- autoFinishAfter: const Duration(seconds: 5),
- setRouteNameAsTransaction: true),
- ],
- ),
- routeInformationProvider: appRouter.routeInfoProvider(),
- routeInformationParser: appRouter.defaultRouteParser(),
+ routerConfig: router,
themeMode: AppTheme.themeMode,
theme: AppTheme.theme,
darkTheme: AppTheme.darkTheme,
diff --git a/lib/presentation/bloc/notification_preferences/notification_preferences_bloc.dart b/lib/presentation/bloc/notification_preferences/notification_preferences_bloc.dart
new file mode 100644
index 00000000..29b36ea7
--- /dev/null
+++ b/lib/presentation/bloc/notification_preferences/notification_preferences_bloc.dart
@@ -0,0 +1,278 @@
+import 'dart:async';
+
+import 'package:equatable/equatable.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:notifications_repository/notifications_repository.dart';
+
+part 'notification_preferences_state.dart';
+part 'notification_preferences_event.dart';
+
+enum Category {
+ /// Общеуниверситетские объявления.
+ announcements,
+
+ /// Уведомления, связанные с изменениями в расписании. Привязаны к группе.
+ scheduleUpdates,
+
+ /// Уведомления для конкретной группы. Обязательная категория, которую
+ /// пользователь не может отключить.
+ group,
+}
+
+const visibleCategoryNames = {
+ Category.announcements: 'Объявления',
+ Category.scheduleUpdates: 'Обновления расписания',
+};
+
+/// Транслитерирует название группы для использования в качестве названия
+/// категории уведомлений.
+String transletirateGroupName(String groupName) {
+ final mappings = {
+ 'А': 'A',
+ 'Б': 'B',
+ 'В': 'V',
+ 'Г': 'G',
+ 'Д': 'D',
+ 'Е': 'E',
+ 'Ё': 'E',
+ 'Ж': 'Zh',
+ 'З': 'Z',
+ 'И': 'I',
+ 'Й': 'I',
+ 'К': 'K',
+ 'Л': 'L',
+ 'М': 'M',
+ 'Н': 'N',
+ 'О': 'O',
+ 'П': 'P',
+ 'Р': 'R',
+ 'С': 'S',
+ 'Т': 'T',
+ 'У': 'U',
+ 'Ф': 'F',
+ 'Х': 'H',
+ 'Ц': 'Ts',
+ 'Ч': 'Ch',
+ 'Ш': 'Sh',
+ 'Щ': 'Sch',
+ 'Ъ': '',
+ 'Ы': 'Y',
+ 'Ь': '',
+ 'Э': 'E',
+ 'Ю': 'Ju',
+ 'Я': 'Ja',
+ };
+
+ return groupName
+ .split('-')
+ .map((word) =>
+ word.split('').map((char) => mappings[char] ?? char).join(''))
+ .join('-');
+}
+
+/// Категория уведомлений. [toString] возвращает название категории, которое
+/// используется при подписке на уведомления. [fromString] возвращает объект
+/// [Topic] из названия категории.
+class Topic extends Equatable {
+ Topic({
+ required this.topic,
+ String? groupName,
+ }) {
+ if (topic == Category.group || topic == Category.scheduleUpdates) {
+ assert(groupName != null);
+
+ this.groupName = transletirateGroupName(groupName ?? '');
+ } else {
+ this.groupName = null;
+ }
+ }
+
+ final Category topic;
+ late final String? groupName;
+
+ @override
+ String toString() {
+ switch (topic) {
+ case Category.announcements:
+ return 'Announcements';
+ case Category.scheduleUpdates:
+ return 'ScheduleUpdates__${groupName!}';
+ case Category.group:
+ return groupName!;
+ }
+ }
+
+ String getVisibleName() {
+ switch (topic) {
+ case Category.announcements:
+ return visibleCategoryNames[Category.announcements]!;
+ case Category.scheduleUpdates:
+ return visibleCategoryNames[Category.scheduleUpdates]!;
+ case Category.group:
+ return groupName!;
+ }
+ }
+
+ static Topic fromVisibleName(String name, String groupName) {
+ switch (name) {
+ case 'Объявления':
+ return Topic(topic: Category.announcements);
+ case 'Обновления расписания':
+ return Topic(
+ topic: Category.scheduleUpdates,
+ groupName: groupName,
+ );
+ default:
+ return Topic(
+ topic: Category.group,
+ groupName: name,
+ );
+ }
+ }
+
+ static Topic fromString(String category) {
+ final categoryParts = category.split('__');
+ final topic = categoryParts[0];
+
+ switch (topic) {
+ case 'Announcements':
+ return Topic(topic: Category.announcements);
+ case 'ScheduleUpdates':
+ return Topic(
+ topic: Category.scheduleUpdates,
+ groupName: categoryParts[1],
+ );
+ default:
+ return Topic(
+ topic: Category.group,
+ groupName: category,
+ );
+ }
+ }
+
+ @override
+ List